1565 行
40 KiB
Vue
1565 行
40 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 && !categories.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 categories" :key="c.id" class="home__cat" @click="goCategory(c)">
|
||
<view class="home__cat-icon">
|
||
<image class="home__cat-badge" :src="catIcon(c)" mode="aspectFit" />
|
||
</view>
|
||
<text class="home__cat-name">{{ c.name }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 会员引导条 -->
|
||
<view class="home__vip" @click="go('/pages/member/index')">
|
||
<view class="home__vip-left">
|
||
<image class="fc-icon fc-icon--md" :src="iconUrl('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">{{ 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>
|
||
</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" />
|
||
|
||
<!-- ============================================================
|
||
左上角位置手动替换面板(春绿生态风格半屏抽屉)
|
||
========================================================== -->
|
||
<view v-if="showLocModal" class="home__modal-mask" @click="closeLocationModal" @touchmove.stop.prevent>
|
||
<view class="home__modal-content" @click.stop>
|
||
<view class="home__modal-handle"></view>
|
||
<view class="home__modal-head">
|
||
<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">
|
||
<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>
|
||
|
||
<!-- 快捷区县网格(当前城市各区县直达) -->
|
||
<view class="home__modal-section">
|
||
<view class="home__modal-section-title">
|
||
<text class="home__modal-section-main">常用与同城区县快捷选择</text>
|
||
<text class="home__modal-section-tip">点击即刻切换生效</text>
|
||
</view>
|
||
<view class="home__modal-chip-grid">
|
||
<!-- 全部区域 -->
|
||
<view
|
||
class="home__modal-chip"
|
||
:class="{ 'home__modal-chip--active': !areaId }"
|
||
@click="selectAreaItem(null)"
|
||
>
|
||
<text class="home__modal-chip-text">全部区域</text>
|
||
</view>
|
||
<!-- 各开通区县 -->
|
||
<view
|
||
v-for="item in quickAreaList"
|
||
:key="item.id"
|
||
class="home__modal-chip"
|
||
:class="{ 'home__modal-chip--active': areaId === Number(item.id) }"
|
||
@click="selectAreaItem(item)"
|
||
>
|
||
<text class="home__modal-chip-text">{{ item.district }}</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 切换其他省市/区县(三级级联选择器) -->
|
||
<picker
|
||
mode="multiSelector"
|
||
:range="pickerColumns"
|
||
range-key="label"
|
||
:value="pickerIndex"
|
||
@columnchange="onPickerColumnChange"
|
||
@change="onPickerChange"
|
||
>
|
||
<view class="home__modal-picker-trigger">
|
||
<view class="home__modal-picker-left">
|
||
<image class="home__modal-picker-icon" :src="iconUrl('category')" mode="aspectFit" />
|
||
<view class="home__modal-picker-text-wrap">
|
||
<text class="home__modal-picker-label">选择其他省市或区县</text>
|
||
<text class="home__modal-picker-hint">浏览所有已开通城市与区县列表</text>
|
||
</view>
|
||
</view>
|
||
<view class="home__modal-picker-right">
|
||
<text class="home__modal-picker-arrow">›</text>
|
||
</view>
|
||
</view>
|
||
</picker>
|
||
</view>
|
||
</view>
|
||
</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 FcAreaPicker from '@/components/fc-area-picker/fc-area-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 { bannerArtUrl, bannerHasValue, bannerBg } from '@/common/assets/banners.js'
|
||
import { getNavMetrics } from '@/common/utils/navbar.js'
|
||
import { groupAreaTree } from '@/common/utils/area.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_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, FcAreaPicker },
|
||
data() {
|
||
// 优先从本地缓存提取首屏数据,达到即开即显零白屏体验
|
||
const cachedBanners = safeGetStorage(STORAGE_KEY_BANNERS, [])
|
||
const cachedCats = safeGetStorage(STORAGE_KEY_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: '/static/banners/banner-1.png', link: '/pages/category/channel?categoryId=3' },
|
||
{ id: 2, title: '同城高薪直招', image: '/static/banners/banner-2.png', link: '/pages/post/create' },
|
||
{ id: 3, title: '本地便民到家', image: '/static/banners/banner-3.png', link: '/pages/category/channel?categoryId=5' },
|
||
],
|
||
categories: Array.isArray(cachedCats) ? cachedCats : [],
|
||
list: [],
|
||
page: 1,
|
||
size: 10,
|
||
hasMore: true,
|
||
loading: false,
|
||
sort: 'new',
|
||
// 服务区域筛选(若用户曾手动选择,直接恢复)
|
||
areaId: savedUserArea ? Number(savedUserArea.areaId || 0) : 0,
|
||
areaText: savedUserArea ? (savedUserArea.district || '') : '',
|
||
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' },
|
||
],
|
||
// ---------- 位置手动替换弹窗控制 ----------
|
||
showLocModal: false,
|
||
isLocating: false,
|
||
areaTree: [],
|
||
pickerColumns: [[], [], []],
|
||
pickerIndex: [0, 0, 0],
|
||
}
|
||
},
|
||
computed: {
|
||
/**
|
||
* 用户是否是有效会员
|
||
*/
|
||
isMember() {
|
||
try {
|
||
return useUserStore().isMember.value
|
||
} catch (e) {
|
||
return false
|
||
}
|
||
},
|
||
/**
|
||
* 会员引导条主标题
|
||
*/
|
||
vipTitle() {
|
||
if (this.isMember) return '会员权益已生效'
|
||
const discount = this.benefits && this.benefits.postDiscount
|
||
return discount ? `开通会员,发布${discount}` : '开通会员,享专属特权'
|
||
},
|
||
/**
|
||
* 顶栏显示的城市或区域文案(优先当前选择区县,其次城市,缺省回退到同城服务)
|
||
*/
|
||
currentCityText() {
|
||
if (this.areaText) return this.areaText
|
||
if (this.location) return this.location
|
||
const first = (this.areaRows || [])[0]
|
||
if (first && (first.district || first.city)) {
|
||
return first.district || first.city
|
||
}
|
||
return '同城服务'
|
||
},
|
||
/**
|
||
* 位置弹窗内的状态描述文案
|
||
*/
|
||
locStatusDesc() {
|
||
if (this.manualArea) {
|
||
return '当前由您手动锁定展示该区域信息'
|
||
}
|
||
if (this.lat != null && this.lng != null) {
|
||
return '已基于您的地理位置自动匹配同城区县'
|
||
}
|
||
return '暂未获取定位,展示平台推荐区域'
|
||
},
|
||
/**
|
||
* 快捷区县选择列表:优先展示与当前城市相同的区县,便于快速直达
|
||
*/
|
||
quickAreaList() {
|
||
if (!Array.isArray(this.areaRows) || !this.areaRows.length) return []
|
||
const targetCity = this.location || (this.areaRows[0] && this.areaRows[0].city) || ''
|
||
if (targetCity) {
|
||
const filtered = this.areaRows.filter((r) => r.city === targetCity)
|
||
if (filtered.length) return filtered
|
||
}
|
||
return this.areaRows.slice(0, 12)
|
||
},
|
||
/**
|
||
* 会员引导条副标题
|
||
*/
|
||
vipSub() {
|
||
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} 起` : '超值会员权益,立即开通'
|
||
},
|
||
},
|
||
watch: {
|
||
areaRows: {
|
||
handler(rows) {
|
||
this.rebuildAreaTree(rows)
|
||
},
|
||
deep: true,
|
||
immediate: true,
|
||
},
|
||
},
|
||
onLoad() {
|
||
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')
|
||
},
|
||
onShareAppMessage() {
|
||
return {
|
||
title: uiState.shareTitle || uiState.siteName || '同城分类信息',
|
||
path: '/pages/index/index',
|
||
}
|
||
},
|
||
onShareTimeline() {
|
||
return {
|
||
title: uiState.shareTitle || uiState.siteName || '同城分类信息',
|
||
query: '',
|
||
}
|
||
},
|
||
/**
|
||
* 下拉刷新优化:保留页面显示并平滑拉取最新数据
|
||
*/
|
||
onPullDownRefresh() {
|
||
this.page = 1
|
||
this.hasMore = true
|
||
loadUiConfig(true)
|
||
this.bootstrap(true).finally(() => uni.stopPullDownRefresh())
|
||
},
|
||
onReachBottom() {
|
||
this.loadMore()
|
||
},
|
||
methods: {
|
||
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) {
|
||
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 pAreas = this.loadAreas()
|
||
const pFeed = this.fetchFeed(true)
|
||
const pMember = this.loadMemberCard()
|
||
const pUnread = this.loadUnread()
|
||
|
||
try {
|
||
await Promise.allSettled([pBanners, pCategories, 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)
|
||
this.rebuildAreaTree(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 oldId = this.areaId
|
||
this.areaId = Number(found.areaId) || 0
|
||
this.areaText = found.district || ''
|
||
this.location = found.city || ''
|
||
// 若匹配出的区域有变动,平滑刷新信息流
|
||
if (oldId !== this.areaId) {
|
||
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,
|
||
sort: this.sort,
|
||
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 (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()
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 中间筛选栏区域变化(与左上角位置双向同步)
|
||
* @param {Object|null} area 选中的区域
|
||
*/
|
||
onFilterAreaChange(area) {
|
||
this.applyAreaSelection(area, true)
|
||
},
|
||
|
||
/**
|
||
* 打开左上角位置手动替换弹窗
|
||
*/
|
||
openLocationModal() {
|
||
this.showLocModal = true
|
||
this.syncPickerFromValue()
|
||
},
|
||
|
||
/**
|
||
* 关闭位置切换弹窗
|
||
*/
|
||
closeLocationModal() {
|
||
this.showLocModal = false
|
||
},
|
||
|
||
/**
|
||
* 手动选择具体区域项(来自弹窗快捷标签)
|
||
* @param {Object|null} item 区县对象(null 表示全部区域)
|
||
*/
|
||
selectAreaItem(item) {
|
||
if (!item) {
|
||
this.applyAreaSelection(null, true)
|
||
} else {
|
||
this.applyAreaSelection({
|
||
areaId: Number(item.id) || 0,
|
||
district: item.district || '',
|
||
city: item.city || '',
|
||
province: item.province || '',
|
||
}, true)
|
||
}
|
||
this.closeLocationModal()
|
||
},
|
||
|
||
/**
|
||
* 统一应用区域选择并持久化(供手动选区与筛选栏共享)
|
||
* @param {Object|null} area 区域数据
|
||
* @param {boolean} isManual 是否为用户主动操作
|
||
*/
|
||
applyAreaSelection(area, isManual = true) {
|
||
if (!area) {
|
||
this.areaId = 0
|
||
this.areaText = ''
|
||
this.manualArea = isManual
|
||
if (isManual) {
|
||
safeSetStorage(STORAGE_KEY_SELECTED_AREA, { areaId: 0, district: '', city: this.location || '全部区域' })
|
||
}
|
||
} else {
|
||
this.areaId = Number(area.areaId) || 0
|
||
this.areaText = area.district || ''
|
||
if (area.city) {
|
||
this.location = area.city
|
||
}
|
||
this.manualArea = isManual
|
||
if (isManual) {
|
||
safeSetStorage(STORAGE_KEY_SELECTED_AREA, {
|
||
areaId: this.areaId,
|
||
district: this.areaText,
|
||
city: this.location,
|
||
province: area.province || '',
|
||
})
|
||
}
|
||
}
|
||
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 = Number(found.areaId) || 0
|
||
this.areaText = found.district || ''
|
||
this.location = found.city || ''
|
||
this.page = 1
|
||
this.fetchFeed(true)
|
||
uni.showToast({ title: `已定位到:${found.district || 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 {Array} rows 平铺区域列表
|
||
*/
|
||
rebuildAreaTree(rows) {
|
||
if (!Array.isArray(rows) || !rows.length) return
|
||
this.areaTree = groupAreaTree(rows)
|
||
this.buildPickerColumns()
|
||
},
|
||
|
||
/**
|
||
* 按选中区域同步三级联动索引
|
||
*/
|
||
syncPickerFromValue() {
|
||
if (!this.areaTree || !this.areaTree.length) return
|
||
let pi = 0
|
||
let ci = 0
|
||
let di = 0
|
||
if (this.areaId > 0) {
|
||
let matched = false
|
||
for (let p = 0; p < this.areaTree.length && !matched; p++) {
|
||
const cities = this.areaTree[p].cities || []
|
||
for (let c = 0; c < cities.length && !matched; c++) {
|
||
const districts = cities[c].districts || []
|
||
for (let d = 0; d < districts.length; d++) {
|
||
if (Number(districts[d].id) === this.areaId) {
|
||
pi = p
|
||
ci = c
|
||
di = d
|
||
matched = true
|
||
break
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
this.pickerIndex = [pi, ci, di]
|
||
this.buildPickerColumns()
|
||
},
|
||
|
||
/**
|
||
* 构建三级选择器列数据
|
||
*/
|
||
buildPickerColumns() {
|
||
if (!this.areaTree || !this.areaTree.length) {
|
||
this.pickerColumns = [[], [], []]
|
||
return
|
||
}
|
||
const pi = Math.max(0, Number(this.pickerIndex[0]) || 0)
|
||
const ci = Math.max(0, Number(this.pickerIndex[1]) || 0)
|
||
const province = this.areaTree[pi] || this.areaTree[0]
|
||
const cities = (province && province.cities) || []
|
||
const city = cities[ci] || cities[0] || {}
|
||
const districts = city.districts || []
|
||
|
||
this.pickerColumns = [
|
||
this.areaTree.map((p) => ({ label: p.name, value: p.name })),
|
||
cities.map((c) => ({ label: c.name, value: c.name })),
|
||
districts.map((d) => ({ label: d.name, id: d.id, item: d })),
|
||
]
|
||
},
|
||
|
||
/**
|
||
* 级联选择器滚动列联动
|
||
* @param {Object} e 事件对象
|
||
*/
|
||
onPickerColumnChange(e) {
|
||
const col = Number(e.detail.column)
|
||
const val = Number(e.detail.value)
|
||
const next = [...this.pickerIndex]
|
||
next[col] = val
|
||
if (col === 0) {
|
||
next[1] = 0
|
||
next[2] = 0
|
||
} else if (col === 1) {
|
||
next[2] = 0
|
||
}
|
||
this.pickerIndex = next
|
||
this.buildPickerColumns()
|
||
},
|
||
|
||
/**
|
||
* 级联选择器确认选中
|
||
* @param {Object} e 事件对象
|
||
*/
|
||
onPickerChange(e) {
|
||
const vals = e.detail.value || []
|
||
const pi = Number(vals[0] || 0)
|
||
const ci = Number(vals[1] || 0)
|
||
const di = Number(vals[2] || 0)
|
||
const province = this.areaTree[pi]
|
||
const city = province && province.cities ? province.cities[ci] : null
|
||
const district = city && city.districts ? city.districts[di] : null
|
||
if (district) {
|
||
this.applyAreaSelection({
|
||
areaId: Number(district.id) || 0,
|
||
district: district.name,
|
||
city: city.name,
|
||
province: province.name,
|
||
}, true)
|
||
this.closeLocationModal()
|
||
}
|
||
},
|
||
|
||
/**
|
||
* 页面跳转助手
|
||
* @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) {
|
||
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 {
|
||
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;
|
||
box-shadow: $fc-shadow;
|
||
}
|
||
.home__vip-left {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
.home__vip-texts {
|
||
display: flex;
|
||
flex-direction: column;
|
||
margin-left: 12rpx;
|
||
}
|
||
.home__vip-title {
|
||
color: #ffffff;
|
||
font-size: $fc-font-lg;
|
||
font-weight: 700;
|
||
line-height: $fc-leading-tight;
|
||
}
|
||
.home__vip-sub {
|
||
color: rgba(255, 255, 255, 0.9);
|
||
font-size: $fc-font-xs;
|
||
margin-top: 8rpx;
|
||
}
|
||
.home__vip-btn {
|
||
padding: 12rpx 28rpx;
|
||
border-radius: $fc-radius-pill;
|
||
background: #ffffff;
|
||
color: $fc-primary-text;
|
||
font-size: $fc-font-sm;
|
||
font-weight: 700;
|
||
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;
|
||
&--active {
|
||
color: $fc-primary-text;
|
||
font-weight: 700;
|
||
}
|
||
}
|
||
.home__filter-area {
|
||
margin-left: auto;
|
||
}
|
||
|
||
/* 拟真骨架屏列表 */
|
||
.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;
|
||
}
|
||
|
||
/* ============================================================
|
||
位置手动替换弹窗样式(春绿生态风格 Bottom Sheet)
|
||
========================================================== */
|
||
.home__modal-mask {
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
background: rgba(0, 0, 0, 0.45);
|
||
backdrop-filter: blur(4px);
|
||
z-index: 999;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: flex-end;
|
||
animation: modalFadeIn 0.22s ease-out;
|
||
}
|
||
.home__modal-content {
|
||
background: #ffffff;
|
||
border-radius: 36rpx 36rpx 0 0;
|
||
padding: 20rpx 32rpx calc(48rpx + env(safe-area-inset-bottom));
|
||
animation: modalSlideUp 0.26s cubic-bezier(0.16, 1, 0.3, 1);
|
||
box-shadow: 0 -8rpx 36rpx rgba(4, 48, 30, 0.12);
|
||
}
|
||
.home__modal-handle {
|
||
width: 72rpx;
|
||
height: 8rpx;
|
||
background: #dcece3;
|
||
border-radius: 999rpx;
|
||
margin: 0 auto 24rpx;
|
||
}
|
||
.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;
|
||
}
|
||
}
|
||
|
||
/* 快捷区县网格 */
|
||
.home__modal-section {
|
||
margin-bottom: 28rpx;
|
||
}
|
||
.home__modal-section-title {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 18rpx;
|
||
}
|
||
.home__modal-section-main {
|
||
font-size: 26rpx;
|
||
font-weight: 700;
|
||
color: $fc-text;
|
||
}
|
||
.home__modal-section-tip {
|
||
font-size: 22rpx;
|
||
color: $fc-text-placeholder;
|
||
}
|
||
.home__modal-chip-grid {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 16rpx;
|
||
max-height: 380rpx;
|
||
overflow-y: auto;
|
||
}
|
||
.home__modal-chip {
|
||
padding: 14rpx 26rpx;
|
||
border-radius: 999rpx;
|
||
background: #f4fbf7;
|
||
border: 1rpx solid #d5efe1;
|
||
transition: all 0.15s ease;
|
||
&:active {
|
||
transform: scale(0.96);
|
||
}
|
||
&--active {
|
||
background: #00e070;
|
||
border-color: #00e070;
|
||
box-shadow: 0 4rpx 14rpx rgba(0, 224, 112, 0.35);
|
||
.home__modal-chip-text {
|
||
color: $fc-primary-ink;
|
||
font-weight: 700;
|
||
}
|
||
}
|
||
}
|
||
.home__modal-chip-text {
|
||
font-size: 26rpx;
|
||
color: $fc-text-regular;
|
||
}
|
||
|
||
/* 三级联动选择器触发入口 */
|
||
.home__modal-picker-trigger {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 24rpx 24rpx;
|
||
background: #f7fdfa;
|
||
border: 1rpx dashed #a8e5c3;
|
||
border-radius: 20rpx;
|
||
margin-top: 8rpx;
|
||
&:active {
|
||
background: #edfbf3;
|
||
}
|
||
}
|
||
.home__modal-picker-left {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
.home__modal-picker-icon {
|
||
width: 40rpx;
|
||
height: 40rpx;
|
||
margin-right: 16rpx;
|
||
}
|
||
.home__modal-picker-text-wrap {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
.home__modal-picker-label {
|
||
font-size: 28rpx;
|
||
font-weight: 600;
|
||
color: $fc-primary-text;
|
||
}
|
||
.home__modal-picker-hint {
|
||
font-size: 22rpx;
|
||
color: $fc-text-secondary;
|
||
margin-top: 4rpx;
|
||
}
|
||
.home__modal-picker-right {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
.home__modal-picker-arrow {
|
||
font-size: 34rpx;
|
||
color: #619a7d;
|
||
font-weight: 600;
|
||
}
|
||
|
||
/* 动效 */
|
||
@keyframes modalFadeIn {
|
||
from { opacity: 0; }
|
||
to { opacity: 1; }
|
||
}
|
||
@keyframes modalSlideUp {
|
||
from { transform: translateY(100%); }
|
||
to { transform: translateY(0); }
|
||
}
|
||
@keyframes spin {
|
||
from { transform: rotate(0deg); }
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
</style>
|