| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289 |
- <script setup lang="ts">
- import { storeToRefs } from 'pinia'
- import { useI18n } from 'vue-i18n'
- import LanguageSvg from '~/assets/icons/language.svg'
- import ProfileSvg from '~/assets/icons/profile.svg'
- import AboutSvg from '~/assets/icons/about.svg'
- import SearchSvg from '~/assets/icons/search.svg'
- import { useApi } from '~/composables/useApi'
- import { skillApi } from '~/api/skill'
- import type { SkillSearchDTO, SkillSearchVo } from '~/types/api'
- import { useCategoryStore } from '~/stores/category'
- import { APP_LOCALE_KEY } from '~/constants'
- defineOptions({
- name: 'HomePage',
- })
- definePageMeta({
- auth: false,
- })
- const router = useRouter()
- const route = useRoute()
- const {
- open: openFindPartnerPopup,
- resetFilters: resetFindPartnerFilters,
- getFilters: getFindPartnerFilters,
- onApply: onFindPartnerApply,
- } = useFindPartnerPopup()
- const { open: openSelectListPopup } = useSelectListPopup()
- const { open: openAbout } = useAboutPopup()
- const { locale, locales, t, setLocale } = useI18n()
- const { request } = useApi()
- // 全局品类 store(品类树 & 当前选中的一级/二级品类)
- const categoryStore = useCategoryStore()
- const { categoryTree, activeFirstCode, activeSecondCode } = storeToRefs(categoryStore)
- const playmateList = ref<SkillSearchVo[]>([])
- const pageSize = 10
- const pageCursor = ref<unknown | '' | null>('')
- const listLoading = ref(false)
- const listFinished = ref(false)
- // 是否已完成首次列表加载,用于控制排序区域的展示时机
- const hasInitialLoaded = ref(false)
- // 排序:rating -> sortByStar, price -> sortByPrice
- const sortByStar = ref(-1)
- const sortByPrice = ref(-1)
- const resetPlaymateList = () => {
- playmateList.value = []
- pageCursor.value = ''
- listFinished.value = false
- }
- const loadPlaymateList = async () => {
- // 还没有选定二级品类时,不请求列表
- if (!activeSecondCode.value) {
- listLoading.value = false
- return
- }
- listLoading.value = true
- try {
- // 从「寻找陪玩伙伴」弹窗中读取筛选项映射
- const {
- gender,
- ageRange,
- areCode,
- priceRange,
- sortByStar: sortByStarParam,
- } = getFindPartnerFilters(sortByStar.value)
- const payload: SkillSearchDTO = {
- // 使用二级品类 code 作为筛选条件
- code: activeSecondCode.value,
- sortByStar: sortByStarParam,
- sortByPrice: sortByPrice.value,
- ageRange,
- gender,
- areCode,
- priceRange,
- page: {
- size: pageSize,
- next: pageCursor.value ?? '',
- },
- }
- const res = await request(() => skillApi.list(payload))
- // 请求失败(例如网络错误或业务错误)时,停止后续自动触发加载,避免 van-list 无限尝试加载
- if (!res) {
- listFinished.value = true
- return
- }
- playmateList.value.push(...res.list)
- const hasNext = res.next && !(typeof res.next === 'string' && res.next === '')
- if (!hasNext) {
- listFinished.value = true
- }
- else {
- pageCursor.value = res.next
- }
- // 首次加载完成后再展示排序区域
- if (!hasInitialLoaded.value) {
- hasInitialLoaded.value = true
- }
- }
- finally {
- // 无论成功还是失败,都要把 loading 复位,防止一直处于“加载中”状态
- listLoading.value = false
- }
- }
- // 点击「寻找陪玩伙伴」弹窗内确认按钮时,重新加载列表
- onFindPartnerApply(() => {
- if (!activeSecondCode.value)
- return
- resetPlaymateList()
- loadPlaymateList()
- })
- // 一级品类切换:通过全局 store 统一控制(内部会自动联动二级品类)
- const handleFirstCategoryChange = (firstCode: string) => {
- categoryStore.setActiveFirst(firstCode)
- }
- // 二级品类切换:仅更新二级 code,由监听器统一处理列表刷新
- const handleSecondCategoryChange = (secondCode: string) => {
- categoryStore.setActiveSecond(secondCode)
- }
- // 排序变更:更新排序字段并刷新列表
- const handleSortChange = (payload: { sortByStar: number, sortByPrice: number }) => {
- sortByStar.value = payload.sortByStar
- sortByPrice.value = payload.sortByPrice
- // 排序改变后,重置并重新加载列表
- resetPlaymateList()
- loadPlaymateList()
- }
- const handleAbout = () => {
- openAbout({
- onSelect: k => router.push(`/about/${k}?header=true`),
- })
- }
- // 当二级品类发生变化时:
- // 1. 重置高级筛选(FindPartner 弹窗)
- // 2. 重置列表并按默认条件重新加载
- watch(activeSecondCode, (code) => {
- if (!code)
- return
- resetFindPartnerFilters()
- resetPlaymateList()
- loadPlaymateList()
- }, { immediate: true })
- watch(locale, () => {
- categoryStore.loadCategories(true)
- })
- onActivated(async () => {
- // 通过 Pinia 全局 store 加载品类数据(默认只会请求一次)
- await categoryStore.loadCategories()
- // 如果从品类页带回了指定的一级/二级品类,则优先使用参数
- const fcode = route.query.fcode as string | undefined
- const scode = route.query.scode as string | undefined
- if (fcode) {
- categoryStore.setActiveFirst(fcode)
- if (scode) {
- categoryStore.setActiveSecond(scode)
- }
- }
- else {
- // 无路由参数时,使用 store 内部提供的默认选中逻辑
- categoryStore.initDefaultSelection()
- }
- })
- const handleProfile = () => {
- router.push('/mine')
- }
- const handleFindPartner = () => {
- openFindPartnerPopup()
- }
- const handleLanguageSelect = () => {
- openSelectListPopup({
- options: [
- ...locales.value.map(locale => ({
- label: locale.name ?? '',
- value: locale.code,
- })),
- ],
- defaultValue: locale.value,
- onSelect: (value) => {
- const nextLocale = value as typeof locale.value
- setLocale(nextLocale)
- if (typeof window !== 'undefined') {
- window.localStorage.setItem(APP_LOCALE_KEY, nextLocale)
- }
- },
- })
- }
- </script>
- <template>
- <div class="min-h-screen bg-bg-primary text-text-primary">
- <div class="relative mx-auto flex flex-col px-4 pt-6 pb-8">
- <div class="header-background absolute left-0 right-0 top-0" />
- <header class="z-10 flex items-center justify-between">
- <div class="logo">
- <img
- src="~/assets/images/logo.png"
- alt="logo"
- >
- </div>
- <div class="flex items-center gap-[16px]">
- <SearchSvg
- class="cursor-pointer"
- @click="router.push('/search')"
- />
- <AboutSvg @click="handleAbout" />
- <LanguageSvg @click="handleLanguageSelect" />
- <ProfileSvg @click="handleProfile" />
- </div>
- </header>
- <div class="relative z-10 flex flex-col">
- <HomeNavTabs
- class="mt-2"
- :category-tree="categoryTree"
- :active-first-code="activeFirstCode"
- :active-second-code="activeSecondCode"
- @change-first="handleFirstCategoryChange"
- @change-second="handleSecondCategoryChange"
- />
- <HomeSortSection
- v-show="hasInitialLoaded"
- class="sticky top-0 py-2"
- :on-find-partner="handleFindPartner"
- @change-sort="handleSortChange"
- />
- <CommonEmpty v-if="!hasInitialLoaded && !playmateList.length && categoryTree.length === 0" />
- <van-list
- v-model:loading="listLoading"
- :finished="listFinished"
- :finished-text="t('common.noMoreData')"
- :loading-text="t('common.loading')"
- :immediate-check="false"
- @load="loadPlaymateList"
- >
- <HomePlaymateCards :cards="playmateList" />
- </van-list>
- </div>
- </div>
- </div>
- </template>
- <style lang="scss" scoped>
- .logo {
- width: 103px;
- img {
- width: 100%;
- height: auto;
- }
- }
- .header-background {
- @include size(100%, 173px);
- @include bg('~/assets/images/home/header-bg.png', 100% 173px);
- }
- </style>
|