文件
fenleixinxi/uniapp/pages/category/index.vue
MiAn d812e54f8e build: 更新正式环境后端域名并删除无用公钥文件
删除了不再使用的公钥文件 pub_key.pem,同时将生产环境的后端域名从原地址更新为新的正式域名,确保项目配置与线上环境匹配。
2026-10-30 15:23:27 +08:00

1927 行
51 KiB
Vue

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

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

<template>
<view class="cat-page fc-page">
<!-- 规范化统一全绿顶部导航栏:与通知中心统一全绿质感与居中标题 -->
<fc-navbar title="全部分类" theme="gradient" />
<!-- 顶栏通栏直接搜索:在当前分类内直接检索,无需跳转任何页面 -->
<view class="cat-search-bar">
<view class="cat-search-bar__inner">
<image class="fc-icon fc-icon--sm" :src="iconUrl('search')" mode="aspectFit" />
<input
v-model="keyword"
class="cat-search-bar__input"
:placeholder="searchPlaceholder"
placeholder-class="cat-search-bar__ph"
confirm-type="search"
@confirm="handleSearch"
/>
<view v-if="keyword" class="cat-search-bar__clear" @click="handleClear">
<text class="cat-search-bar__clear-txt">×</text>
</view>
</view>
<view class="cat-search-bar__btn" @click="handleSearch">
<text class="cat-search-bar__btn-txt">搜索</text>
</view>
</view>
<!-- 经典双联主体区域 -->
<view class="cat-main">
<!-- 左侧一级分类垂直菜单(支持骨架微光占位) -->
<scroll-view class="cat-sidebar" scroll-y :show-scrollbar="false">
<template v-if="categories.length">
<view
v-for="c in categories"
:key="c.id"
class="cat-sidebar__item"
:class="{ 'cat-sidebar__item--active': currentCategory && currentCategory.id === c.id }"
@click="selectCategory(c)"
>
<view v-if="currentCategory && currentCategory.id === c.id" class="cat-sidebar__bar"></view>
<image class="cat-sidebar__icon" :src="catIcon(c)" mode="aspectFit" />
<text class="cat-sidebar__name">{{ c.name }}</text>
</view>
</template>
<template v-else>
<view v-for="n in 5" :key="n" class="cat-sidebar__item cat-sidebar__item--skel">
<view class="cat-sidebar__skel-icon pulse-shimmer"></view>
<view class="cat-sidebar__skel-name pulse-shimmer"></view>
</view>
</template>
</scroll-view>
<!-- 右侧内容区域(支持下拉刷新、骨架屏微光缓冲、平滑切换) -->
<scroll-view
class="cat-content"
scroll-y
:show-scrollbar="false"
refresher-enabled
:refresher-triggered="isRefreshing"
refresher-background="#f2fef7"
@refresherrefresh="onPullDownRefresh"
@scrolltolower="loadMore"
>
<!-- 二级分类图文宫格(3×3 固定展示:超出在该区域内上下滚动,高度不随分类数变化,杜绝页面跳动) -->
<view v-if="currentSubCategories.length" class="cat-subcard">
<view class="cat-subcard__head">
<view class="cat-subcard__head-left">
<text class="cat-subcard__title">{{ categorySectionTitle }}</text>
<view v-if="currentSubCategory" class="cat-subcard__active-pill" @click="selectSubCategory(null)">
<text class="cat-subcard__active-pill-txt">{{ currentSubCategory.name }}</text>
<text class="cat-subcard__active-pill-close">✕</text>
</view>
</view>
<!-- 右上角二级品类微搜索 -->
<view class="cat-subcard__search">
<image class="cat-subcard__search-icon" :src="iconUrl('search')" mode="aspectFit" />
<input
v-model="subKeyword"
class="cat-subcard__search-input"
placeholder="找品类…"
placeholder-class="cat-subcard__search-ph"
/>
<view v-if="subKeyword" class="cat-subcard__search-clear" @click.stop="subKeyword = ''">
<text class="cat-subcard__search-clear-txt">×</text>
</view>
</view>
</view>
<scroll-view
class="cat-subgrid-scroll"
scroll-y
:show-scrollbar="false"
:style="(displaySubCategories.length + (subKeyword ? 0 : 1)) > 9 ? 'height: 474rpx' : ''"
>
<view v-if="displaySubCategories.length > 0 || !subKeyword" class="cat-subgrid">
<view
v-if="!subKeyword"
class="cat-subgrid__item"
:class="{ 'cat-subgrid__item--active': !currentSubCategory }"
@click="selectSubCategory(null)"
>
<view class="cat-subgrid__thumb">
<image class="cat-subgrid__img" :src="catIcon(currentCategory)" mode="aspectFill" />
<view v-if="!currentSubCategory" class="cat-subgrid__badge">✓</view>
</view>
<text class="cat-subgrid__name">全部</text>
</view>
<view
v-for="sub in displaySubCategories"
:key="sub.id"
class="cat-subgrid__item"
:class="{ 'cat-subgrid__item--active': currentSubCategory && Number(currentSubCategory.id) === Number(sub.id) }"
@click="selectSubCategory(sub)"
>
<view class="cat-subgrid__thumb">
<image
class="cat-subgrid__img"
:src="catSubThumb(sub)"
mode="aspectFill"
@error="handleSubImgError(sub)"
/>
<view
v-if="currentSubCategory && Number(currentSubCategory.id) === Number(sub.id)"
class="cat-subgrid__badge"
>✓</view>
</view>
<text class="cat-subgrid__name">{{ sub.name }}</text>
</view>
</view>
<!-- 搜索品类无匹配结果提示 -->
<view v-else class="cat-subgrid-empty">
<text class="cat-subgrid-empty__txt">未找到“{{ subKeyword }}”相关品类</text>
<text class="cat-subgrid-empty__clear" @click="subKeyword = ''">清空搜索</text>
</view>
</scroll-view>
</view>
<!-- 快捷排序选项(价格支持升/降序切换)+ 区域筛选(与首页一致的半屏级联面板) -->
<view class="cat-sort">
<view
v-for="s in sortOptions"
:key="s.value"
class="cat-sort__item"
:class="{ 'cat-sort__item--active': sort === s.value }"
@click="changeSort(s.value)"
>
<text>{{ s.label }}</text>
<!-- 价格排序双三角:升/降序切换指示(主流交互) -->
<view v-if="s.value === 'price'" class="cat-sort__caret">
<view class="cat-sort__caret-up" :class="{ 'is-on': sort === 'price' && priceOrder === 'asc' }"></view>
<view class="cat-sort__caret-down" :class="{ 'is-on': sort === 'price' && priceOrder === 'desc' }"></view>
</view>
</view>
<view class="cat-sort__area" @click="openLocationModal">
<image class="cat-sort__area-icon" :src="iconUrl('home-location')" mode="aspectFit" />
<text class="cat-sort__area-txt fc-ellipsis">{{ currentAreaText }}</text>
<text class="cat-sort__area-arrow">▾</text>
</view>
</view>
<!-- 骨架屏:首屏无缓存或初次加载且列表为空时展示,微光呼吸,杜绝大片白屏空白 -->
<view v-if="loading && (!list.length || isInitialLoading)" class="cat-skeleton-wrap">
<view v-for="n in 3" :key="n" class="cat-skeleton-card">
<view class="cat-skeleton-thumb pulse-shimmer"></view>
<view class="cat-skeleton-info">
<view class="cat-skeleton-line cat-skeleton-line--title pulse-shimmer"></view>
<view class="cat-skeleton-line cat-skeleton-line--sub pulse-shimmer"></view>
<view class="cat-skeleton-row">
<view class="cat-skeleton-pill pulse-shimmer"></view>
<view class="cat-skeleton-price pulse-shimmer"></view>
</view>
</view>
</view>
</view>
<!-- 信息流列表:使用 compact 紧凑模式,平滑过渡防闪烁 -->
<view v-else-if="list.length" class="cat-list" :class="{ 'cat-list--switching': isSwitching }">
<fc-post-card
v-for="item in list"
:key="item.id"
:post="item"
:compact="true"
@click="goDetail"
/>
<view v-if="loading" class="cat-loading-bar">
<view class="cat-loading-spinner"></view>
<text class="cat-loading-txt">正在为您寻找好物…</text>
</view>
<view v-else-if="!hasMore" class="cat-nomore-bar">
<text class="cat-nomore-txt">— 已呈现全部同城信息 —</text>
</view>
</view>
<!-- 空状态:紧凑微质感卡片,融入右侧信息流,首屏完整可视 -->
<view v-else-if="!loading" class="cat-empty-card">
<image class="cat-empty-card__icon" :src="iconUrl(keyword ? 'search' : 'inbox')" mode="aspectFit" />
<text class="cat-empty-card__title">
{{ keyword ? '未找到「' + keyword + '」相关信息' : (currentSubCategory ? '暂无「' + currentSubCategory.name + '」相关发布' : '该品类下暂无发布信息') }}
</text>
<text class="cat-empty-card__desc">抢先发布此分类,让同城人脉第一眼发现</text>
<view class="cat-empty-card__actions">
<button v-if="keyword" class="cat-empty-card__btn cat-empty-card__btn--sub" @click="handleClear">
清空搜索
</button>
<button v-if="currentSubCategory" class="cat-empty-card__btn cat-empty-card__btn--sub" @click="selectSubCategory(null)">
查看全部
</button>
<button class="cat-empty-card__btn cat-empty-card__btn--primary" @click="goPublish">
立即发布
</button>
</view>
</view>
<!-- 底部留白防止遮挡 -->
<view class="cat-content__space"></view>
</scroll-view>
</view>
<!-- 底部 Tabbar(当前选中索引为 1) -->
<fc-tabbar :current="1" />
<!-- ============================================================
区域筛选半屏面板(与首页同款:省/市两列级联,筛选只到市级)
========================================================== -->
<view v-if="showLocModal" class="cat-modal-mask" @click="closeLocationModal" @touchmove.stop.prevent>
<view class="cat-modal-content" @click.stop>
<view class="cat-modal-handle"></view>
<view class="cat-modal-head">
<view class="cat-modal-title-box">
<text class="cat-modal-title">切换服务位置</text>
<text class="cat-modal-subtitle">筛选当前分类下的同城信息区域</text>
</view>
<view class="cat-modal-close" @click="closeLocationModal">✕</view>
</view>
<!-- 当前状态与一键定位 -->
<view class="cat-modal-loc-card">
<view class="cat-modal-loc-info">
<image class="cat-modal-loc-pin" :src="iconUrl('home-location')" mode="aspectFit" />
<view class="cat-modal-loc-texts">
<view class="cat-modal-loc-row">
<text class="cat-modal-loc-label">当前位置:</text>
<text class="cat-modal-loc-val">{{ currentAreaText }}</text>
<text v-if="manualArea" class="cat-modal-badge cat-modal-badge--manual">手动选择</text>
<text v-else class="cat-modal-badge cat-modal-badge--gps">定位匹配</text>
</view>
<text class="cat-modal-loc-sub">{{ locStatusDesc }}</text>
</view>
</view>
<view
class="cat-modal-reloc-btn"
:class="{ 'cat-modal-reloc-btn--loading': isLocating }"
@click="relocateCurrentPosition"
>
<text class="cat-modal-reloc-icon" :class="{ 'cat-modal-reloc-icon--spin': isLocating }">⟳</text>
<text>{{ isLocating ? '定位中…' : '重新定位' }}</text>
</view>
</view>
<!-- 全国省 / 市 两列联动直选面板 -->
<view class="cat-cascade">
<view class="cat-cascade-head">
<text class="cat-cascade-head-label cat-cascade-head-label--prov">省份 / 全国</text>
<text class="cat-cascade-head-label cat-cascade-head-label--city">城市</text>
</view>
<view class="cat-cascade-body">
<!-- 第一列:全国 + 省份 -->
<scroll-view class="cat-cascade-col cat-cascade-col--prov" scroll-y :show-scrollbar="false">
<view
class="cat-cascade-item"
:class="{ 'cat-cascade-item--active': activeProvince === '' }"
@click="onSelectCascadeProvince('')"
>
<text class="cat-cascade-text">全国</text>
<text v-if="activeProvince === ''" class="cat-cascade-check">✓</text>
</view>
<view
v-for="p in areaTree"
:key="p.name"
class="cat-cascade-item"
:class="{ 'cat-cascade-item--active': activeProvince === p.name }"
@click="onSelectCascadeProvince(p.name)"
>
<text class="cat-cascade-text fc-ellipsis">{{ p.name }}</text>
</view>
</scroll-view>
<!-- 第二列:全部 + 地级市/州 -->
<scroll-view class="cat-cascade-col cat-cascade-col--city" scroll-y :show-scrollbar="false">
<view
class="cat-cascade-item"
:class="{ 'cat-cascade-item--active': activeCity === '' }"
@click="onSelectCascadeCity('')"
>
<text class="cat-cascade-text">全部</text>
<text v-if="activeCity === '' && activeProvince" class="cat-cascade-check">✓</text>
</view>
<view
v-for="c in cascadeCities"
:key="c.name"
class="cat-cascade-item"
:class="{ 'cat-cascade-item--active': activeCity === c.name }"
@click="onSelectCascadeCity(c.name)"
>
<text class="cat-cascade-text fc-ellipsis">{{ c.name }}</text>
</view>
</scroll-view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
import FcNavbar from '@/components/fc-navbar/fc-navbar.vue'
import FcTabbar from '@/components/fc-tabbar/fc-tabbar.vue'
import FcPostCard from '@/components/fc-post-card/fc-post-card.vue'
import FcEmpty from '@/components/fc-empty/fc-empty.vue'
import homeApi from '@/common/api/home.js'
import { iconUrl, categoryIconUrl, catIcon, loadUiConfig } from '@/common/store/ui.js'
import { getNavMetrics } from '@/common/utils/navbar.js'
import { groupAreaTree } from '@/common/utils/area.js'
// 与首页共享的区域选择缓存(两端区域筛选状态互通)
const STORAGE_KEY_SELECTED_AREA = 'fc_user_selected_area'
// 分类数据快照缓存(实现 0ms 秒开无白屏)
const STORAGE_KEY_CATEGORIES = 'fc_cached_categories_v2'
/**
* 安全读取本地缓存
* @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: { FcNavbar, FcTabbar, FcPostCard, FcEmpty },
data() {
return {
statusBarHeight: 0,
navBarHeight: 44,
capsulePadding: 0,
keyword: '',
// 二级品类过滤搜索关键词
subKeyword: '',
// 图片加载失败映射表(防止宫格出现浅灰空块,支持降级为精美微插画)
failedSubImgIds: {},
categories: [],
currentCategory: null,
currentSubCategory: null,
sort: 'new',
// 价格排序方向(asc 升序 / desc 降序),点击已选中的「价格」时切换
priceOrder: 'asc',
// 区域筛选(与首页一致:筛选只到市级,省/市两列级联半屏面板)
areaRows: [],
areaTree: [],
selectedProvince: '',
selectedCity: '',
manualArea: false,
showLocModal: false,
isLocating: false,
activeProvince: '',
activeCity: '',
sortOptions: [
{ label: '最新', value: 'new' },
{ label: '最热', value: 'hot' },
{ label: '附近', value: 'nearby' },
{ label: '价格', value: 'price' },
],
list: [],
page: 1,
size: 10,
total: 0,
hasMore: true,
loading: false,
// 页面首次初始化加载骨架状态
isInitialLoading: true,
// 分类切换平滑过渡状态(防止旧列表瞬间销毁导致的猛烈闪屏抖动)
isSwitching: false,
// 下拉刷新触发状态
isRefreshing: false,
// 请求序号:切换分类/排序/搜索时丢弃过期响应,避免「点了筛选但列表没变」
reqSeq: 0,
lat: null,
lng: null,
}
},
computed: {
/**
* 输入框占位文案,动态根据当前选中一级/二级分类切换
*/
searchPlaceholder() {
const name = (this.currentSubCategory && this.currentSubCategory.name) || (this.currentCategory ? this.currentCategory.name : '全部分类')
return '搜索“' + name + '”信息…'
},
/**
* 二级品类区域标题(例如:“招聘品类”、“水果品类”、“精选品类”)
* 消除冗长复杂的“专区品类”字样,提升右上角区域呼吸感
* @returns {string} 优化后的分类标题
*/
categorySectionTitle() {
if (!this.currentCategory || !this.currentCategory.name) return '精选品类'
const raw = this.currentCategory.name.trim()
return raw.replace(/专区$/, '') + '品类'
},
/**
* 当前一级分类下的二级分类列表
*/
currentSubCategories() {
if (!this.currentCategory || !Array.isArray(this.currentCategory.children)) return []
return this.currentCategory.children
},
/**
* 经过二级分类搜索关键词过滤后的展示品类列表
* @returns {Array} 过滤后的二级分类数组
*/
displaySubCategories() {
const list = this.currentSubCategories
if (!this.subKeyword || !this.subKeyword.trim()) {
return list
}
const kw = this.subKeyword.trim().toLowerCase()
return list.filter((item) => item && item.name && item.name.toLowerCase().includes(kw))
},
/**
* 区域筛选按钮文案(市 > 省 > 全国)
*/
currentAreaText() {
if (this.selectedCity) return this.selectedCity
if (this.selectedProvince) return this.selectedProvince
return '全国'
},
/**
* 位置面板内的状态描述文案
*/
locStatusDesc() {
if (this.manualArea) {
return '当前由您手动锁定展示该区域信息'
}
if (this.lat != null && this.lng != null) {
return '已基于您的地理位置自动匹配所在城市'
}
return '暂未获取定位,展示全部区域信息'
},
/**
* 级联面板:当前选中省份下的城市列表
*/
cascadeCities() {
if (!this.activeProvince || !Array.isArray(this.areaTree)) return []
const prov = this.areaTree.find((p) => p.name === this.activeProvince)
return (prov && prov.cities) || []
},
},
onShow() {
loadUiConfig(true)
this.syncAreaFromStorage()
},
onLoad(options) {
this.initMetrics()
this.initLocation()
this.fetchAreas()
this.fetchCategories(options && options.categoryId)
},
methods: {
/** 暴露给模板:全功能分类图标解析(支持后台无上限扩展) */
catIcon,
/** 暴露给模板:图标地址解析 */
iconUrl,
/** 暴露给模板:分类图标解析 */
categoryIconUrl,
/**
* 解析二级分类图标(含加载失败自动降级与预设行业插画兜底,彻底解决空白浅灰块)
* @param {Object} sub 二级分类对象
* @returns {string} 可靠的图片或图标真实地址
*/
catSubThumb(sub) {
if (!sub) return catIcon(this.currentCategory)
if (this.failedSubImgIds && this.failedSubImgIds[sub.id]) {
return this.fallbackSubIcon(sub)
}
return catIcon(sub)
},
/**
* 根据分类名称回退到包内最贴合的高清矢量微插画
* @param {Object} sub 二级分类对象
* @returns {string} 兜底矢量图标地址
*/
fallbackSubIcon(sub) {
const name = (sub && sub.name) || ''
if (/技工|普工|制造|工厂/i.test(name)) return iconUrl('cat-job')
if (/餐饮|服务|厨师|服务员/i.test(name)) return iconUrl('cat-service')
if (/保洁|家政|清洁/i.test(name)) return iconUrl('cat-clean')
if (/教育|培训|老师|辅导/i.test(name)) return iconUrl('cat-education')
if (/司机|出行|货运|代驾/i.test(name)) return iconUrl('cat-car')
if (/兼职|短工/i.test(name)) return iconUrl('cat-job')
return catIcon(this.currentCategory)
},
/**
* 二级分类图片加载失败容错处理
* @param {Object} sub 二级分类对象
* @returns {void}
*/
handleSubImgError(sub) {
if (!sub || !sub.id) return
if (!this.failedSubImgIds) this.failedSubImgIds = {}
this.$set(this.failedSubImgIds, sub.id, true)
},
/**
* 初始化设备导航栏与胶囊避让度量
*/
initMetrics() {
const m = getNavMetrics()
this.statusBarHeight = m.statusBarHeight
this.navBarHeight = m.navBarHeight
this.capsulePadding = m.capsulePadding
},
/**
* 初始化获取当前用户定位坐标(用于“附近”距离计算与排序;未手动选区时自动匹配所在城市)
*/
initLocation() {
uni.getLocation({
type: 'gcj02',
success: (res) => {
this.lat = res.latitude
this.lng = res.longitude
// 定位晚于「附近」点击返回时,坐标到手后重排一次
if (this.sort === 'nearby' && this.currentCategory) {
this.page = 1
this.list = []
this.hasMore = true
this.fetchPosts()
}
// 未手动选区时,按定位自动匹配城市(与首页口径一致)
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 nextProv = found.province || ''
const nextCity = found.city || ''
if (nextProv !== this.selectedProvince || nextCity !== this.selectedCity) {
this.selectedProvince = nextProv
this.selectedCity = nextCity
this.activeProvince = nextProv
this.activeCity = nextCity
this.page = 1
this.list = []
this.hasMore = true
this.fetchPosts()
}
}
} catch (e) {}
},
/**
* 拉取全部分类数据(快照缓存秒开 + 后台静默对齐)
* @param {string|number} defaultCatId 默认选中的分类 ID
* @returns {Promise<void>}
*/
async fetchCategories(defaultCatId) {
// 1. 优先读取本地持久化快照,0ms 立即挂载大类与二级品类
const cached = safeGetStorage(STORAGE_KEY_CATEGORIES, null)
if (Array.isArray(cached) && cached.length) {
this.applyCategoriesTree(cached, defaultCatId, !this.list.length)
}
try {
const cats = await homeApi.categories()
const list = Array.isArray(cats) ? cats : []
if (list.length) {
safeSetStorage(STORAGE_KEY_CATEGORIES, list)
this.applyCategoriesTree(list, defaultCatId, true)
}
} catch (e) {
if (!this.categories || !this.categories.length) {
uni.showToast({ title: '分类加载失败', icon: 'none' })
}
} finally {
this.isInitialLoading = false
}
},
/**
* 统一解析并挂载分类树结构
* @param {Array} cats 分类原始数据列表
* @param {string|number} defaultCatId 默认选中的分类ID
* @param {boolean} shouldFetchPosts 是否自动触发信息流拉取
* @returns {void}
*/
applyCategoriesTree(cats, defaultCatId, shouldFetchPosts = true) {
this.categories = cats
if (!this.categories.length) return
let targetParent = null
let targetSub = null
if (defaultCatId) {
targetParent = this.categories.find((c) => String(c.id) === String(defaultCatId)) || null
if (!targetParent) {
for (const p of this.categories) {
const foundSub = (p.children || []).find((s) => String(s.id) === String(defaultCatId))
if (foundSub) {
targetParent = p
targetSub = foundSub
break
}
}
}
}
if (this.currentCategory) {
const exist = this.categories.find((c) => c.id === this.currentCategory.id)
this.currentCategory = exist || this.categories[0]
} else {
this.currentCategory = targetParent || this.categories[0]
this.currentSubCategory = targetSub || null
}
if (shouldFetchPosts) {
this.fetchPosts(false)
}
},
/**
* 切换当前选中的左侧一级大类(平滑过渡防闪烁)
* @param {Object} cat 选中的一级分类对象
* @returns {void}
*/
selectCategory(cat) {
if (!cat) return
if (this.currentCategory && this.currentCategory.id === cat.id && !this.currentSubCategory) return
this.currentCategory = cat
this.currentSubCategory = null
this.subKeyword = '' // 切换一级大类时重置二级品类搜索
this.page = 1
this.hasMore = true
// 开启平滑过渡,不先置空旧列表,杜绝视口高度剧烈塌陷与白屏闪烁
this.isSwitching = true
this.fetchPosts(false)
},
/**
* 切换右侧二级分类(如:花菜、白菜、紫甘蓝、青甘蓝、青菜等)
* @param {Object|null} sub 二级分类对象(null代表当前大类全部)
* @returns {void}
*/
selectSubCategory(sub) {
const nextId = sub ? Number(sub.id) : 0
const curId = this.currentSubCategory ? Number(this.currentSubCategory.id) : 0
if (nextId === curId) return
this.currentSubCategory = sub || null
this.page = 1
this.hasMore = true
this.isSwitching = true
this.fetchPosts(false)
},
/**
* 切换排序规则(价格项:首次点击升序,再次点击升降序来回切换)
* @param {string} s 排序值 new|hot|nearby|price
* @returns {void}
*/
changeSort(s) {
if (s === 'price') {
if (this.sort === 'price') {
this.priceOrder = this.priceOrder === 'asc' ? 'desc' : 'asc'
} else {
this.priceOrder = 'asc'
}
} else if (this.sort === s) {
return
}
this.sort = s
this.page = 1
this.hasMore = true
this.isSwitching = true
this.fetchPosts(false)
// 「附近」依赖坐标:无坐标时补取一次定位(成功后自动重排)
if (s === 'nearby' && (!this.lat || !this.lng)) {
uni.showToast({ title: '正在获取定位…', icon: 'none' })
this.initLocation()
}
},
/**
* 拉取服务区域列表(平铺:每条自带省/市/区县),构建省市级联树
*/
async fetchAreas() {
try {
const rows = await homeApi.areas()
this.areaRows = Array.isArray(rows) ? rows : []
this.rebuildAreaTree(this.areaRows)
} catch (e) {}
},
/**
* 重建级联选择树
* @param {Array} rows 平铺区域列表
*/
rebuildAreaTree(rows) {
if (!Array.isArray(rows) || !rows.length) return
this.areaTree = groupAreaTree(rows)
},
/**
* onShow 时与首页区域选择保持同步(共享同一份缓存)
*/
syncAreaFromStorage() {
const saved = safeGetStorage(STORAGE_KEY_SELECTED_AREA, null)
const prov = (saved && saved.province) || ''
const city = (saved && saved.city) || ''
if (prov === this.selectedProvince && city === this.selectedCity) return
this.selectedProvince = prov
this.selectedCity = city
this.manualArea = !!saved
this.activeProvince = prov
this.activeCity = city
this.page = 1
this.list = []
this.hasMore = true
if (this.currentCategory) {
this.fetchPosts()
}
},
/**
* 打开区域切换半屏面板(与首页同款省/市两列级联)
*/
openLocationModal() {
this.showLocModal = true
if (!this.activeProvince && this.selectedProvince) {
this.activeProvince = this.selectedProvince
} else if (!this.activeProvince && this.areaTree.length) {
const gz = this.areaTree.find((p) => p.name.indexOf('贵州') !== -1)
this.activeProvince = gz ? gz.name : ''
}
if (!this.activeCity && this.selectedCity) {
this.activeCity = this.selectedCity
}
},
/**
* 关闭区域切换面板
*/
closeLocationModal() {
this.showLocModal = false
},
/**
* 级联第一列:点击全国或省份
* @param {string} provName 省份名称(空串代表全国)
*/
onSelectCascadeProvince(provName) {
this.activeProvince = provName || ''
if (!provName) {
this.activeCity = ''
this.applyAreaSelection(null, true)
this.closeLocationModal()
return
}
const prov = this.areaTree.find((p) => p.name === provName)
const firstCity = prov && prov.cities && prov.cities[0] ? prov.cities[0].name : ''
this.activeCity = firstCity
},
/**
* 级联第二列:点击全部(全省)或具体城市(全市筛选,只到市级)
* @param {string} cityName 城市名称(空串代表全省全部)
*/
onSelectCascadeCity(cityName) {
if (!cityName) {
this.activeCity = ''
if (!this.activeProvince) {
this.applyAreaSelection(null, true)
} else {
this.applyAreaSelection({ province: this.activeProvince, city: '' }, true)
}
this.closeLocationModal()
return
}
this.activeCity = cityName
this.applyAreaSelection({ province: this.activeProvince, city: cityName }, true)
this.closeLocationModal()
},
/**
* 统一应用区域选择并持久化(与首页共享缓存,null = 全国)
* @param {Object|null} area 区域数据
* @param {boolean} isManual 是否为用户主动操作
*/
applyAreaSelection(area, isManual = true) {
if (!area) {
this.selectedProvince = ''
this.selectedCity = ''
this.manualArea = isManual
if (isManual) {
safeSetStorage(STORAGE_KEY_SELECTED_AREA, { areaId: 0, district: '', city: '', province: '' })
}
} else {
this.selectedProvince = area.province || ''
this.selectedCity = area.city || ''
this.manualArea = isManual
if (isManual) {
safeSetStorage(STORAGE_KEY_SELECTED_AREA, {
areaId: 0,
district: '',
city: this.selectedCity,
province: this.selectedProvince,
})
}
}
this.page = 1
this.list = []
this.hasMore = true
this.fetchPosts()
},
/**
* 面板内:重新定位当前位置并切换(清除手动标记,恢复自动匹配)
*/
relocateCurrentPosition() {
if (this.isLocating) return
this.isLocating = true
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.selectedProvince = found.province || ''
this.selectedCity = found.city || ''
this.activeProvince = this.selectedProvince
this.activeCity = this.selectedCity
this.page = 1
this.list = []
this.hasMore = true
this.fetchPosts()
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' })
},
})
},
/**
* 下拉刷新:重新获取分类与当前列表数据(支持下拉动画指示)
* @returns {Promise<void>}
*/
async onPullDownRefresh() {
this.isRefreshing = true
this.page = 1
try {
await Promise.all([
this.fetchCategories(this.currentCategory ? this.currentCategory.id : null),
this.fetchPosts(false)
])
} finally {
this.isRefreshing = false
uni.stopPullDownRefresh()
}
},
/**
* 获取当前分类下的信息列表(支持关键字在当前页面直接检索)
* @param {boolean} isLoadMore 是否为触底加载下一页数据
* @returns {Promise<void>}
*/
async fetchPosts(isLoadMore = false) {
if (!this.currentCategory) return
const seq = ++this.reqSeq
this.loading = true
if (!isLoadMore) {
this.isSwitching = true
}
try {
const params = {
categoryId: this.currentCategory.id,
subCategoryId: this.currentSubCategory ? this.currentSubCategory.id : undefined,
// 价格排序带方向:price_asc / price_desc(后端按方向排序)
sort: this.sort === 'price' ? `price_${this.priceOrder}` : this.sort,
// 区域筛选只到市级(与首页一致)
province: !this.selectedCity && this.selectedProvince ? this.selectedProvince : undefined,
city: this.selectedCity || undefined,
page: this.page,
size: this.size,
}
if (this.keyword && this.keyword.trim()) {
params.keyword = this.keyword.trim()
}
if (this.lat && this.lng) {
params.lat = this.lat
params.lng = this.lng
}
const res = await homeApi.feed(params)
// 期间又切换了分类/排序/搜索:以最后一次请求为准
if (seq !== this.reqSeq) return
const rows = (res && res.list) || []
this.list = this.page === 1 ? rows : this.list.concat(rows)
this.total = (res && res.total) || 0
this.hasMore = res ? !!res.hasMore : false
} catch (e) {
if (seq !== this.reqSeq) return
uni.showToast({ title: '加载信息失败', icon: 'none' })
} finally {
if (seq === this.reqSeq) {
this.loading = false
this.isSwitching = false
this.isInitialLoading = false
this.isRefreshing = false
}
}
},
/**
* 触底加载下一页数据
* @returns {void}
*/
loadMore() {
if (this.hasMore && !this.loading) {
this.page += 1
this.fetchPosts(true)
}
},
/**
* 在当前页面直接执行搜索
*/
handleSearch() {
this.page = 1
this.list = []
this.hasMore = true
this.fetchPosts()
},
/**
* 清空搜索关键词并在当前分类下重置列表
*/
handleClear() {
this.keyword = ''
this.handleSearch()
},
/**
* 跳转到当前分类的发布页
*/
goPublish() {
const catId = this.currentCategory ? this.currentCategory.id : ''
const subId = this.currentSubCategory ? `&subCategoryId=${this.currentSubCategory.id}` : ''
uni.navigateTo({ url: `/pages/post/create?categoryId=${catId}${subId}` })
},
/**
* 查看信息详情
* @param {Object} post 点击的信息项
*/
goDetail(post) {
if (post && post.id) {
uni.navigateTo({ url: `/pages/detail/detail?id=${post.id}` })
}
},
},
}
</script>
<style lang="scss" scoped>
.cat-page {
height: 100vh;
display: flex;
flex-direction: column;
background: $fc-bg;
overflow: hidden;
}
/* 顶栏通栏直接搜索:整齐大方、不换行、直接当前页过滤 */
.cat-search-bar {
background: #ffffff;
padding: 16rpx 24rpx;
display: flex;
align-items: center;
gap: 16rpx;
border-bottom: 1rpx solid rgba(4, 48, 30, 0.05);
box-shadow: 0 4rpx 14rpx rgba(4, 48, 30, 0.03);
flex-shrink: 0;
z-index: 10;
}
.cat-search-bar__inner {
flex: 1;
height: 72rpx;
border-radius: 999rpx;
background: #f2fef7;
border: 1rpx solid rgba(0, 224, 112, 0.28);
display: flex;
align-items: center;
padding: 0 20rpx;
gap: 12rpx;
box-sizing: border-box;
transition: all 0.2s ease;
&:focus-within {
background: #ffffff;
border-color: #00c860;
box-shadow: 0 0 12rpx rgba(0, 200, 96, 0.15);
}
}
.cat-search-bar__input {
flex: 1;
height: 100%;
font-size: 26rpx;
color: $fc-text;
}
.cat-search-bar__ph {
font-size: 25rpx;
color: $fc-text-placeholder;
}
.cat-search-bar__clear {
width: 36rpx;
height: 36rpx;
border-radius: 50%;
background: rgba(4, 48, 30, 0.15);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
flex-shrink: 0;
}
.cat-search-bar__clear-txt {
font-size: 22rpx;
color: #ffffff;
line-height: 1;
margin-top: -2rpx;
}
.cat-search-bar__btn {
height: 72rpx;
padding: 0 32rpx;
border-radius: 999rpx;
background: $fc-gradient-primary;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
box-shadow: 0 4rpx 14rpx rgba(0, 224, 112, 0.3);
flex-shrink: 0;
transition: transform 0.15s ease;
&:active {
transform: scale(0.96);
opacity: 0.9;
}
}
.cat-search-bar__btn-txt {
font-size: 26rpx;
font-weight: 700;
color: $fc-primary-ink;
}
/* 双联主体容器 */
.cat-main {
flex: 1;
display: flex;
min-height: 0; /* 必须,确保内部滚动条生效 */
min-width: 0; /* 防止长文本卡片把整个双栏撑出屏幕 */
overflow: hidden;
}
/* 左侧分类栏:图标与标签左右水平排列,扩充宽度彻底杜绝 4 字分类名截断 */
.cat-sidebar {
width: 204rpx;
height: 100%;
background: #f6f8fa;
flex-shrink: 0;
border-right: 1rpx solid rgba(0, 0, 0, 0.05);
}
.cat-sidebar__item {
cursor: pointer;
position: relative;
height: 96rpx;
display: flex;
flex-direction: row;
align-items: center;
padding: 0 10rpx 0 18rpx;
box-sizing: border-box;
gap: 10rpx;
color: $fc-text-secondary;
font-size: 24rpx;
transition: all 0.15s ease;
border-bottom: 1rpx solid rgba(0, 0, 0, 0.02);
&--active {
background: #ffffff;
color: $fc-primary-text;
font-weight: 700;
.cat-sidebar__icon {
opacity: 1;
transform: scale(1.06);
}
}
&--skel {
pointer-events: none;
}
}
.cat-sidebar__skel-icon {
width: 34rpx;
height: 34rpx;
border-radius: 8rpx;
flex-shrink: 0;
background: #edf2f0;
}
.cat-sidebar__skel-name {
width: 100rpx;
height: 24rpx;
border-radius: 6rpx;
background: #edf2f0;
}
.cat-sidebar__bar {
position: absolute;
left: 0;
top: 24rpx;
bottom: 24rpx;
width: 6rpx;
border-radius: 0 4rpx 4rpx 0;
background: $fc-primary;
}
.cat-sidebar__icon {
width: 34rpx;
height: 34rpx;
flex-shrink: 0;
opacity: 0.8;
transition: transform 0.15s ease, opacity 0.15s ease;
}
.cat-sidebar__name {
flex: 1;
line-height: 1.25;
white-space: nowrap;
text-align: left;
letter-spacing: -0.2rpx;
}
/* 右侧内容展示区 */
.cat-content {
/* 关键修复:flex 子项默认 min-width:auto,会被列表里长标题/长标签卡片的
min-content 撑破(间歇性把右栏顶出屏幕)。flex-basis 0% + min-width 0
把宽度死锁在剩余空间内,超宽内容交给 scroll-view 裁剪。 */
flex: 1 1 0%;
min-width: 0;
width: 0; /* 双保险:微信 scroll-view 包装节点不回传内容宽度 */
height: 100%;
padding: 14rpx 14rpx 0;
box-sizing: border-box;
overflow: hidden;
}
/* 二级分类实拍宫格卡片 */
.cat-subcard {
background: #ffffff;
border-radius: 20rpx;
padding: 18rpx 16rpx 10rpx;
margin-bottom: 14rpx;
box-shadow: 0 2rpx 10rpx rgba(4, 48, 30, 0.04);
}
.cat-subcard__head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 4rpx 14rpx;
gap: 12rpx;
}
.cat-subcard__head-left {
display: flex;
align-items: center;
gap: 10rpx;
min-width: 0;
flex: 1;
}
.cat-subcard__title {
font-size: 26rpx;
font-weight: 700;
color: #1e293b;
white-space: nowrap;
}
/* 当前选中的二级品类微胶囊标签 */
.cat-subcard__active-pill {
display: inline-flex;
align-items: center;
gap: 6rpx;
background: #ecfdf5;
border: 1rpx solid #a7f3d0;
padding: 2rpx 12rpx;
border-radius: 999rpx;
max-width: 170rpx;
cursor: pointer;
transition: opacity 0.15s ease;
&:active {
opacity: 0.8;
}
}
.cat-subcard__active-pill-txt {
font-size: 21rpx;
color: #00a854;
font-weight: 600;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.cat-subcard__active-pill-close {
font-size: 18rpx;
color: #00a854;
opacity: 0.75;
line-height: 1;
}
/* 右上角二级品类微搜索条 */
.cat-subcard__search {
display: flex;
align-items: center;
background: #f4fbf7;
border: 1rpx solid #d1fae5;
border-radius: 999rpx;
height: 52rpx;
padding: 0 14rpx;
box-sizing: border-box;
width: 200rpx;
flex-shrink: 0;
transition: all 0.2s ease;
&:focus-within {
background: #ffffff;
border-color: #00d26a;
box-shadow: 0 0 0 2rpx rgba(0, 210, 106, 0.15);
}
}
.cat-subcard__search-icon {
width: 24rpx;
height: 24rpx;
flex-shrink: 0;
opacity: 0.6;
}
.cat-subcard__search-input {
flex: 1;
height: 100%;
font-size: 21rpx;
color: #1e293b;
padding: 0 6rpx;
min-width: 0;
}
.cat-subcard__search-ph {
font-size: 21rpx;
color: #94a3b8;
}
.cat-subcard__search-clear {
width: 28rpx;
height: 28rpx;
border-radius: 50%;
background: #cbd5e1;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.cat-subcard__search-clear-txt {
font-size: 19rpx;
color: #ffffff;
line-height: 1;
margin-top: -2rpx;
}
.cat-subgrid-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 36rpx 16rpx;
gap: 12rpx;
}
.cat-subgrid-empty__txt {
font-size: 23rpx;
color: #94a3b8;
}
.cat-subgrid-empty__clear {
font-size: 22rpx;
color: #00a854;
background: #ecfdf5;
padding: 6rpx 20rpx;
border-radius: 999rpx;
font-weight: 600;
}
/* 二级品类宫格:≤9 个自适应高度不滚动;>9 个锁 3 行高度(474rpx)区域内上下滚动 */
.cat-subgrid-scroll {
width: 100%;
}
.cat-subgrid {
display: flex;
flex-wrap: wrap;
}
.cat-subgrid__item {
width: 33.3333%;
display: flex;
flex-direction: column;
align-items: center;
padding: 10rpx 6rpx 14rpx;
box-sizing: border-box;
position: relative;
transition: all 0.15s ease;
&--active {
.cat-subgrid__thumb {
border-color: #00d26a;
background: #ecfdf5;
box-shadow: 0 6rpx 16rpx rgba(0, 210, 106, 0.26);
transform: scale(1.03);
}
.cat-subgrid__name {
color: #00a854;
font-weight: 700;
}
}
}
.cat-subgrid__thumb {
width: 98rpx;
height: 98rpx;
border-radius: 22rpx;
background: #f8fafc;
border: 2.5rpx solid transparent;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
position: relative;
overflow: hidden;
transition: all 0.18s ease;
}
.cat-subgrid__img {
width: 100%;
height: 100%;
display: block;
}
/* 选中态右上角微质感小圆勾 */
.cat-subgrid__badge {
position: absolute;
right: 4rpx;
top: 4rpx;
width: 26rpx;
height: 26rpx;
border-radius: 50%;
background: #00d26a;
color: #ffffff;
font-size: 18rpx;
font-weight: 800;
display: flex;
align-items: center;
justify-content: center;
line-height: 1;
box-shadow: 0 2rpx 6rpx rgba(0, 210, 106, 0.4);
z-index: 2;
}
.cat-subgrid__name {
margin-top: 10rpx;
font-size: 23rpx;
color: #334155;
text-align: center;
line-height: 1.25;
max-width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* 排序切换:轻巧微胶囊风格(价格项带升/降序双三角,右侧为区域筛选) */
.cat-sort {
display: flex;
align-items: center;
background: #ffffff;
border-radius: 20rpx;
padding: 8rpx 12rpx;
margin-bottom: 14rpx;
box-shadow: 0 2rpx 10rpx rgba(4, 48, 30, 0.04);
}
.cat-sort__item {
cursor: pointer;
display: inline-flex;
align-items: center;
flex-shrink: 0;
font-size: 24rpx;
color: #64748b;
padding: 8rpx 14rpx;
border-radius: 999rpx;
transition: all 0.15s ease;
&--active {
color: #00a854;
font-weight: 700;
background: #ecfdf5;
}
}
/* 价格排序双三角指示器(未选中灰、选中主题绿) */
.cat-sort__caret {
display: inline-flex;
flex-direction: column;
align-items: center;
margin-left: 4rpx;
gap: 3rpx;
}
.cat-sort__caret-up {
width: 0;
height: 0;
border-left: 6rpx solid transparent;
border-right: 6rpx solid transparent;
border-bottom: 8rpx solid #cbd5e1;
&.is-on {
border-bottom-color: #00a854;
}
}
.cat-sort__caret-down {
width: 0;
height: 0;
border-left: 6rpx solid transparent;
border-right: 6rpx solid transparent;
border-top: 8rpx solid #cbd5e1;
&.is-on {
border-top-color: #00a854;
}
}
/* 区域筛选入口:薄荷浅绿胶囊(与首页区域按钮同风格,紧凑适配窄栏) */
.cat-sort__area {
margin-left: auto;
display: inline-flex;
align-items: center;
flex-shrink: 0;
padding: 6rpx 14rpx;
background: #f2fef7;
border: 1rpx solid rgba(0, 168, 84, 0.2);
border-radius: 999rpx;
cursor: pointer;
transition: opacity 0.15s ease;
&:active {
opacity: 0.8;
}
}
.cat-sort__area-icon {
width: 24rpx;
height: 24rpx;
margin-right: 6rpx;
flex-shrink: 0;
}
.cat-sort__area-txt {
font-size: 22rpx;
color: #00a854;
font-weight: 600;
max-width: 120rpx;
}
.cat-sort__area-arrow {
font-size: 18rpx;
color: #00a854;
margin-left: 2rpx;
}
/* 骨架屏微光流卡片 */
.cat-skeleton-wrap {
display: flex;
flex-direction: column;
gap: 14rpx;
margin-top: 4rpx;
}
.cat-skeleton-card {
background: #ffffff;
border-radius: 20rpx;
padding: 16rpx;
display: flex;
gap: 16rpx;
box-shadow: 0 2rpx 8rpx rgba(4, 48, 30, 0.03);
}
.cat-skeleton-thumb {
width: 140rpx;
height: 140rpx;
border-radius: 16rpx;
flex-shrink: 0;
background: #edf3f0;
}
.cat-skeleton-info {
flex: 1;
display: flex;
flex-direction: column;
justify-content: space-between;
padding: 4rpx 0;
}
.cat-skeleton-line {
height: 26rpx;
border-radius: 8rpx;
background: #edf3f0;
&--title {
width: 85%;
}
&--sub {
width: 55%;
height: 22rpx;
}
}
.cat-skeleton-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.cat-skeleton-pill {
width: 90rpx;
height: 28rpx;
border-radius: 8rpx;
background: #edf3f0;
}
.cat-skeleton-price {
width: 110rpx;
height: 32rpx;
border-radius: 8rpx;
background: #edf3f0;
}
/* 呼吸微光动效 */
.pulse-shimmer {
animation: pulseShimmer 1.4s ease-in-out infinite;
}
@keyframes pulseShimmer {
0% {
opacity: 0.55;
background: #edf5f0;
}
50% {
opacity: 1;
background: #d8eee1;
}
100% {
opacity: 0.55;
background: #edf5f0;
}
}
/* 信息流列表:支持平滑遮罩切换防闪烁 */
.cat-list {
display: flex;
flex-direction: column;
transition: opacity 0.18s ease;
&--switching {
opacity: 0.55;
pointer-events: none;
}
}
/* 底部加载中指示条 */
.cat-loading-bar {
display: flex;
align-items: center;
justify-content: center;
padding: 24rpx 0;
gap: 12rpx;
}
.cat-loading-spinner {
width: 28rpx;
height: 28rpx;
border: 3rpx solid #d1fae5;
border-top-color: #00d26a;
border-radius: 50%;
animation: catSpin 0.7s linear infinite;
}
@keyframes catSpin {
to {
transform: rotate(360deg);
}
}
.cat-loading-txt {
font-size: 22rpx;
color: #64748b;
}
/* 触底提示条 */
.cat-nomore-bar {
display: flex;
align-items: center;
justify-content: center;
padding: 28rpx 0;
}
.cat-nomore-txt {
font-size: 22rpx;
color: #94a3b8;
letter-spacing: 0.5rpx;
}
/* 紧凑微质感空状态卡片:首屏完整呈现,双引导操作 */
.cat-empty-card {
background: #ffffff;
border-radius: 20rpx;
padding: 44rpx 20rpx 40rpx;
margin-top: 4rpx;
display: flex;
flex-direction: column;
align-items: center;
box-shadow: 0 2rpx 10rpx rgba(4, 48, 30, 0.04);
}
.cat-empty-card__icon {
width: 90rpx;
height: 90rpx;
margin-bottom: 14rpx;
opacity: 0.85;
}
.cat-empty-card__title {
font-size: 26rpx;
font-weight: 700;
color: #1e293b;
text-align: center;
line-height: 1.4;
max-width: 92%;
}
.cat-empty-card__desc {
margin-top: 8rpx;
font-size: 22rpx;
color: #94a3b8;
text-align: center;
}
.cat-empty-card__actions {
display: flex;
align-items: center;
justify-content: center;
gap: 16rpx;
margin-top: 26rpx;
flex-wrap: wrap;
}
.cat-empty-card__btn {
height: 64rpx;
line-height: 64rpx;
padding: 0 32rpx;
border-radius: 999rpx;
font-size: 24rpx;
font-weight: 600;
margin: 0;
border: none;
&::after {
border: none;
}
&--primary {
background: $fc-gradient-primary;
color: $fc-primary-ink;
box-shadow: 0 4rpx 14rpx rgba(0, 224, 112, 0.32);
}
&--sub {
background: #f1f5f9;
color: #475569;
border: 1rpx solid #e2e8f0;
}
&:active {
transform: scale(0.97);
opacity: 0.9;
}
}
.cat-content__space {
height: 140rpx;
}
/* ============================================================
区域筛选半屏面板样式(与首页 Bottom Sheet 同款)
========================================================== */
.cat-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: catModalFadeIn 0.22s ease-out;
}
.cat-modal-content {
background: #ffffff;
border-radius: 36rpx 36rpx 0 0;
padding: 20rpx 32rpx calc(48rpx + env(safe-area-inset-bottom));
animation: catModalSlideUp 0.26s cubic-bezier(0.16, 1, 0.3, 1);
box-shadow: 0 -8rpx 36rpx rgba(4, 48, 30, 0.12);
}
.cat-modal-handle {
width: 72rpx;
height: 8rpx;
background: #dcece3;
border-radius: 999rpx;
margin: 0 auto 24rpx;
}
.cat-modal-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 28rpx;
}
.cat-modal-title-box {
display: flex;
flex-direction: column;
}
.cat-modal-title {
font-size: 34rpx;
font-weight: 700;
color: #1e293b;
}
.cat-modal-subtitle {
font-size: 22rpx;
color: $fc-text-secondary;
margin-top: 6rpx;
}
.cat-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;
}
}
/* 当前定位状态卡片 */
.cat-modal-loc-card {
background: #f4fbf7;
border: 1rpx solid #c9ebd8;
border-radius: 20rpx;
padding: 24rpx;
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 30rpx;
}
.cat-modal-loc-info {
display: flex;
align-items: center;
flex: 1;
min-width: 0;
}
.cat-modal-loc-pin {
width: 44rpx;
height: 44rpx;
flex-shrink: 0;
margin-right: 16rpx;
}
.cat-modal-loc-texts {
display: flex;
flex-direction: column;
min-width: 0;
}
.cat-modal-loc-row {
display: flex;
align-items: center;
}
.cat-modal-loc-label {
font-size: 26rpx;
color: $fc-text-regular;
flex-shrink: 0;
}
.cat-modal-loc-val {
font-size: 30rpx;
font-weight: 700;
color: $fc-primary-text;
max-width: 200rpx;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
.cat-modal-badge {
font-size: 20rpx;
padding: 2rpx 12rpx;
border-radius: 999rpx;
margin-left: 12rpx;
font-weight: 600;
flex-shrink: 0;
&--manual {
background: #e6f6ee;
color: $fc-primary-text;
border: 1rpx solid #b5e8cb;
}
&--gps {
background: #00e070;
color: $fc-primary-ink;
}
}
.cat-modal-loc-sub {
font-size: 22rpx;
color: $fc-text-secondary;
margin-top: 6rpx;
}
.cat-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;
}
}
.cat-modal-reloc-icon {
font-size: 26rpx;
margin-right: 6rpx;
font-weight: bold;
&--spin {
display: inline-block;
animation: catSpin 1s linear infinite;
}
}
/* 全国省/市 两列级联面板(薄荷容器 + 白色圆角列卡 + 柔绿选中态) */
.cat-cascade {
background: #f2fef7;
border-radius: 24rpx;
padding: 24rpx 20rpx 20rpx;
}
.cat-cascade-head {
display: flex;
margin-bottom: 16rpx;
}
.cat-cascade-head-label {
font-size: 22rpx;
font-weight: 600;
color: $fc-text-muted;
padding-left: 24rpx;
box-sizing: border-box;
&--prov {
width: 40%;
}
&--city {
width: 60%;
}
}
.cat-cascade-body {
display: flex;
height: 560rpx;
background: #ffffff;
border-radius: 20rpx;
overflow: hidden;
box-shadow: 0 4rpx 16rpx rgba(4, 48, 30, 0.05);
}
.cat-cascade-col {
height: 100%;
box-sizing: border-box;
&--prov {
width: 40%;
background: #f7fcf9;
border-right: 1rpx solid #eef6f1;
}
&--city {
width: 60%;
background: #ffffff;
}
}
.cat-cascade-item {
min-height: 80rpx;
margin: 6rpx 12rpx;
padding: 10rpx 18rpx;
display: flex;
align-items: center;
justify-content: space-between;
box-sizing: border-box;
border-radius: 14rpx;
transition: background-color 0.15s ease;
&--active {
background: linear-gradient(90deg, rgba(0, 210, 106, 0.14), rgba(0, 224, 112, 0.07));
.cat-cascade-text {
color: #04874a;
font-weight: 700;
}
}
}
.cat-cascade-text {
font-size: 26rpx;
color: $fc-text-regular;
flex: 1;
line-height: 1.5;
}
.cat-cascade-check {
font-size: 26rpx;
color: #00c566;
font-weight: 700;
margin-left: 8rpx;
flex-shrink: 0;
}
/* 动效 */
@keyframes catModalFadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes catModalSlideUp {
from { transform: translateY(100%); }
to { transform: translateY(0); }
}
@keyframes catSpin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>