xiangyixiangqin/miniapp/pages/search/index.vue
2026-01-05 20:57:08 +08:00

1050 lines
26 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<view class="search-page">
<!-- 自定义导航栏 -->
<view class="custom-navbar" :style="{ paddingTop: statusBarHeight + 'px' }">
<view class="navbar-content">
<view class="navbar-back" @click="handleBack">
<text class="back-icon"></text>
</view>
<text class="navbar-title">搜索</text>
<view class="navbar-placeholder"></view>
</view>
</view>
<!-- 内容区域 -->
<scroll-view
class="content-scroll"
scroll-y
:style="{ paddingTop: (statusBarHeight + 44) + 'px' }"
>
<!-- 顶部Banner -->
<view class="banner-section">
<view class="banner-bg"></view>
<view class="banner-text">
<text class="banner-title">大胆说出</text>
<text class="banner-title">你的爱</text>
</view>
<view class="banner-decoration">
<text class="deco-heart">💕</text>
<text class="deco-ring">💍</text>
</view>
</view>
<!-- 筛选表单 -->
<view class="filter-form">
<!-- 年龄要求 -->
<view class="filter-item" @click="showAgePicker">
<text class="filter-label">年龄要求</text>
<view class="filter-value">
<text>{{ ageText }}</text>
<text class="arrow"></text>
</view>
</view>
<!-- 身高要求 -->
<view class="filter-item" @click="showHeightPicker">
<text class="filter-label">身高要求</text>
<view class="filter-value">
<text>{{ heightText }}</text>
<text class="arrow"></text>
</view>
</view>
<!-- 学历要求(可多选) -->
<view class="filter-item-multi">
<text class="filter-label">学历要求(可多选)</text>
<view class="tag-group">
<view
v-for="edu in educationOptions"
:key="edu.value"
class="tag-item"
:class="{ active: filters.education.includes(edu.value) }"
@click="toggleEducation(edu.value)"
>
{{ edu.label }}
</view>
</view>
</view>
<!-- 地区要求(可多选) -->
<view class="filter-item-multi">
<text class="filter-label">地区要求(可多选)</text>
<view class="tag-group">
<view
v-for="(city, index) in selectedCities"
:key="index"
class="tag-item active"
@click="removeCity(index)"
>
{{ city }}
<text class="tag-close">×</text>
</view>
<view class="tag-item add-tag" @click="showCityPicker">
+其他地区
</view>
</view>
</view>
<!-- 月收入要求 -->
<view class="filter-item" @click="showIncomePicker">
<text class="filter-label">月收入要求</text>
<view class="filter-value">
<text>{{ incomeText }}</text>
<text class="arrow"></text>
</view>
</view>
<!-- 房产要求 -->
<view class="filter-item" @click="showHousePicker">
<text class="filter-label">房产要求</text>
<view class="filter-value">
<text>{{ houseText }}</text>
<text class="arrow"></text>
</view>
</view>
<!-- 车产要求 -->
<view class="filter-item" @click="showCarPicker">
<text class="filter-label">车产要求</text>
<view class="filter-value">
<text>{{ carText }}</text>
<text class="arrow"></text>
</view>
</view>
<!-- 婚姻要求 -->
<view class="filter-item" @click="showMarriagePicker">
<text class="filter-label">婚姻要求</text>
<view class="filter-value">
<text>{{ marriageText }}</text>
<text class="arrow"></text>
</view>
</view>
</view>
<!-- 搜索按钮 -->
<view class="search-btn-section">
<button class="btn-search" @click="handleSearch">立即搜索</button>
</view>
</scroll-view>
<!-- 年龄选择弹窗 -->
<view class="picker-popup" v-if="showAgePopup" @click.self="showAgePopup = false">
<view class="picker-content">
<view class="picker-header">
<text class="picker-cancel" @click="showAgePopup = false">取消</text>
<text class="picker-title">年龄要求</text>
<text class="picker-confirm" @click="confirmAge">确定</text>
</view>
<view class="picker-body">
<picker-view :value="agePickerValue" @change="onAgeChange" class="picker-view">
<picker-view-column>
<view v-for="age in ageMinOptions" :key="age" class="picker-item">{{ age }}</view>
</picker-view-column>
<picker-view-column>
<view class="picker-item">至</view>
</picker-view-column>
<picker-view-column>
<view v-for="age in ageMaxOptions" :key="age" class="picker-item">{{ age }}</view>
</picker-view-column>
</picker-view>
</view>
</view>
</view>
<!-- 身高选择弹窗 -->
<view class="picker-popup" v-if="showHeightPopup" @click.self="showHeightPopup = false">
<view class="picker-content">
<view class="picker-header">
<text class="picker-cancel" @click="showHeightPopup = false">取消</text>
<text class="picker-title">身高要求</text>
<text class="picker-confirm" @click="confirmHeight">确定</text>
</view>
<view class="picker-body">
<picker-view :value="heightPickerValue" @change="onHeightChange" class="picker-view">
<picker-view-column>
<view v-for="h in heightMinOptions" :key="h" class="picker-item">{{ h }}</view>
</picker-view-column>
<picker-view-column>
<view class="picker-item">至</view>
</picker-view-column>
<picker-view-column>
<view v-for="h in heightMaxOptions" :key="h" class="picker-item">{{ h }}</view>
</picker-view-column>
</picker-view>
</view>
</view>
</view>
<!-- 收入选择弹窗 -->
<view class="picker-popup" v-if="showIncomePopup" @click.self="showIncomePopup = false">
<view class="picker-content">
<view class="picker-header">
<text class="picker-cancel" @click="showIncomePopup = false">取消</text>
<text class="picker-title">月收入要求</text>
<text class="picker-confirm" @click="confirmIncome">确定</text>
</view>
<view class="picker-body">
<picker-view :value="incomePickerValue" @change="onIncomeChange" class="picker-view">
<picker-view-column>
<view v-for="item in incomeOptions" :key="item.value" class="picker-item">{{ item.label }}</view>
</picker-view-column>
</picker-view>
</view>
</view>
</view>
<!-- 房产选择弹窗 -->
<view class="picker-popup" v-if="showHousePopup" @click.self="showHousePopup = false">
<view class="picker-content">
<view class="picker-header">
<text class="picker-cancel" @click="showHousePopup = false">取消</text>
<text class="picker-title">房产要求</text>
<text class="picker-confirm" @click="confirmHouse">确定</text>
</view>
<view class="picker-body">
<picker-view :value="housePickerValue" @change="onHouseChange" class="picker-view">
<picker-view-column>
<view v-for="item in houseOptions" :key="item.value" class="picker-item">{{ item.label }}</view>
</picker-view-column>
</picker-view>
</view>
</view>
</view>
<!-- 车产选择弹窗 -->
<view class="picker-popup" v-if="showCarPopup" @click.self="showCarPopup = false">
<view class="picker-content">
<view class="picker-header">
<text class="picker-cancel" @click="showCarPopup = false">取消</text>
<text class="picker-title">车产要求</text>
<text class="picker-confirm" @click="confirmCar">确定</text>
</view>
<view class="picker-body">
<picker-view :value="carPickerValue" @change="onCarChange" class="picker-view">
<picker-view-column>
<view v-for="item in carOptions" :key="item.value" class="picker-item">{{ item.label }}</view>
</picker-view-column>
</picker-view>
</view>
</view>
</view>
<!-- 婚姻选择弹窗 -->
<view class="picker-popup" v-if="showMarriagePopup" @click.self="showMarriagePopup = false">
<view class="picker-content">
<view class="picker-header">
<text class="picker-cancel" @click="showMarriagePopup = false">取消</text>
<text class="picker-title">婚姻要求</text>
<text class="picker-confirm" @click="confirmMarriage">确定</text>
</view>
<view class="picker-body">
<picker-view :value="marriagePickerValue" @change="onMarriageChange" class="picker-view">
<picker-view-column>
<view v-for="item in marriageOptions" :key="item.value" class="picker-item">{{ item.label }}</view>
</picker-view-column>
</picker-view>
</view>
</view>
</view>
<!-- 城市选择弹窗 -->
<view class="picker-popup" v-if="showCityPopup" @click="closeCityPopup">
<view class="picker-content city-picker-content" @click.stop>
<view class="picker-header">
<text class="picker-cancel" @click="showCityPopup = false">取消</text>
<text class="picker-title">选择地区</text>
<text class="picker-confirm" @click="showCityPopup = false">完成</text>
</view>
<!-- 热门城市 -->
<view class="city-section">
<view class="section-title">热门城市</view>
<view class="city-grid">
<view
v-for="city in hotCities"
:key="city"
class="city-item"
:class="{ selected: selectedCities.includes(city) }"
@click.stop="toggleCity(city)"
>
{{ city }}
</view>
</view>
</view>
<!-- 省份列表 -->
<view class="province-section">
<view class="section-title">按省份选择</view>
<scroll-view scroll-y class="province-scroll">
<view
v-for="province in provinceData"
:key="province.name"
class="province-item"
>
<view class="province-name" @click.stop="toggleProvince(province.name)">
<text>{{ province.name }}</text>
<text class="province-arrow" :class="{ expanded: expandedProvince === province.name }"></text>
</view>
<view class="city-list" v-if="expandedProvince === province.name">
<view
v-for="city in province.cities"
:key="city"
class="city-tag"
:class="{ selected: selectedCities.includes(city) }"
@click.stop="toggleCity(city)"
>
{{ city }}
</view>
</view>
</view>
</scroll-view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { provinceData, hotCities } from '@/utils/cityData.js'
// 状态栏高度
const statusBarHeight = ref(20)
// 获取系统信息
const getSystemInfo = () => {
uni.getSystemInfo({
success: (res) => {
statusBarHeight.value = res.statusBarHeight || 20
}
})
}
// 返回
const handleBack = () => {
uni.navigateBack()
}
// 筛选条件
const filters = reactive({
ageMin: 0,
ageMax: 0,
heightMin: 0,
heightMax: 0,
education: [],
cities: [],
monthlyIncome: 0,
houseStatus: 0,
carStatus: 0,
marriageStatus: 0
})
// 已选城市
const selectedCities = ref([])
// 弹窗显示状态
const showAgePopup = ref(false)
const showHeightPopup = ref(false)
const showIncomePopup = ref(false)
const showHousePopup = ref(false)
const showCarPopup = ref(false)
const showMarriagePopup = ref(false)
// 选项配置
const educationOptions = [
{ value: 0, label: '不限' },
{ value: 2, label: '中专' },
{ value: 1, label: '高中' },
{ value: 3, label: '大专' },
{ value: 4, label: '本科' },
{ value: 5, label: '研究生/博士' }
]
const incomeOptions = [
{ value: 0, label: '请选择' },
{ value: 1, label: '5000以下' },
{ value: 2, label: '5000-10000' },
{ value: 3, label: '10000-20000' },
{ value: 4, label: '20000-50000' },
{ value: 5, label: '50000以上' }
]
const houseOptions = [
{ value: 0, label: '请选择' },
{ value: 1, label: '现居地已购房' },
{ value: 2, label: '家乡已购房' },
{ value: 3, label: '婚后购房' },
{ value: 4, label: '父母同住' },
{ value: 5, label: '租房' },
{ value: 6, label: '近期有购房计划' }
]
const carOptions = [
{ value: 0, label: '请选择' },
{ value: 1, label: '已购车' },
{ value: 2, label: '无车' },
{ value: 3, label: '近期购车' }
]
const marriageOptions = [
{ value: 0, label: '请选择' },
{ value: 1, label: '未婚' },
{ value: 2, label: '离异未育' },
{ value: 3, label: '离异已育' }
]
// 年龄选项
const ageMinOptions = computed(() => {
const options = ['不限']
for (let a = 18; a <= 60; a++) {
options.push(a + '岁')
}
return options
})
const ageMaxOptions = computed(() => {
const options = ['不限']
for (let a = 18; a <= 60; a++) {
options.push(a + '岁')
}
return options
})
// 身高选项
const heightMinOptions = computed(() => {
const options = ['不限']
for (let h = 140; h <= 200; h++) {
options.push(h + 'cm')
}
return options
})
const heightMaxOptions = computed(() => {
const options = ['不限']
for (let h = 140; h <= 200; h++) {
options.push(h + 'cm')
}
return options
})
// Picker值
const agePickerValue = ref([0, 0, 0])
const heightPickerValue = ref([0, 0, 0])
const incomePickerValue = ref([0])
const housePickerValue = ref([0])
const carPickerValue = ref([0])
const marriagePickerValue = ref([0])
// 显示文本
const ageText = computed(() => {
if (!filters.ageMin && !filters.ageMax) return '请选择'
if (filters.ageMin && filters.ageMax) return `${filters.ageMin}-${filters.ageMax}`
if (filters.ageMin) return `${filters.ageMin}岁以上`
if (filters.ageMax) return `${filters.ageMax}岁以下`
return '请选择'
})
const heightText = computed(() => {
if (!filters.heightMin && !filters.heightMax) return '请选择'
if (filters.heightMin && filters.heightMax) return `${filters.heightMin}-${filters.heightMax}cm`
if (filters.heightMin) return `${filters.heightMin}cm以上`
if (filters.heightMax) return `${filters.heightMax}cm以下`
return '请选择'
})
const incomeText = computed(() => {
const item = incomeOptions.find(o => o.value === filters.monthlyIncome)
return item ? item.label : '请选择'
})
const houseText = computed(() => {
const item = houseOptions.find(o => o.value === filters.houseStatus)
return item ? item.label : '请选择'
})
const carText = computed(() => {
const item = carOptions.find(o => o.value === filters.carStatus)
return item ? item.label : '请选择'
})
const marriageText = computed(() => {
const item = marriageOptions.find(o => o.value === filters.marriageStatus)
return item ? item.label : '请选择'
})
// 显示弹窗
const showAgePicker = () => {
showAgePopup.value = true
}
const showHeightPicker = () => {
showHeightPopup.value = true
}
const showIncomePicker = () => {
showIncomePopup.value = true
}
const showHousePicker = () => {
showHousePopup.value = true
}
const showCarPicker = () => {
showCarPopup.value = true
}
const showMarriagePicker = () => {
showMarriagePopup.value = true
}
// Picker变化
const onAgeChange = (e) => {
agePickerValue.value = e.detail.value
}
const onHeightChange = (e) => {
heightPickerValue.value = e.detail.value
}
const onIncomeChange = (e) => {
incomePickerValue.value = e.detail.value
}
const onHouseChange = (e) => {
housePickerValue.value = e.detail.value
}
const onCarChange = (e) => {
carPickerValue.value = e.detail.value
}
const onMarriageChange = (e) => {
marriagePickerValue.value = e.detail.value
}
// 确认选择
const confirmAge = () => {
const minIdx = agePickerValue.value[0]
const maxIdx = agePickerValue.value[2]
filters.ageMin = minIdx === 0 ? 0 : minIdx + 17
filters.ageMax = maxIdx === 0 ? 0 : maxIdx + 17
showAgePopup.value = false
}
const confirmHeight = () => {
const minIdx = heightPickerValue.value[0]
const maxIdx = heightPickerValue.value[2]
filters.heightMin = minIdx === 0 ? 0 : minIdx + 139
filters.heightMax = maxIdx === 0 ? 0 : maxIdx + 139
showHeightPopup.value = false
}
const confirmIncome = () => {
filters.monthlyIncome = incomeOptions[incomePickerValue.value[0]].value
showIncomePopup.value = false
}
const confirmHouse = () => {
filters.houseStatus = houseOptions[housePickerValue.value[0]].value
showHousePopup.value = false
}
const confirmCar = () => {
filters.carStatus = carOptions[carPickerValue.value[0]].value
showCarPopup.value = false
}
const confirmMarriage = () => {
filters.marriageStatus = marriageOptions[marriagePickerValue.value[0]].value
showMarriagePopup.value = false
}
// 学历多选
const toggleEducation = (value) => {
if (value === 0) {
// 选择不限时清空其他选择
filters.education = [0]
return
}
// 移除不限
const noLimitIdx = filters.education.indexOf(0)
if (noLimitIdx >= 0) {
filters.education.splice(noLimitIdx, 1)
}
const idx = filters.education.indexOf(value)
if (idx >= 0) {
filters.education.splice(idx, 1)
} else {
filters.education.push(value)
}
}
// 城市选择弹窗
const showCityPopup = ref(false)
// 展开的省份
const expandedProvince = ref('')
// 城市选择
const showCityPicker = () => {
showCityPopup.value = true
}
// 关闭城市弹窗(点击遮罩层)
const closeCityPopup = (e) => {
// 只有点击遮罩层才关闭
if (e.target === e.currentTarget) {
showCityPopup.value = false
}
}
// 切换省份展开
const toggleProvince = (provinceName) => {
if (expandedProvince.value === provinceName) {
expandedProvince.value = ''
} else {
expandedProvince.value = provinceName
}
}
// 切换城市选择
const toggleCity = (city) => {
const idx = selectedCities.value.indexOf(city)
if (idx >= 0) {
selectedCities.value.splice(idx, 1)
} else {
selectedCities.value.push(city)
}
}
const removeCity = (index) => {
selectedCities.value.splice(index, 1)
}
// 搜索
const handleSearch = () => {
// 构建搜索参数
const params = {
ageMin: filters.ageMin,
ageMax: filters.ageMax,
heightMin: filters.heightMin,
heightMax: filters.heightMax,
education: filters.education.filter(e => e !== 0),
cities: selectedCities.value,
monthlyIncome: filters.monthlyIncome,
houseStatus: filters.houseStatus,
carStatus: filters.carStatus,
marriageStatus: filters.marriageStatus
}
// 跳转到搜索结果页
uni.navigateTo({
url: `/pages/search/result?params=${encodeURIComponent(JSON.stringify(params))}`
})
}
onMounted(() => {
getSystemInfo()
})
</script>
<style lang="scss" scoped>
.search-page {
min-height: 100vh;
background-color: #f5f6fa;
}
// 自定义导航栏
.custom-navbar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
background: linear-gradient(135deg, #ffb5b5 0%, #ff9a9a 100%);
.navbar-content {
height: 44px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24rpx;
.navbar-back {
width: 80rpx;
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
.back-icon {
font-size: 64rpx;
color: #fff;
font-weight: 400;
}
}
.navbar-title {
font-size: 34rpx;
font-weight: 600;
color: #fff;
}
.navbar-placeholder {
width: 80rpx;
}
}
}
// 内容滚动区域
.content-scroll {
min-height: 100vh;
padding-bottom: 60rpx;
}
// Banner区域
.banner-section {
position: relative;
width: 100%;
height: 320rpx;
overflow: hidden;
.banner-bg {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, #ffb5b5 0%, #ffd1d1 50%, #ffe4e4 100%);
}
.banner-text {
position: absolute;
left: 40rpx;
top: 50%;
transform: translateY(-50%);
z-index: 2;
.banner-title {
display: block;
font-size: 52rpx;
font-weight: bold;
color: #ff6b9d;
text-shadow: 2rpx 2rpx 4rpx rgba(255, 107, 157, 0.3);
line-height: 1.4;
}
}
.banner-decoration {
position: absolute;
right: 40rpx;
top: 50%;
transform: translateY(-50%);
z-index: 2;
.deco-heart {
font-size: 80rpx;
display: block;
animation: float 2s ease-in-out infinite;
}
.deco-ring {
font-size: 60rpx;
display: block;
margin-top: -20rpx;
margin-left: 40rpx;
animation: float 2s ease-in-out infinite 0.5s;
}
}
}
@keyframes float {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-10rpx);
}
}
// 筛选表单
.filter-form {
background: #fff;
margin: 24rpx;
border-radius: 24rpx;
overflow: hidden;
}
.filter-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 32rpx 28rpx;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.filter-label {
font-size: 30rpx;
color: #333;
}
.filter-value {
display: flex;
align-items: center;
text {
font-size: 28rpx;
color: #999;
}
.arrow {
margin-left: 8rpx;
font-size: 28rpx;
color: #ccc;
}
}
}
.filter-item-multi {
padding: 28rpx;
border-bottom: 1rpx solid #f5f5f5;
&:last-child {
border-bottom: none;
}
.filter-label {
display: block;
font-size: 30rpx;
color: #333;
margin-bottom: 20rpx;
}
}
.tag-group {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
.tag-item {
padding: 16rpx 28rpx;
background: #f5f5f5;
border-radius: 8rpx;
font-size: 26rpx;
color: #666;
border: 2rpx solid transparent;
transition: all 0.2s;
&.active {
background: #fff5f5;
color: #ff6b6b;
border-color: #ff6b6b;
}
&.add-tag {
color: #999;
border: 2rpx dashed #ddd;
background: transparent;
}
.tag-close {
margin-left: 8rpx;
font-size: 24rpx;
}
}
}
// 搜索按钮
.search-btn-section {
padding: 40rpx 24rpx;
.btn-search {
width: 100%;
height: 96rpx;
line-height: 96rpx;
background: linear-gradient(135deg, #ffb5b5 0%, #ff9a9a 100%);
border-radius: 48rpx;
font-size: 34rpx;
color: #fff;
border: none;
&::after {
border: none;
}
}
}
// Picker弹窗
.picker-popup {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 1000;
display: flex;
align-items: flex-end;
.picker-content {
width: 100%;
background: #fff;
border-radius: 24rpx 24rpx 0 0;
}
.picker-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 28rpx 32rpx;
border-bottom: 1rpx solid #f0f0f0;
.picker-cancel {
font-size: 30rpx;
color: #999;
}
.picker-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.picker-confirm {
font-size: 30rpx;
color: #ff6b6b;
}
}
.picker-body {
height: 400rpx;
}
.picker-view {
height: 100%;
}
.picker-item {
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
color: #333;
}
}
// 城市选择弹窗
.city-picker-content {
max-height: 80vh;
display: flex;
flex-direction: column;
.picker-placeholder {
width: 60rpx;
}
}
.city-section {
padding: 20rpx 24rpx;
border-bottom: 1rpx solid #f0f0f0;
}
.section-title {
font-size: 26rpx;
color: #999;
margin-bottom: 16rpx;
}
.city-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16rpx;
.city-item {
height: 64rpx;
line-height: 64rpx;
text-align: center;
background: #f5f5f5;
border-radius: 8rpx;
font-size: 26rpx;
color: #333;
border: 2rpx solid transparent;
&.selected {
background: #fff5f5;
color: #ff6b6b;
border-color: #ff6b6b;
}
}
}
.province-section {
flex: 1;
overflow: hidden;
display: flex;
flex-direction: column;
padding: 20rpx 24rpx 0;
.section-title {
flex-shrink: 0;
}
}
.province-scroll {
flex: 1;
height: 500rpx;
}
.province-item {
border-bottom: 1rpx solid #f5f5f5;
.province-name {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 0;
font-size: 30rpx;
color: #333;
.province-arrow {
font-size: 32rpx;
color: #ccc;
transition: transform 0.2s;
&.expanded {
transform: rotate(90deg);
}
}
}
.city-list {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
padding-bottom: 20rpx;
.city-tag {
padding: 12rpx 24rpx;
background: #f5f5f5;
border-radius: 8rpx;
font-size: 26rpx;
color: #666;
border: 2rpx solid transparent;
&.selected {
background: #fff5f5;
color: #ff6b6b;
border-color: #ff6b6b;
}
}
}
}
</style>