1927 行
51 KiB
Vue
1927 行
51 KiB
Vue
<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>
|