AI模块联调

This commit is contained in:
francis_fh
2026-01-22 18:58:19 +08:00
parent 4dce6d3e39
commit b92e3b8adb
23 changed files with 424 additions and 453 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,820 @@
<template>
<AppLayout title="选择地址" :showBack="true">
<view class="map-container">
<!-- 搜索框 -->
<view class="search-box">
<view class="search-input-wrapper">
<uni-icons type="search" size="20" color="#999"></uni-icons>
<input
class="search-input"
v-model="searchKeyword"
placeholder="输入关键词搜索地址(支持模糊搜索)"
@input="onSearchInput"
@confirm="searchLocation"
/>
<uni-icons
v-if="searchKeyword"
type="clear"
size="18"
color="#999"
@click="clearSearch"
></uni-icons>
</view>
</view>
<!-- 搜索结果列表 -->
<view class="search-results" v-if="showSearchResults">
<scroll-view scroll-y class="results-scroll" v-if="searchResults.length > 0">
<view
class="result-item"
v-for="(item, index) in searchResults"
:key="index"
@click="selectSearchResult(item)"
>
<view class="result-name">{{ item.name }}</view>
<view class="result-address">{{ item.address }}</view>
</view>
</scroll-view>
<view class="empty-results" v-else-if="isSearching">
<view class="loading-icon">
<uni-icons type="loop" size="40" color="#999"></uni-icons>
</view>
<text>搜索中...</text>
</view>
<view class="empty-results" v-else>
<uni-icons type="info" size="40" color="#999"></uni-icons>
<text>未找到相关地址请尝试其他关键词</text>
<view class="search-tips">
<text class="tip-title">搜索建议</text>
<text class="tip-item"> 输入具体地址名称</text>
<text class="tip-item"> 输入地标建筑名称</text>
<text class="tip-item"> 输入街道或区域名称</text>
</view>
</view>
</view>
<!-- 地图 -->
<view class="map-wrapper" v-show="!showSearchResults">
<!-- #ifdef H5 -->
<view id="amap-container" class="amap-container"></view>
<!-- #endif -->
<!-- #ifndef H5 -->
<map
id="map"
class="map"
:latitude="latitude"
:longitude="longitude"
:markers="markers"
:show-location="true"
@markertap="onMarkerTap"
@regionchange="onRegionChange"
@tap="onMapTap"
>
<cover-view class="map-center-marker">
<cover-image src="/static/icon/Location.png" class="marker-icon"></cover-image>
</cover-view>
</map>
<!-- #endif -->
</view>
<!-- 当前位置信息 -->
<view class="location-info" v-if="currentAddress && !showSearchResults">
<view class="info-title">当前选择位置</view>
<view class="info-name">{{ currentAddress.name }}</view>
<view class="info-address">{{ currentAddress.address }}</view>
</view>
<!-- 底部按钮 -->
<view class="bottom-actions">
<button class="locate-btn" @click="getCurrentLocation" :disabled="isLocating">
<uni-icons type="location-filled" size="20" color="#256BFA"></uni-icons>
<text>{{ isLocating ? '定位中...' : '定位' }}</text>
</button>
<button class="confirm-btn" @click="confirmLocation">确认选择</button>
</view>
</view>
</AppLayout>
</template>
<script setup>
import { ref, inject, onMounted } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
const { $api } = inject('globalFunction')
// 搜索相关
const searchKeyword = ref('')
const searchResults = ref([])
const showSearchResults = ref(false)
const isSearching = ref(false)
const isLocating = ref(false)
let searchTimer = null
// 地图相关
const latitude = ref(36.066938)
const longitude = ref(120.382665)
const markers = ref([])
const currentAddress = ref(null)
// H5地图实例
let map = null
let AMap = null
let geocoder = null
let placeSearch = null
onLoad((options) => {
// 可以接收初始位置参数
if (options.latitude && options.longitude) {
latitude.value = parseFloat(options.latitude)
longitude.value = parseFloat(options.longitude)
}
})
onMounted(() => {
// #ifdef H5
initAmapH5()
// #endif
// #ifndef H5
// 先设置默认位置,避免地图显示空白
markers.value = [{
id: 1,
latitude: latitude.value,
longitude: longitude.value,
iconPath: '/static/icon/Location.png',
width: 30,
height: 30
}]
// 延迟执行定位,避免页面加载时立即定位失败
setTimeout(() => {
getCurrentLocation()
}, 1000)
// #endif
})
// H5端初始化高德地图
const initAmapH5 = () => {
// #ifdef H5
if (window.AMap) {
AMap = window.AMap
initMap()
} else {
const script = document.createElement('script')
script.src = 'https://webapi.amap.com/maps?v=2.0&key=9cfc9370bd8a941951da1cea0308e9e3&plugin=AMap.Geocoder,AMap.PlaceSearch'
script.onload = () => {
AMap = window.AMap
initMap()
}
document.head.appendChild(script)
}
// #endif
}
// 初始化地图
const initMap = () => {
// #ifdef H5
map = new AMap.Map('amap-container', {
zoom: 15,
center: [longitude.value, latitude.value],
resizeEnable: true
})
// 创建标记
const marker = new AMap.Marker({
position: [longitude.value, latitude.value],
draggable: true
})
marker.on('dragend', (e) => {
const position = e.target.getPosition()
longitude.value = position.lng
latitude.value = position.lat
reverseGeocode(position.lng, position.lat)
})
map.add(marker)
// 初始化地理编码
geocoder = new AMap.Geocoder({
city: '全国'
})
// 初始化地点搜索
placeSearch = new AMap.PlaceSearch({
city: '全国',
pageSize: 10
})
// 地图点击事件
map.on('click', (e) => {
const { lng, lat } = e.lnglat
longitude.value = lng
latitude.value = lat
marker.setPosition([lng, lat])
reverseGeocode(lng, lat)
})
// 获取当前位置信息
reverseGeocode(longitude.value, latitude.value)
// #endif
}
// 搜索输入
const onSearchInput = () => {
if (searchTimer) {
clearTimeout(searchTimer)
}
if (!searchKeyword.value.trim()) {
showSearchResults.value = false
searchResults.value = []
isSearching.value = false
return
}
showSearchResults.value = true
isSearching.value = true
searchTimer = setTimeout(() => {
if (searchKeyword.value.trim()) {
searchLocation()
}
}, 300) // 优化防抖时间从500ms改为300ms
}
// 搜索地点
const searchLocation = () => {
if (!searchKeyword.value.trim()) {
return
}
showSearchResults.value = true
isSearching.value = true
// #ifdef H5
if (placeSearch) {
placeSearch.search(searchKeyword.value, (status, result) => {
isSearching.value = false
if (status === 'complete' && result.poiList) {
searchResults.value = result.poiList.pois.map(poi => ({
name: poi.name,
address: poi.address || poi.pname + poi.cityname + poi.adname,
location: poi.location,
lng: poi.location.lng,
lat: poi.location.lat
}))
} else {
searchResults.value = []
}
})
}
// #endif
// #ifndef H5
// 小程序端使用uni.request调用高德API
uni.request({
url: 'https://restapi.amap.com/v3/place/text',
data: {
key: '9cfc9370bd8a941951da1cea0308e9e3',
keywords: searchKeyword.value,
city: '全国',
offset: 20,
citylimit: false, // 不限制城市,支持全国搜索
extensions: 'all' // 返回详细信息
},
success: (res) => {
isSearching.value = false
console.log('搜索响应:', res.data) // 调试日志
if (res.data.status === '1' && res.data.pois && res.data.pois.length > 0) {
searchResults.value = res.data.pois.map(poi => {
const [lng, lat] = poi.location.split(',')
return {
name: poi.name,
address: poi.address || `${poi.pname || ''}${poi.cityname || ''}${poi.adname || ''}`,
lng: parseFloat(lng),
lat: parseFloat(lat)
}
})
console.log('搜索结果:', searchResults.value) // 调试日志
} else {
// 如果第一次搜索没有结果,尝试更宽泛的搜索
if (searchKeyword.value.length > 2) {
tryAlternativeSearch()
} else {
searchResults.value = []
console.log('搜索无结果:', res.data) // 调试日志
}
}
},
fail: (err) => {
isSearching.value = false
searchResults.value = []
console.error('搜索请求失败:', err) // 调试日志
$api.msg('搜索失败,请检查网络连接')
}
})
// #endif
}
// 备用搜索策略
const tryAlternativeSearch = () => {
// 尝试使用地理编码API搜索
uni.request({
url: 'https://restapi.amap.com/v3/geocode/geo',
data: {
key: '9cfc9370bd8a941951da1cea0308e9e3',
address: searchKeyword.value,
city: '全国'
},
success: (res) => {
isSearching.value = false
console.log('备用搜索响应:', res.data) // 调试日志
if (res.data.status === '1' && res.data.geocodes && res.data.geocodes.length > 0) {
searchResults.value = res.data.geocodes.map(geo => {
const [lng, lat] = geo.location.split(',')
return {
name: geo.formatted_address,
address: geo.formatted_address,
lng: parseFloat(lng),
lat: parseFloat(lat)
}
})
console.log('备用搜索结果:', searchResults.value) // 调试日志
} else {
searchResults.value = []
console.log('备用搜索也无结果:', res.data) // 调试日志
}
},
fail: (err) => {
isSearching.value = false
searchResults.value = []
console.error('备用搜索失败:', err) // 调试日志
}
})
}
// 选择搜索结果
const selectSearchResult = (item) => {
longitude.value = item.lng
latitude.value = item.lat
currentAddress.value = {
name: item.name,
address: item.address,
longitude: item.lng,
latitude: item.lat
}
// #ifdef H5
if (map) {
map.setCenter([item.lng, item.lat])
const marker = map.getAllOverlays('marker')[0]
if (marker) {
marker.setPosition([item.lng, item.lat])
}
}
// #endif
// #ifndef H5
markers.value = [{
id: 1,
latitude: item.lat,
longitude: item.lng,
iconPath: '/static/icon/Location.png',
width: 30,
height: 30
}]
// #endif
showSearchResults.value = false
searchKeyword.value = ''
}
// 清除搜索
const clearSearch = () => {
searchKeyword.value = ''
searchResults.value = []
showSearchResults.value = false
isSearching.value = false
if (searchTimer) {
clearTimeout(searchTimer)
}
}
// 逆地理编码(根据坐标获取地址)
const reverseGeocode = (lng, lat) => {
// #ifdef H5
if (geocoder) {
geocoder.getAddress([lng, lat], (status, result) => {
if (status === 'complete' && result.regeocode) {
const addressComponent = result.regeocode.addressComponent
const formattedAddress = result.regeocode.formattedAddress
currentAddress.value = {
name: addressComponent.building || addressComponent.township,
address: formattedAddress,
longitude: lng,
latitude: lat
}
}
})
}
// #endif
// #ifndef H5
uni.request({
url: 'https://restapi.amap.com/v3/geocode/regeo',
data: {
key: '9cfc9370bd8a941951da1cea0308e9e3',
location: `${lng},${lat}`
},
success: (res) => {
if (res.data.status === '1' && res.data.regeocode) {
const addressComponent = res.data.regeocode.addressComponent
const formattedAddress = res.data.regeocode.formatted_address
currentAddress.value = {
name: addressComponent.building || addressComponent.township || '选择的位置',
address: formattedAddress,
longitude: lng,
latitude: lat
}
}
}
})
// #endif
}
// 获取当前定位
const getCurrentLocation = () => {
if (isLocating.value) return // 防止重复定位
isLocating.value = true
uni.showLoading({ title: '定位中...' })
// 先检查定位权限
uni.getSetting({
success: (settingRes) => {
if (settingRes.authSetting['scope.userLocation'] === false) {
// 用户拒绝了定位权限,引导用户开启
isLocating.value = false
uni.hideLoading()
uni.showModal({
title: '定位权限',
content: '需要获取您的位置信息来提供更好的服务,请在设置中开启定位权限',
confirmText: '去设置',
success: (modalRes) => {
if (modalRes.confirm) {
uni.openSetting()
}
}
})
return
}
// 执行定位
uni.getLocation({
type: 'gcj02',
altitude: false,
success: (res) => {
console.log('定位成功:', res) // 调试日志
longitude.value = res.longitude
latitude.value = res.latitude
// #ifdef H5
if (map) {
map.setCenter([res.longitude, res.latitude])
const marker = map.getAllOverlays('marker')[0]
if (marker) {
marker.setPosition([res.longitude, res.latitude])
}
}
// #endif
// #ifndef H5
// 更新小程序端标记
markers.value = [{
id: 1,
latitude: res.latitude,
longitude: res.longitude,
iconPath: '/static/icon/Location.png',
width: 30,
height: 30
}]
// #endif
reverseGeocode(res.longitude, res.latitude)
uni.hideLoading()
isLocating.value = false
},
fail: (err) => {
console.error('定位失败:', err) // 调试日志
uni.hideLoading()
isLocating.value = false
// 根据错误类型给出不同提示
// let errorMsg = '定位失败'
// if (err.errMsg.includes('auth deny')) {
// errorMsg = '定位权限被拒绝,请在设置中开启'
// } else if (err.errMsg.includes('timeout')) {
// errorMsg = '定位超时,请重试'
// } else if (err.errMsg.includes('network')) {
// errorMsg = '网络异常,请检查网络连接'
// }
// uni.showModal({
// title: '定位失败',
// content: errorMsg + ',是否使用默认位置?',
// confirmText: '使用默认位置',
// cancelText: '重试',
// success: (modalRes) => {
// if (modalRes.confirm) {
// // 使用默认位置(北京)
// longitude.value = 116.397428
// latitude.value = 39.90923
// reverseGeocode(longitude.value, latitude.value)
// } else {
// // 重试定位
// setTimeout(() => {
// getCurrentLocation()
// }, 2000)
// }
// }
// })
}
})
},
fail: () => {
uni.hideLoading()
isLocating.value = false
$api.msg('无法获取定位权限设置')
}
})
}
// 地图区域变化(小程序端)
const onRegionChange = (e) => {
// #ifndef H5
// 只有在用户手动拖动地图结束时才更新位置
if (e.type === 'end' && e.causedBy === 'drag') {
const mapContext = uni.createMapContext('map')
mapContext.getCenterLocation({
success: (res) => {
longitude.value = res.longitude
latitude.value = res.latitude
reverseGeocode(res.longitude, res.latitude)
}
})
}
// #endif
}
// 地图点击事件(小程序端)
const onMapTap = (e) => {
// #ifndef H5
const { latitude: lat, longitude: lng } = e.detail
longitude.value = lng
latitude.value = lat
// 更新标记
markers.value = [{
id: 1,
latitude: lat,
longitude: lng,
iconPath: '/static/icon/Location.png',
width: 30,
height: 30
}]
reverseGeocode(lng, lat)
// #endif
}
// 确认选择
const confirmLocation = () => {
if (!currentAddress.value) {
$api.msg('请选择地址')
return
}
// 返回上一页并传递数据
const pages = getCurrentPages()
const prevPage = pages[pages.length - 2]
if (prevPage) {
prevPage.$vm.handleLocationSelected({
address: currentAddress.value.address,
name: currentAddress.value.name,
longitude: currentAddress.value.longitude,
latitude: currentAddress.value.latitude
})
}
uni.navigateBack()
}
const onMarkerTap = (e) => {
console.log('marker点击', e)
}
</script>
<style lang="stylus" scoped>
.map-container
width: 100%
height: 100vh
position: relative
display: flex
flex-direction: column
.search-box
position: absolute
top: 20rpx
left: 32rpx
right: 32rpx
z-index: 10
.search-input-wrapper
background: #fff
border-radius: 40rpx
padding: 20rpx 30rpx
display: flex
align-items: center
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.1)
.search-input
flex: 1
margin: 0 20rpx
font-size: 28rpx
uni-icons
flex-shrink: 0
.search-results
position: absolute
top: 100rpx
left: 32rpx
right: 32rpx
bottom: 180rpx
background: #fff
border-radius: 20rpx
box-shadow: 0 4rpx 12rpx rgba(0,0,0,0.1)
z-index: 9
overflow: hidden
.results-scroll
height: 100%
.result-item
padding: 30rpx
border-bottom: 1rpx solid #f0f0f0
&:active
background: #f5f5f5
.result-name
font-size: 32rpx
color: #333
font-weight: 500
margin-bottom: 10rpx
.result-address
font-size: 26rpx
color: #999
.empty-results
height: 100%
display: flex
flex-direction: column
align-items: center
justify-content: center
color: #999
font-size: 28rpx
.loading-icon
animation: rotate 1s linear infinite
margin-bottom: 20rpx
uni-icons
margin-bottom: 20rpx
text
padding: 0 60rpx
text-align: center
line-height: 1.5
.search-tips
margin-top: 40rpx
padding: 0 40rpx
.tip-title
font-size: 26rpx
color: #666
font-weight: 500
margin-bottom: 20rpx
display: block
.tip-item
font-size: 24rpx
color: #999
line-height: 1.8
display: block
margin-bottom: 8rpx
@keyframes rotate
from
transform: rotate(0deg)
to
transform: rotate(360deg)
.map-wrapper
flex: 1
position: relative
.map, .amap-container
width: 100%
height: 100%
.map-center-marker
position: absolute
left: 50%
top: 50%
transform: translate(-50%, -100%)
z-index: 5
.marker-icon
width: 60rpx
height: 80rpx
.location-info
position: absolute
bottom: 180rpx
left: 32rpx
right: 32rpx
background: #fff
border-radius: 20rpx
padding: 30rpx
box-shadow: 0 -4rpx 12rpx rgba(0,0,0,0.1)
z-index: 10
.info-title
font-size: 24rpx
color: #999
margin-bottom: 10rpx
.info-name
font-size: 32rpx
color: #333
font-weight: 500
margin-bottom: 10rpx
.info-address
font-size: 28rpx
color: #666
.bottom-actions
position: absolute
bottom: 0
left: 0
right: 0
background: #fff
padding: 20rpx 32rpx
display: flex
gap: 20rpx
box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.1)
z-index: 11
.locate-btn
width: 120rpx
height: 80rpx
background: #fff
border: 2rpx solid #256BFA
border-radius: 40rpx
display: flex
flex-direction: column
align-items: center
justify-content: center
font-size: 24rpx
color: #256BFA
&:disabled
opacity: 0.5
color: #999
border-color: #999
text
margin-top: 4rpx
.confirm-btn
flex: 1
height: 80rpx
background: #256BFA
color: #fff
border-radius: 40rpx
font-size: 32rpx
border: none
button::after
border: none
</style>

View File

@@ -0,0 +1,377 @@
<template>
<AppLayout>
<view class="skill-search-container">
<!-- 固定顶部区域 -->
<view class="fixed-header">
<!-- 搜索框 -->
<view class="search-box">
<input
class="search-input"
v-model="searchKeyword"
placeholder="请输入技能名称进行搜索"
@input="handleSearch"
confirm-type="search"
/>
<view class="search-icon" @click="handleSearch">搜索</view>
</view>
<!-- 已选技能提示 -->
<view class="selected-tip" v-if="selectedSkill">
已选择技能{{ selectedSkill }}
</view>
</view>
<!-- 可滚动内容区域 -->
<scroll-view class="scroll-content" scroll-y>
<view class="scroll-inner">
<!-- 搜索结果列表 -->
<view class="result-list" v-if="searchResults.length > 0">
<view
class="result-item"
v-for="(item, index) in searchResults"
:key="index"
@click="toggleSelect(item)"
>
<view class="item-content">
<text class="item-name">{{ item.name }}</text>
</view>
<view
class="item-checkbox"
:class="{ 'checked': isSelected(item) }"
>
<text v-if="isSelected(item)" class="check-icon"></text>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="!isSearching && searchResults.length === 0 && searchKeyword">
<text class="empty-text">未找到相关技能</text>
</view>
<!-- 初始提示 -->
<view class="empty-state" v-if="!isSearching && searchResults.length === 0 && !searchKeyword">
<text class="empty-text">请输入技能名称进行搜索</text>
</view>
</view>
</scroll-view>
<!-- 固定底部操作栏 -->
<view class="bottom-bar">
<view class="selected-skills" v-if="selectedSkill">
<view class="skill-tag">
<text class="tag-text">{{ selectedSkill }}</text>
<text class="tag-close" @click.stop="removeSkill">×</text>
</view>
</view>
<view class="action-buttons">
<button class="btn-cancel" @click="handleCancel">取消</button>
<button
class="btn-confirm"
:class="{ 'disabled': !selectedSkill }"
@click="handleConfirm"
:disabled="!selectedSkill"
>
确定
</button>
</view>
</view>
</view>
</AppLayout>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { inject } from 'vue';
const { $api } = inject('globalFunction');
const searchKeyword = ref('');
const searchResults = ref([]);
const selectedSkill = ref(''); // 改为单选,存储单个技能名称
const isSearching = ref(false);
let searchTimer = null;
onLoad((options) => {
// 接收已选中的技能(单选模式,只接收单个技能)
if (options.selected) {
try {
const skills = JSON.parse(decodeURIComponent(options.selected));
if (Array.isArray(skills) && skills.length > 0) {
selectedSkill.value = skills[0]; // 只取第一个技能
}
} catch (e) {
console.error('解析已选技能失败:', e);
}
}
});
// 搜索处理(防抖)
function handleSearch() {
const keyword = searchKeyword.value.trim();
if (!keyword) {
searchResults.value = [];
return;
}
// 清除之前的定时器
if (searchTimer) {
clearTimeout(searchTimer);
}
// 防抖处理500ms后执行搜索
searchTimer = setTimeout(() => {
performSearch(keyword);
}, 500);
}
// 执行搜索
async function performSearch(keyword) {
if (!keyword.trim()) {
searchResults.value = [];
return;
}
isSearching.value = true;
try {
const response = await $api.createRequest('/cms/dict/jobCategory', { name: keyword }, 'GET');
// 处理接口返回的数据,支持多种可能的返回格式
let results = [];
if (response) {
if (Array.isArray(response)) {
// 如果直接返回数组
results = response;
} else if (response.data) {
// 如果返回 { data: [...] }
results = Array.isArray(response.data) ? response.data : [];
} else if (response.list) {
// 如果返回 { list: [...] }
results = Array.isArray(response.list) ? response.list : [];
}
}
// 确保每个结果都有name字段
searchResults.value = results.map(item => {
if (typeof item === 'string') {
return { name: item };
}
return item;
});
} catch (error) {
console.error('搜索技能失败:', error);
$api.msg('搜索失败,请稍后重试');
searchResults.value = [];
} finally {
isSearching.value = false;
}
}
// 判断技能是否已选中
function isSelected(item) {
return selectedSkill.value === item.name;
}
// 切换技能选择状态(单选模式)
function toggleSelect(item) {
const skillName = item.name;
if (selectedSkill.value === skillName) {
// 已选中,取消选择
selectedSkill.value = '';
} else {
// 选择新的技能
selectedSkill.value = skillName;
}
}
// 移除技能(单选模式,直接清空)
function removeSkill() {
selectedSkill.value = '';
}
// 取消操作
function handleCancel() {
uni.navigateBack();
}
// 确定操作
function handleConfirm() {
if (!selectedSkill.value) {
$api.msg('请选择一个技能');
return;
}
// 通过事件总线传递选中的技能(单选模式,传递单个技能名称)
uni.$emit('skillSelected', selectedSkill.value);
// 返回上一页
uni.navigateBack();
}
onUnmounted(() => {
// 清理定时器
if (searchTimer) {
clearTimeout(searchTimer);
}
});
</script>
<style lang="stylus" scoped>
.skill-search-container
display: flex
flex-direction: column
height: 100%
background-color: #f5f5f5
.fixed-header
flex-shrink: 0
background-color: #fff
border-bottom: 2rpx solid #ebebeb
.search-box
display: flex
align-items: center
padding: 24rpx
background-color: #fff
.search-input
flex: 1
height: 72rpx
padding: 0 24rpx
background-color: #f5f5f5
border-radius: 36rpx
font-size: 28rpx
color: #333
.search-icon
margin-left: 24rpx
padding: 16rpx 32rpx
background-color: #256bfa
color: #fff
border-radius: 36rpx
font-size: 28rpx
.selected-tip
padding: 16rpx 24rpx
background-color: #fff3cd
color: #856404
font-size: 24rpx
text-align: center
.scroll-content
flex: 1
overflow: hidden
height: 0 // 关键让flex布局正确计算高度
background-color: #fff
.scroll-inner
padding-bottom: 40rpx
.result-list
background-color: #fff
padding: 0 24rpx
.result-item
display: flex
align-items: center
justify-content: space-between
padding: 32rpx 0
border-bottom: 2rpx solid #ebebeb
.item-content
flex: 1
.item-name
font-size: 32rpx
color: #333
.item-checkbox
width: 48rpx
height: 48rpx
border: 2rpx solid #ddd
border-radius: 50%
display: flex
align-items: center
justify-content: center
margin-left: 24rpx
&.checked
background-color: #256bfa
border-color: #256bfa
.check-icon
color: #fff
font-size: 32rpx
font-weight: bold
.empty-state
display: flex
align-items: center
justify-content: center
min-height: 400rpx
.empty-text
font-size: 28rpx
color: #999
.bottom-bar
flex-shrink: 0
background-color: #fff
border-top: 2rpx solid #ebebeb
padding: 24rpx
box-shadow: 0 -4rpx 12rpx rgba(0, 0, 0, 0.05) // 添加阴影,让底部栏更明显
.selected-skills
display: flex
flex-wrap: wrap
margin-bottom: 24rpx
min-height: 60rpx
.skill-tag
display: inline-flex
align-items: center
padding: 12rpx 24rpx
margin-right: 16rpx
margin-bottom: 16rpx
background-color: #e8f4ff
border-radius: 32rpx
border: 2rpx solid #256bfa
.tag-text
font-size: 26rpx
color: #256bfa
margin-right: 12rpx
.tag-close
font-size: 32rpx
color: #256bfa
line-height: 1
cursor: pointer
.action-buttons
display: flex
gap: 24rpx
.btn-cancel, .btn-confirm
flex: 1
height: 88rpx
border-radius: 12rpx
font-size: 32rpx
border: none
.btn-cancel
background-color: #f5f5f5
color: #666
.btn-confirm
background-color: #256bfa
color: #fff
&.disabled
background-color: #ccc
color: #999
</style>

View File

@@ -0,0 +1,474 @@
<template>
<view class="company-search-page">
<!-- 头部导航 -->
<view class="header">
<view class="header-left" @click="goBack">
<view class="back-arrow"><uni-icons type="search" size="32" color="#FFFFFF"></uni-icons></view>
</view>
<view class="header-title">选择企业</view>
<view class="header-right"></view>
</view>
<!-- 搜索框 -->
<view class="search-container">
<view class="search-box">
<view class="search-icon">🔍</view>
<input
class="search-input"
placeholder="请输入企业名称进行搜索"
v-model="searchKeyword"
@input="onSearchInput"
/>
<view class="clear-btn" v-if="searchKeyword" @click="clearSearch">
<view class="clear-icon"></view>
</view>
</view>
</view>
<!-- 搜索结果 -->
<scroll-view class="content" scroll-y="true" :style="{ height: scrollViewHeight }">
<!-- 搜索提示 -->
<view class="search-tip" v-if="!searchKeyword">
<text class="tip-text">请输入企业名称进行搜索</text>
</view>
<!-- 加载中 -->
<view class="loading-container" v-if="loading">
<view class="loading-text">搜索中...</view>
</view>
<!-- 搜索结果列表 -->
<view class="result-list" v-if="searchResults.length > 0">
<view
class="result-item"
v-for="(company, index) in searchResults"
:key="company.companyId"
@click="selectCompany(company)"
>
<view class="company-info">
<view class="company-name">{{ company.name }}</view>
<view class="company-location" v-if="company.location">
<text class="location-icon">📍</text>
<text class="location-text">{{ company.location }}</text>
</view>
<view class="company-scale" v-if="company.scale">
<text class="scale-label">规模</text>
<text class="scale-text">{{ getScaleText(company.scale) }}</text>
</view>
<view class="company-nature" v-if="company.nature">
<text class="nature-label">性质</text>
<text class="nature-text">{{ getNatureText(company.nature) }}</text>
</view>
<view class="company-description" v-if="company.description">
<text class="desc-text">{{ company.description }}</text>
</view>
</view>
<view class="select-icon">
<view class="arrow-icon"></view>
</view>
</view>
</view>
<!-- 无结果提示 -->
<view class="no-result" v-if="searchKeyword && !loading && searchResults.length === 0">
<view class="empty-icon">📭</view>
<view class="no-result-text">未找到相关企业</view>
<view class="no-result-tip">请尝试其他关键词</view>
</view>
<!-- 底部安全区域 -->
<view class="bottom-safe-area"></view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue';
import { createRequest } from '@/utils/request';
// 搜索关键词
const searchKeyword = ref('');
// 搜索结果
const searchResults = ref([]);
// 加载状态
const loading = ref(false);
// 防抖定时器
let debounceTimer = null;
// 滚动视图高度
const scrollViewHeight = ref('calc(100vh - 200rpx)');
// 计算滚动视图高度
const calculateScrollViewHeight = () => {
const systemInfo = uni.getSystemInfoSync();
const windowHeight = systemInfo.windowHeight;
const headerHeight = 100; // 头部高度
const searchHeight = 120; // 搜索框高度
const scrollHeight = windowHeight - headerHeight - searchHeight;
scrollViewHeight.value = `${scrollHeight}px`;
};
// 页面加载时计算高度
onMounted(() => {
calculateScrollViewHeight();
});
// 搜索输入处理(防抖)
const onSearchInput = () => {
// 清除之前的定时器
if (debounceTimer) {
clearTimeout(debounceTimer);
}
// 设置新的定时器
debounceTimer = setTimeout(() => {
if (searchKeyword.value.trim()) {
searchCompanies();
} else {
searchResults.value = [];
}
}, 500); // 500ms 防抖延迟
};
// 搜索企业
const searchCompanies = async () => {
if (!searchKeyword.value.trim()) {
searchResults.value = [];
return;
}
try {
loading.value = true;
const response = await createRequest('/app/company/likeList', {
name: searchKeyword.value.trim()
}, 'get', false);
if (response.code === 200) {
searchResults.value = response.rows || [];
} else {
uni.showToast({
title: response.msg || '搜索失败',
icon: 'none'
});
searchResults.value = [];
}
} catch (error) {
console.error('搜索企业失败:', error);
uni.showToast({
title: '搜索失败,请重试',
icon: 'none'
});
searchResults.value = [];
} finally {
loading.value = false;
}
};
// 清除搜索
const clearSearch = () => {
searchKeyword.value = '';
searchResults.value = [];
if (debounceTimer) {
clearTimeout(debounceTimer);
}
};
// 获取企业规模文本
const getScaleText = (scale) => {
const scaleMap = {
'1': '1-20人',
'2': '21-50人',
'3': '51-100人',
'4': '101-200人',
'5': '201-500人',
'6': '500人以上'
};
return scaleMap[scale] || '未知';
};
// 获取企业性质文本
const getNatureText = (nature) => {
const natureMap = {
'1': '有限责任公司',
'2': '股份有限公司',
'3': '个人独资企业',
'4': '合伙企业',
'5': '外商投资企业',
'6': '其他'
};
return natureMap[nature] || '未知';
};
// 选择企业
const selectCompany = (company) => {
// 返回上一页并传递选中的企业信息
uni.navigateBack({
delta: 1,
success: () => {
// 通过事件总线或全局状态传递数据
uni.$emit('companySelected', {
id: company.companyId,
name: company.name,
address: company.location
});
}
});
};
// 返回上一页
const goBack = () => {
uni.navigateBack();
};
</script>
<style lang="scss" scoped>
.company-search-page {
height: 100vh;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 30rpx;
background: #fff;
border-bottom: 1rpx solid #eee;
.header-left {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
.back-arrow {
font-size: 40rpx;
color: #333;
font-weight: bold;
}
}
.header-title {
font-size: 36rpx;
font-weight: 600;
color: #333;
}
.header-right {
width: 60rpx;
}
}
.search-container {
padding: 20rpx 30rpx;
background: #fff;
border-bottom: 1rpx solid #eee;
.search-box {
display: flex;
align-items: center;
background: #f5f5f5;
border-radius: 12rpx;
padding: 0 20rpx;
height: 80rpx;
.search-icon {
font-size: 32rpx;
margin-right: 20rpx;
color: #999;
}
.search-input {
flex: 1;
height: 100%;
background: transparent;
border: none;
font-size: 28rpx;
color: #333;
line-height: 1.4;
display: flex;
align-items: center;
&::placeholder {
color: #999;
font-size: 28rpx;
line-height: 1.4;
}
}
.clear-btn {
width: 32rpx;
height: 32rpx;
display: flex;
align-items: center;
justify-content: center;
margin-left: 20rpx;
.clear-icon {
font-size: 24rpx;
color: #999;
}
}
}
}
.content {
flex: 1;
padding: 0;
overflow: hidden;
}
.search-tip {
display: flex;
justify-content: center;
align-items: center;
height: 400rpx;
.tip-text {
font-size: 28rpx;
color: #999;
}
}
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 200rpx;
.loading-text {
font-size: 28rpx;
color: #666;
}
}
.result-list {
background: #fff;
.result-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
}
&:active {
background: #f8f8f8;
}
.company-info {
flex: 1;
.company-name {
font-size: 30rpx;
color: #333;
font-weight: 500;
margin-bottom: 15rpx;
line-height: 1.4;
}
.company-location {
display: flex;
align-items: center;
margin-bottom: 10rpx;
.location-icon {
font-size: 20rpx;
margin-right: 8rpx;
}
.location-text {
font-size: 24rpx;
color: #666;
flex: 1;
}
}
.company-scale, .company-nature {
display: flex;
align-items: center;
margin-bottom: 8rpx;
.scale-label, .nature-label {
font-size: 22rpx;
color: #999;
margin-right: 8rpx;
}
.scale-text, .nature-text {
font-size: 22rpx;
color: #666;
}
}
.company-description {
margin-top: 10rpx;
.desc-text {
font-size: 22rpx;
color: #888;
line-height: 1.4;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
line-clamp: 2;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
.select-icon {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
.arrow-icon {
font-size: 24rpx;
color: #999;
}
}
}
}
.no-result {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 400rpx;
.empty-icon {
font-size: 120rpx;
margin-bottom: 30rpx;
}
.no-result-text {
font-size: 28rpx;
color: #666;
margin-bottom: 10rpx;
}
.no-result-tip {
font-size: 24rpx;
color: #999;
}
}
.bottom-safe-area {
height: 120rpx;
background: transparent;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,385 @@
<template>
<view class="nearby-container">
<view class="two-head">
<view
class="head-item"
:class="{ active: state.comId === item.commercialAreaId }"
v-for="(item, index) in state.comlist"
:key="item.commercialAreaName"
@click="clickCommercialArea(item)"
>
{{ item.commercialAreaName }}
</view>
</view>
<scroll-view :scroll-y="true" class="nearby-scroll" @scrolltolower="scrollBottom" lower-threshold="50">
<view class="nearby-list">
<view class="nav-filter" @touchmove.stop.prevent>
<view class="filter-top">
<scroll-view :scroll-x="true" :show-scrollbar="false" class="tab-scroll">
<view class="jobs-left">
<view
class="job button-click"
:class="{ active: state.tabIndex === 'all' }"
@click="choosePosition('all')"
>
全部
</view>
<view
class="job button-click"
:class="{ active: state.tabIndex === index }"
v-for="(item, index) in userInfo.jobTitle"
:key="index"
@click="choosePosition(index)"
>
{{ item }}
</view>
</view>
</scroll-view>
<view class="jobs-add button-click" @click="navTo('/packageA/pages/addPosition/addPosition')">
<uni-icons class="iconsearch" color="#666D7F" type="plusempty" size="18"></uni-icons>
<text>添加</text>
</view>
</view>
<view class="filter-bottom">
<view class="btm-left">
<view
class="button-click filterbtm"
:class="{ active: pageState.search.order === item.value }"
v-for="item in rangeOptions"
@click="handelHostestSearch(item)"
:key="item.value"
>
{{ item.text }}
</view>
</view>
<view class="btm-right button-click" @click="openFilter">
筛选
<image class="right-sx" :class="{ active: showFilter }" src="@/static/icon/shaixun.png"></image>
</view>
</view>
</view>
<view class="one-cards">
<renderJobs
v-if="list.length"
:list="list"
:longitude="longitudeVal"
:latitude="latitudeVal"
></renderJobs>
<empty v-else pdTop="60"></empty>
<loadmore ref="loadmoreRef"></loadmore>
</view>
</view>
</scroll-view>
<!-- 筛选 -->
<select-filter ref="selectFilterModel"></select-filter>
</view>
</template>
<script setup>
import { reactive, inject, watch, ref, onMounted, onBeforeUnmount } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
const { $api, navTo, debounce, customSystem } = inject('globalFunction');
import { storeToRefs } from 'pinia';
import useLocationStore from '@/stores/useLocationStore';
import useUserStore from '@/stores/useUserStore';
import useDictStore from '@/stores/useDictStore';
const { oneDictData } = useDictStore();
const { userInfo } = storeToRefs(useUserStore());
const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
import point2 from '@/static/icon/point2.png';
import LocationPng from '@/static/icon/Location.png';
import selectFilter from '@/components/selectFilter/selectFilter.vue';
const emit = defineEmits(['onFilter']);
const state = reactive({
tabIndex: 'all',
tabBxText: 'buxianquyu',
comlist: [],
comId: 0,
areaInfo: {},
});
const isLoaded = ref(false);
const showFilter = ref(false);
const selectFilterModel = ref();
const fromValue = reactive({
area: 0,
});
const loadmoreRef = ref(null);
const pageState = reactive({
page: 0,
total: 0,
maxPage: 2,
pageSize: 10,
search: {
order: 0,
},
});
const list = ref([]);
const rangeOptions = ref([
{ value: 0, text: '推荐' },
{ value: 1, text: '最热' },
{ value: 2, text: '最新发布' },
{ value: 3, text: '疆外' },
]);
function choosePosition(index) {
state.tabIndex = index;
list.value = [];
if (index === 'all') {
pageState.search = {
...pageState.search,
jobTitle: '',
};
getJobList('refresh');
} else {
// const id = useUserStore().userInfo.jobTitleId.split(',')[index];
pageState.search.jobTitle = userInfo.value.jobTitle[index];
getJobList('refresh');
}
}
function openFilter() {
showFilter.value = true;
selectFilterModel.value?.open({
title: '筛选',
maskClick: true,
success: (values) => {
pageState.search = {
...pageState.search,
};
for (const [key, value] of Object.entries(values)) {
pageState.search[key] = value.join(',');
}
showFilter.value = false;
console.log(pageState.search);
getJobList('refresh');
},
cancel: () => {
showFilter.value = false;
},
});
}
onLoad(() => {
getBusinessDistrict();
});
async function loadData() {
try {
if (isLoaded.value) return;
const area = oneDictData('area')[0];
fromValue.area = area.value;
getJobList('refresh');
isLoaded.value = true;
} catch (err) {
isLoaded.value = false; // 重置状态允许重试
throw err;
}
}
function clickCommercialArea(area) {
state.areaInfo = area;
state.comId = area.commercialAreaId;
getJobList('refresh');
}
function scrollBottom() {
getJobList();
loadmoreRef.value.change('loading');
}
// function choosePosition(index) {
// state.tabIndex = index;
// if (index === 'all') {
// pageState.search.jobTitle = '';
// } else {
// pageState.search.jobTitle = userInfo.jobTitle[index];
// }
// getJobList('refresh');
// }
function changeArea(area, item) {
fromValue.area = area;
getJobList('refresh');
}
function getBusinessDistrict() {
$api.createRequest(`/app/common/commercialArea`).then((resData) => {
if (resData.data.length) {
state.comlist = resData.data;
state.areaInfo = resData.data[0];
state.comId = resData.data[0].commercialAreaId;
}
});
}
function getJobList(type = 'add') {
if (type === 'add' && pageState.page < pageState.maxPage) {
pageState.page += 1;
}
if (type === 'refresh') {
pageState.page = 1;
pageState.maxPage = 2;
}
let params = {
longitude: state.areaInfo.longitude,
latitude: state.areaInfo.latitude,
current: pageState.page,
pageSize: pageState.pageSize,
radius: 2,
...pageState.search,
};
if (fromValue.area === state.tabBxText) {
params.countyIds = [];
}
$api.createRequest('/app/job/countyJob', params, 'POST').then((resData) => {
const { rows, total } = resData;
if (type === 'add') {
const str = pageState.pageSize * (pageState.page - 1);
const end = list.value.length;
const reslist = rows;
list.value.splice(str, end, ...reslist);
} else {
list.value = rows;
}
pageState.total = resData.total;
pageState.maxPage = Math.ceil(pageState.total / pageState.pageSize);
if (loadmoreRef.value && typeof loadmoreRef.value.change === 'function') {
if (rows.length < pageState.pageSize) {
loadmoreRef.value.change('noMore');
} else {
loadmoreRef.value.change('more');
}
}
});
}
function handelHostestSearch(val) {
pageState.search.order = val.value;
getJobList('refresh');
}
function handleFilterConfirm(val) {
pageState.search = {
order: pageState.search.order,
};
for (const [key, value] of Object.entries(val)) {
pageState.search[key] = value.join(',');
}
getJobList('refresh');
}
defineExpose({ loadData, handleFilterConfirm });
</script>
<style lang="stylus" scoped>
.tabchecked
color: #4778EC !important
.nearby-container
// 确保容器占据整个可用视口高度,包括安全区域
height: calc(100vh - var(--window-top));
display: flex;
flex-direction: column;
overflow: hidden;
.nearby-scroll
// 使用flex布局让scroll-view自适应高度占据剩余空间
flex: 1;
overflow: hidden;
.two-head
margin: 22rpx;
display: flex;
flex-wrap: wrap
// grid-template-columns: repeat(4, 1fr);
// grid-column-gap: 10rpx;
// grid-row-gap: 24rpx;
border-radius: 17rpx 17rpx 17rpx 17rpx;
.head-item
margin: 10rpx
white-space: nowrap
min-width: 156rpx
line-height: 64rpx
text-align: center;
width: fit-content;
font-size: 21rpx;
font-weight: 400;
font-size: 28rpx;
color: #6C7282;
background: #F6F6F6;
border-radius: 12rpx 12rpx 12rpx 12rpx;
.active
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
color: #256BFA;
background: #E9F0FF;
border-radius: 12rpx 12rpx 12rpx 12rpx;
.nearby-list
border-top: 2rpx solid #EBEBEB;
.one-cards{
display: flex;
flex-direction: column;
padding: 0 20rpx 20rpx 20rpx;
background: #f4f4f4
}
.nav-filter
padding: 16rpx 28rpx 0 28rpx
.filter-top
display: flex
justify-content: space-between;
.tab-scroll
flex: 1;
overflow: hidden;
margin-right: 20rpx
white-space: nowrap;
overflow: hidden;
text-overflow: clip;
-webkit-mask-image: linear-gradient(to right, black 60%, transparent);
mask-image: linear-gradient(to right, black 60%, transparent);
.jobs-left
display: flex
flex-wrap: nowrap
.job
font-weight: 400;
font-size: 36rpx;
color: #666D7F;
margin-right: 32rpx;
white-space: nowrap
.active
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
font-weight: 500;
font-size: 36rpx;
color: #000000;
.jobs-add
display: flex
align-items: center;
justify-content: center;
font-weight: 400;
font-size: 32rpx;
color: #666D7F;
line-height: 38rpx;
.filter-bottom
display: flex
justify-content: space-between
padding: 24rpx 0
.btm-left
display: flex
.filterbtm
font-weight: 400;
font-size: 32rpx;
color: #666D7F;
margin-right: 40rpx
.active
font-weight: 500;
font-size: 32rpx;
color: #256BFA;
.btm-right
font-weight: 400;
font-size: 32rpx;
color: #6C7282;
.right-sx
width: 26rpx;
height: 26rpx;
.active
transform: rotate(180deg)
</style>

View File

@@ -0,0 +1,481 @@
<template>
<view class="nearby-container">
<!-- 地图区域 - 可折叠 -->
<view
class="nearby-map"
@touchmove.stop.prevent
:class="{ 'map-collapsed': isMapCollapsed }"
>
<map
style="width: 100%; height: 400px"
:latitude="latitudeVal"
:longitude="longitudeVal"
:markers="mapCovers"
:circles="mapCircles"
:controls="mapControls"
@controltap="handleControl"
></map>
<view class="nearby-select">
<view class="select-view" @click="changeRangeShow">
<text>{{ pageState.search.radius }}km</text>
<image class="view-sx" :class="{ active: rangeShow }" src="@/static/icon/shaixun.png"></image>
</view>
<transition name="fade-slide">
<view class="select-list" v-if="rangeShow">
<view class="list-item button-click" v-for="(item, index) in range" @click="changeRadius(item)">
{{ item }}km
</view>
</view>
</transition>
</view>
</view>
<!-- 筛选条件 - 固定显示 -->
<view class="nav-filter" @touchmove.stop.prevent>
<view class="filter-top">
<scroll-view :scroll-x="true" :show-scrollbar="false" class="tab-scroll">
<view class="jobs-left">
<view
class="job button-click"
:class="{ active: state.tabIndex === 'all' }"
@click="choosePosition('all')"
>
全部
</view>
<view
class="job button-click"
:class="{ active: state.tabIndex === index }"
v-for="(item, index) in userInfo.jobTitle"
:key="index"
@click="choosePosition(index)"
>
{{ item }}
</view>
</view>
</scroll-view>
<view class="jobs-add button-click" @click="navTo('/packageA/pages/addPosition/addPosition')">
<uni-icons class="iconsearch" color="#666D7F" type="plusempty" size="18"></uni-icons>
<text>添加</text>
</view>
</view>
<view class="filter-bottom">
<view class="btm-left">
<view
class="button-click filterbtm"
:class="{ active: pageState.search.order === item.value }"
v-for="item in rangeOptions"
@click="handelHostestSearch(item)"
:key="item.value"
>
{{ item.text }}
</view>
</view>
<view class="btm-right button-click" @click="openFilter">
筛选
<image class="right-sx" :class="{ active: showFilter }" src="@/static/icon/shaixun.png"></image>
</view>
</view>
</view>
<!-- 列表滚动区域 -->
<scroll-view
:scroll-y="true"
class="nearby-scroll"
@scrolltolower="scrollBottom"
lower-threshold="50"
ref="scrollViewRef"
@scroll="handleScroll"
>
<view class="nearby-list">
<view class="one-cards">
<renderJobs
v-if="list.length"
:list="list"
:longitude="longitudeVal"
:latitude="latitudeVal"
></renderJobs>
<empty v-else pdTop="60"></empty>
<loadmore ref="loadmoreRef"></loadmore>
</view>
</view>
</scroll-view>
<!-- 筛选 -->
<select-filter ref="selectFilterModel"></select-filter>
</view>
</template>
<script setup>
import { reactive, inject, watch, ref, onMounted, onBeforeUnmount } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
const { $api, navTo, debounce, customSystem } = inject('globalFunction');
import { storeToRefs } from 'pinia';
import useLocationStore from '@/stores/useLocationStore';
import useUserStore from '@/stores/useUserStore';
const { userInfo } = storeToRefs(useUserStore());
const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
import point2 from '@/static/icon/Location1.png';
import LocationPng from '@/static/icon/Location12.png';
import selectFilter from '@/components/selectFilter/selectFilter.vue';
const emit = defineEmits(['onFilter']);
const range = ref([1, 2, 4, 6, 8, 10]);
const rangeShow = ref(false);
const selectFilterModel = ref(null);
const scrollViewRef = ref(null);
const isMapCollapsed = ref(false);
const tMap = ref();
const progress = ref();
const mapCovers = ref([]);
const mapCircles = ref([]);
const mapControls = ref([
{
id: 1,
position: {
// 控件位置
left: customSystem.systemInfo.screenWidth - 48 - 14,
top: 320,
width: 48,
height: 48,
},
iconPath: LocationPng, // 控件图标
},
]);
const loadmoreRef = ref(null);
const pageState = reactive({
page: 0,
total: 100,
maxPage: 2,
pageSize: 10,
search: {
radius: 1,
order: 0,
},
});
const isLoaded = ref(false);
const showFilter = ref(false);
const list = ref([]);
const state = reactive({
progressWidth: '200px',
});
const rangeOptions = ref([
{ value: 0, text: '推荐' },
{ value: 1, text: '最热' },
{ value: 2, text: '最新发布' },
{ value: 3, text: '疆外' },
]);
function changeRangeShow() {
rangeShow.value = !rangeShow.value;
}
function changeRadius(item) {
pageState.search.radius = item;
rangeShow.value = false;
progressChange(item);
}
function choosePosition(index) {
state.tabIndex = index;
list.value = [];
if (index === 'all') {
pageState.search = {
...pageState.search,
jobTitle: '',
};
getJobList('refresh');
} else {
// const id = useUserStore().userInfo.jobTitleId.split(',')[index];
pageState.search.jobTitle = useUserStore().userInfo.jobTitle[index];
getJobList('refresh');
}
}
function openFilter() {
showFilter.value = true;
selectFilterModel.value?.open({
title: '筛选',
maskClick: true,
success: (values) => {
pageState.search = {
...pageState.search,
};
for (const [key, value] of Object.entries(values)) {
pageState.search[key] = value.join(',');
}
showFilter.value = false;
console.log(pageState.search);
getJobList('refresh');
},
cancel: () => {
showFilter.value = false;
},
});
}
onLoad(() => {});
async function loadData() {
try {
if (isLoaded.value) return;
isLoaded.value = true;
} catch (err) {
isLoaded.value = false; // 重置状态允许重试
throw err;
}
}
function scrollBottom() {
getJobList();
loadmoreRef.value.change('loading');
}
function handleControl(e) {
switch (e.detail.controlId) {
case 1:
getInit();
break;
}
}
onMounted(() => {
// $api.msg('使用模拟定位');
getInit();
});
function getInit() {
useLocationStore()
.getLocation()
.then((res) => {
mapCovers.value = [
{
latitude: res.latitude,
longitude: res.longitude,
iconPath: point2,
},
];
mapCircles.value = [
{
latitude: res.latitude,
longitude: res.longitude,
radius: 1000,
fillColor: '#1c52fa25',
color: '#256BFA',
},
];
getJobList('refresh');
});
}
function progressChange(value) {
const range = 1 + value;
pageState.search.radius = range;
mapCircles.value = [
{
latitude: latitudeVal.value,
longitude: longitudeVal.value,
radius: range * 1000,
fillColor: '#1c52fa25',
color: '#256BFA',
},
];
debounceAjax('refresh');
}
// 处理滚动事件,实现地图折叠
function handleScroll(e) {
const scrollTop = e.detail.scrollTop;
// 当滚动超过100rpx时折叠地图
isMapCollapsed.value = scrollTop > 100;
}
let debounceAjax = debounce(getJobList, 500);
function getJobList(type = 'add') {
if (type === 'add' && pageState.page < pageState.maxPage) {
pageState.page += 1;
}
if (type === 'refresh') {
pageState.page = 1;
pageState.maxPage = 2;
}
let params = {
current: pageState.page,
pageSize: pageState.pageSize,
longitude: longitudeVal.value,
latitude: latitudeVal.value,
...pageState.search,
};
$api.createRequest('/app/job/nearJob', params, 'POST').then((resData) => {
const { rows, total } = resData;
if (type === 'add') {
const str = pageState.pageSize * (pageState.page - 1);
const end = list.value.length;
const reslist = rows;
list.value.splice(str, end, ...reslist);
} else {
list.value = rows;
}
pageState.total = resData.total;
pageState.maxPage = Math.ceil(pageState.total / pageState.pageSize);
if (loadmoreRef.value && typeof loadmoreRef.value.change === 'function') {
if (rows.length < pageState.pageSize) {
loadmoreRef.value.change('noMore');
} else {
loadmoreRef.value.change('more');
}
}
});
}
function handelHostestSearch(val) {
pageState.search.order = val.value;
getJobList('refresh');
}
function handleFilterConfirm(val) {
pageState.search = {
radius: pageState.search.radius,
order: pageState.search.order,
};
for (const [key, value] of Object.entries(val)) {
pageState.search[key] = value.join(',');
}
getJobList('refresh');
}
defineExpose({ loadData, handleFilterConfirm });
</script>
<style lang="stylus" scoped>
.nearby-select{
width: 184rpx;
height: 64rpx;
background: #FFFFFF;
border-radius: 12rpx 12rpx 12rpx 12rpx;
position: absolute
right: 28rpx;
top: 28rpx
.select-view{
display: flex
align-items: center
justify-content: center
height: 100%
.view-sx{
width: 26rpx;
height: 26rpx;
}
.active{
transform: rotate(180deg)
}
}
.select-list{
border-radius: 12rpx 12rpx 12rpx 12rpx;
overflow: hidden
position: absolute
top: 76rpx
left: 0
display: flex
flex-direction: column
background: #FFFFFF;
text-align: center
.list-item{
width: 184rpx;
line-height: 68rpx;
height: 68rpx;
border-radius: 0rpx 0rpx 0rpx 0rpx;
}
}
}
.nearby-container
// 确保容器占据整个可用视口高度,包括安全区域
height: calc(100vh - var(--window-top));
display: flex;
flex-direction: column;
overflow: hidden;
.nearby-map
height: 767rpx;
background: #e8e8e8;
overflow: hidden
transition: height 0.3s ease;
&.map-collapsed {
height: 0;
overflow: hidden;
}
.nearby-scroll
// 使用flex布局让scroll-view自适应高度占据剩余空间
flex: 1;
overflow: hidden;
transition: flex 0.3s ease;
.nearby-list
.one-cards{
display: flex;
flex-direction: column;
padding: 0 20rpx 20rpx 20rpx;
background: #f4f4f4
}
// 筛选条件样式 - 顶级选择器
.nav-filter
padding: 16rpx 28rpx 0 28rpx
background: #ffffff
.filter-top
display: flex
justify-content: space-between;
.tab-scroll
flex: 1;
overflow: hidden;
margin-right: 20rpx
white-space: nowrap;
overflow: hidden;
text-overflow: clip;
-webkit-mask-image: linear-gradient(to right, black 60%, transparent);
mask-image: linear-gradient(to right, black 60%, transparent);
.jobs-left
display: flex
flex-wrap: nowrap
.job
font-weight: 400;
font-size: 36rpx;
color: #666D7F;
margin-right: 32rpx;
white-space: nowrap
.active
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
font-weight: 500;
font-size: 36rpx;
color: #000000;
.jobs-add
display: flex
align-items: center;
justify-content: center;
font-weight: 400;
font-size: 32rpx;
color: #666D7F;
line-height: 38rpx;
.filter-bottom
display: flex
justify-content: space-between
padding: 24rpx 0
.btm-left
display: flex
.filterbtm
font-weight: 400;
font-size: 32rpx;
color: #666D7F;
margin-right: 40rpx
.active
font-weight: 500;
font-size: 32rpx;
color: #256BFA;
.btm-right
font-weight: 400;
font-size: 32rpx;
color: #6C7282;
.right-sx
width: 26rpx;
height: 26rpx;
.active
transform: rotate(180deg)
</style>

View File

@@ -0,0 +1,565 @@
<template>
<view class="nearby-container">
<view class="three-head" @touchmove.stop.prevent>
<view class="one-picker">
<view class="oneleft button-click" @click="openFilterSubway">
<view class="uni-input">{{ inputText(state.subwayId) }}</view>
<view class="btm-right">
<image
class="right-sx"
:class="{ active: showFiltersubway }"
src="@/static/icon/shaixun.png"
></image>
</view>
</view>
<view class="oneright">{{ state.subwayStart.stationName }}-{{ state.subwayEnd.stationName }}</view>
</view>
<scroll-view class="scroll-head" :scroll-x="true" :show-scrollbar="false">
<view class="metro">
<view class="metro-three">
<view class="three-background">
<view class="three-items">
<view
class="three-item"
v-for="(item, index) in subwayCurrent.subwayStationList"
@click="selectSubwayStation(item, index)"
:key="index"
>
<view
class="item-dont"
:class="{
dontstart: index === 0,
dontend: index === subwayCurrent.subwayStationList.length - 1,
donted: index === state.dont,
}"
></view>
<view
class="item-text"
:class="{
textActive: index === state.dont,
}"
>
{{ item.stationName }}
</view>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
</view>
<!-- 筛选项移到scroll-view外面 -->
<view class="nav-filter" @touchmove.stop.prevent>
<view class="filter-top">
<scroll-view :scroll-x="true" :show-scrollbar="false" class="tab-scroll">
<view class="jobs-left">
<view
class="job button-click"
:class="{ active: state.tabIndex === 'all' }"
@click="choosePosition('all')"
>
全部
</view>
<view
class="job button-click"
:class="{ active: state.tabIndex === index }"
v-for="(item, index) in userInfo.jobTitle"
:key="index"
@click="choosePosition(index)"
>
{{ item }}
</view>
</view>
</scroll-view>
<view class="jobs-add button-click" @click="navTo('/packageA/pages/addPosition/addPosition')">
<uni-icons class="iconsearch" color="#666D7F" type="plusempty" size="18"></uni-icons>
<text>添加</text>
</view>
</view>
<view class="filter-bottom">
<view class="btm-left">
<view
class="button-click filterbtm"
:class="{ active: pageState.search.order === item.value }"
v-for="item in rangeOptions"
@click="handelHostestSearch(item)"
:key="item.value"
>
{{ item.text }}
</view>
</view>
<view class="btm-right button-click" @click="openFilter">
筛选
<image class="right-sx" :class="{ active: showFilter }" src="@/static/icon/shaixun.png"></image>
</view>
</view>
</view>
<scroll-view :scroll-y="true" class="nearby-scroll" @scrolltolower="scrollBottom" lower-threshold="50">
<view class="nearby-list">
<view class="one-cards">
<renderJobs
v-if="list.length"
:list="list"
:longitude="longitudeVal"
:latitude="latitudeVal"
></renderJobs>
<empty v-else pdTop="60"></empty>
<loadmore ref="loadmoreRef"></loadmore>
</view>
</view>
</scroll-view>
<!-- 筛选 -->
<select-filter ref="selectFilterModel"></select-filter>
</view>
</template>
<script setup>
import { reactive, inject, watch, ref, onMounted, onBeforeUnmount } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
const { $api, navTo, debounce, customSystem } = inject('globalFunction');
const openSelectPopup = inject('openSelectPopup');
import { storeToRefs } from 'pinia';
import useLocationStore from '@/stores/useLocationStore';
import useUserStore from '@/stores/useUserStore';
import useDictStore from '@/stores/useDictStore';
const { oneDictData } = useDictStore();
const { userInfo } = storeToRefs(useUserStore());
const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
import point2 from '@/static/icon/point2.png';
import LocationPng from '@/static/icon/Location.png';
import selectFilter from '@/components/selectFilter/selectFilter.vue';
const emit = defineEmits(['onFilter']);
// status
const showFiltersubway = ref(false);
const showFilter = ref(false);
const selectFilterModel = ref();
const subwayCurrent = ref([]);
const isLoaded = ref(false);
const range = ref([]);
const state = reactive({
subwayList: [],
subwayStart: {},
subwayEnd: {},
value: 0,
subwayId: 0,
downup: true,
dont: 0,
dontObj: {},
tabIndex: 'all',
});
const pageState = reactive({
page: 0,
total: 0,
maxPage: 2,
pageSize: 10,
search: {
order: 0,
},
});
const list = ref([]);
const loadmoreRef = ref(null);
const rangeOptions = ref([
{ value: 0, text: '推荐' },
{ value: 1, text: '最热' },
{ value: 2, text: '最新发布' },
{ value: 3, text: '疆外' },
]);
onLoad(() => {
getSubway();
});
function navToPost(jobId) {
navTo(`/packageA/pages/post/post?jobId=${encodeURIComponent(jobId)}`);
}
async function loadData() {
try {
if (isLoaded.value) return;
getJobList('refresh');
isLoaded.value = true;
} catch (err) {
isLoaded.value = false; // 重置状态允许重试
throw err;
}
}
function openFilter() {
showFilter.value = true;
selectFilterModel.value?.open({
title: '筛选',
maskClick: true,
success: (values) => {
pageState.search = {
...pageState.search,
};
for (const [key, value] of Object.entries(values)) {
pageState.search[key] = value.join(',');
}
showFilter.value = false;
getJobList('refresh');
},
cancel: () => {
showFilter.value = false;
},
});
}
function openFilterSubway() {
const diti = state.subwayList.map((item) => ({ ...item, label: item.lineName, value: item.lineId }));
openSelectPopup({
title: '地铁',
maskClick: true,
data: [diti],
success: (_, [value]) => {
subwayCurrent.value = value;
state.subwayId = value.value;
state.value = value.value;
const points = value.subwayStationList;
state.downup = true;
if (points.length) {
state.dont = 0;
state.dontObj = points[0];
state.subwayStart = points[0];
state.subwayEnd = points[points.length - 1];
getJobList('refresh');
}
},
});
}
function scrollBottom() {
getJobList();
loadmoreRef.value.change('loading');
}
function choosePosition(index) {
state.tabIndex = index;
if (index === 'all') {
pageState.search.jobTitle = '';
} else {
pageState.search.jobTitle = userInfo.value.jobTitle[index];
}
getJobList('refresh');
}
function selectSubwayStation(point, index) {
console.log(point, index);
state.dont = index;
state.dontObj = point;
getJobList('refresh');
}
function inputText(id) {
if (id) {
const text = range.value.filter((item) => item.value === id)[0].text;
return text;
} else {
return '';
}
}
function bindPickerChange(e) {
const lineId = range.value[e.detail.value];
const value = state.subwayList.filter((iv) => iv.lineId === lineId.value)[0];
subwayCurrent.value = value;
state.value = e.detail.value;
state.subwayId = value.lineId;
const points = value.subwayStationList;
state.downup = true;
if (points.length) {
state.dont = 0;
state.dontObj = points[0];
state.subwayStart = points[0];
state.subwayEnd = points[points.length - 1];
}
}
function getSubway() {
$api.createRequest(`/app/common/subway`).then((resData) => {
state.subwayList = resData.data;
subwayCurrent.value = resData.data[0];
state.subwayId = resData.data[0].lineId;
state.value = 0;
state.dont = 0;
range.value = resData.data.map((iv) => ({ text: iv.lineName, value: iv.lineId }));
const points = resData.data[0].subwayStationList;
if (points.length) {
state.dont = 0;
state.dontObj = points[0];
state.subwayStart = points[0];
state.subwayEnd = points[points.length - 1];
}
});
}
function getJobList(type = 'add') {
if (type === 'add' && pageState.page < pageState.maxPage) {
pageState.page += 1;
}
if (type === 'refresh') {
pageState.page = 1;
pageState.maxPage = 2;
}
let params = {
current: pageState.page,
pageSize: pageState.pageSize,
subwayIds: [state.dontObj.stationId],
latitude: state.dontObj.latitude,
longitude: state.dontObj.longitude,
radius: 2,
...pageState.search,
};
$api.createRequest('/app/job/subway', params, 'POST').then((resData) => {
const { rows, total } = resData;
if (type === 'add') {
const str = pageState.pageSize * (pageState.page - 1);
const end = list.value.length;
const reslist = rows;
list.value.splice(str, end, ...reslist);
} else {
list.value = rows;
}
pageState.total = resData.total;
pageState.maxPage = Math.ceil(pageState.total / pageState.pageSize);
if (loadmoreRef.value && typeof loadmoreRef.value.change === 'function') {
if (rows.length < pageState.pageSize) {
loadmoreRef.value.change('noMore');
} else {
loadmoreRef.value.change('more');
}
}
});
}
function handelHostestSearch(val) {
pageState.search.order = val.value;
getJobList('refresh');
}
function handleFilterConfirm(val) {
pageState.search = {
order: pageState.search.order,
};
for (const [key, value] of Object.entries(val)) {
pageState.search[key] = value.join(',');
}
getJobList('refresh');
}
defineExpose({ loadData, handleFilterConfirm });
</script>
<style lang="stylus" scoped>
.btm-right
font-weight: 400;
font-size: 32rpx;
color: #6C7282;
.right-sx
width: 26rpx;
height: 26rpx;
.active
transform: rotate(180deg)
.tabchecked
color: #4778EC !important;
.nearby-container
// 确保容器占据整个可用高度
height: 100%;
display: flex;
flex-direction: column;
.nearby-scroll
// 为scroll-view设置明确高度减去nav-filter的高度
height: calc(100vh - var(--window-top) - var(--status-bar-height) - 63rpx - 200rpx - 120rpx);
.three-head
margin: 24rpx 0 0 0;
padding: 26rpx 0 0 0;
border-radius: 17rpx 17rpx 17rpx 17rpx;
.one-picker
height: 100%
padding: 0 28rpx
display: flex;
flex-wrap: nowrap;
align-items: center;
justify-content: space-between
.oneleft
display: flex;
flex-wrap: nowrap;
.oneright
display: flex;
flex-wrap: nowrap;
.scroll-head
width: 100%;
overflow: hidden;
.metro
width: 100%;
.metro-one
font-size: 28rpx;
color: #000000;
line-height: 33rpx;
width: 100%;
min-width: 100rpx;
.metro-two
font-size: 21rpx;
color: #606060;
line-height: 25rpx;
margin-top: 6rpx;
.metro-three
width: fit-content;
margin-top: 100rpx;
padding: 0 64rpx;
.three-background
position: relative;
.three-items
position: relative;
top: -17rpx;
display: flex;
justify-content: space-between;
z-index: 2;
height: 90rpx
.three-item
margin-right: 124rpx;
position: relative
.item-dont
width: 17rpx;
height: 17rpx;
background: #FFFFFF;
border-radius: 50%;
position: relative;
margin-bottom: 20rpx;
.item-dont::before
position: absolute;
content: '';
color: #FFFFFF;
font-size: 20rpx;
text-align: center;
left: 50%;
top: 50%;
transform: translate(-50%, -50%)
width: 27rpx;
height: 27rpx;
background: #F7B000;
border-radius: 50%;
.item-dont::after
position: absolute;
// content: '始';
content: '';
font-size: 20rpx;
text-align: center;
left: 50%;
top: 50%;
transform: translate(-50%, -50%)
width: 14rpx;
height: 14rpx;
background: #ffffff;
border-radius: 50%;
// .donted::after
// position: absolute;
// content: '';
// font-size: 20rpx;
// text-align: center;
// left: 50%;
// top: 50%;
// transform: translate(-50%, -50%)
// width: 14rpx;
// height: 14rpx;
// background: #F7B000 !important;
// border-radius: 50%;
.item-text
position: absolute
left: 0
width: fit-content;
font-weight: 400;
font-size: 28rpx;
color: #333333;
line-height: 25rpx;
text-align: center;
white-space: nowrap
transform: translate(-50% + 8rpx, 0)
.textActive
color: #F7B000
.three-item:nth-child(2n)
.item-text
margin-top: -90rpx;
.three-background::after
position: absolute;
content: '';
left: 0;
top: -17rpx;
width: 100%;
height: 17rpx;
background: #F7B000;
border-radius: 17rpx 17rpx 17rpx 17rpx;
z-index: 1;
.nearby-list
.one-cards{
display: flex;
flex-direction: column;
padding: 0 20rpx 20rpx 20rpx;
background: #f4f4f4
}
.nav-filter
border-top: 2rpx solid #EBEBEB;
padding: 16rpx 28rpx 0 28rpx;
margin-top: 125rpx;
.filter-top
display: flex
justify-content: space-between;
.tab-scroll
flex: 1;
overflow: hidden;
margin-right: 20rpx
white-space: nowrap;
overflow: hidden;
text-overflow: clip;
-webkit-mask-image: linear-gradient(to right, black 60%, transparent);
mask-image: linear-gradient(to right, black 60%, transparent);
.jobs-left
display: flex
flex-wrap: nowrap
.job
font-weight: 400;
font-size: 36rpx;
color: #666D7F;
margin-right: 32rpx;
white-space: nowrap
.active
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
font-weight: 500;
font-size: 36rpx;
color: #000000;
.jobs-add
display: flex
align-items: center;
justify-content: center;
font-weight: 400;
font-size: 32rpx;
color: #666D7F;
line-height: 38rpx;
.filter-bottom
display: flex
justify-content: space-between
padding: 24rpx 0
.btm-left
display: flex
.filterbtm
font-weight: 400;
font-size: 32rpx;
color: #666D7F;
margin-right: 40rpx
.active
font-weight: 500;
font-size: 32rpx;
color: #256BFA;
.btm-right
font-weight: 400;
font-size: 32rpx;
color: #6C7282;
.right-sx
width: 26rpx;
height: 26rpx;
.active
transform: rotate(180deg)
</style>

View File

@@ -0,0 +1,400 @@
<template>
<view class="nearby-container">
<!-- 区县选择区域 - 可折叠 -->
<view
class="two-head"
:class="{ 'area-collapsed': isAreaCollapsed }"
>
<view
class="head-item"
:class="{ active: item.value === fromValue.area }"
v-for="(item, index) in oneDictData('area')"
:key="item.value"
@click="changeArea(item.value, item)"
>
{{ item.label }}
</view>
<view
class="head-item"
:class="{ active: state.tabBxText === fromValue.area }"
@click="changeArea(state.tabBxText, item)"
>
不限区域
</view>
</view>
<!-- 筛选条件 - 固定显示 -->
<view class="nav-filter" @touchmove.stop.prevent>
<view class="filter-top">
<scroll-view :scroll-x="true" :show-scrollbar="false" class="tab-scroll">
<view class="jobs-left">
<view
class="job button-click"
:class="{ active: state.tabIndex === 'all' }"
@click="choosePosition('all')"
>
全部
</view>
<view
class="job button-click"
:class="{ active: state.tabIndex === index }"
v-for="(item, index) in userInfo.jobTitle"
:key="index"
@click="choosePosition(index)"
>
{{ item }}
</view>
</view>
</scroll-view>
<view class="jobs-add button-click" @click="navTo('/packageA/pages/addPosition/addPosition')">
<uni-icons class="iconsearch" color="#666D7F" type="plusempty" size="18"></uni-icons>
<text>添加</text>
</view>
</view>
<view class="filter-bottom">
<view class="btm-left">
<view
class="button-click filterbtm"
:class="{ active: pageState.search.order === item.value }"
v-for="item in rangeOptions"
@click="handelHostestSearch(item)"
:key="item.value"
>
{{ item.text }}
</view>
</view>
<view class="btm-right button-click" @click="openFilter">
筛选
<image class="right-sx" :class="{ active: showFilter }" src="@/static/icon/shaixun.png"></image>
</view>
</view>
</view>
<!-- 列表滚动区域 -->
<scroll-view
:scroll-y="true"
class="nearby-scroll"
@scrolltolower="scrollBottom"
lower-threshold="50"
ref="scrollViewRef"
@scroll="handleScroll"
>
<view class="nearby-list">
<view class="one-cards">
<renderJobs v-if="list.length" :list="list"></renderJobs>
<empty v-else pdTop="60"></empty>
<loadmore ref="loadmoreRef"></loadmore>
</view>
</view>
</scroll-view>
<!-- 筛选 -->
<select-filter ref="selectFilterModel"></select-filter>
</view>
</template>
<script setup>
import { reactive, inject, watch, ref, onMounted, onBeforeUnmount } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
const { $api, navTo, debounce, customSystem } = inject('globalFunction');
import { storeToRefs } from 'pinia';
import useLocationStore from '@/stores/useLocationStore';
import useUserStore from '@/stores/useUserStore';
import useDictStore from '@/stores/useDictStore';
const { oneDictData } = useDictStore();
const { userInfo } = storeToRefs(useUserStore());
const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
import point2 from '@/static/icon/point2.png';
import LocationPng from '@/static/icon/Location.png';
import selectFilter from '@/components/selectFilter/selectFilter.vue';
const emit = defineEmits(['onFilter']);
const state = reactive({
tabIndex: 'all',
tabBxText: 'buxianquyu',
});
const isLoaded = ref(false);
const showFilter = ref(false);
const selectFilterModel = ref();
const scrollViewRef = ref(null);
const isAreaCollapsed = ref(false);
const fromValue = reactive({
area: 0,
});
const loadmoreRef = ref(null);
const pageState = reactive({
page: 0,
total: 0,
maxPage: 2,
pageSize: 10,
search: {
order: 0,
},
});
const list = ref([]);
const rangeOptions = ref([
{ value: 0, text: '推荐' },
{ value: 1, text: '最热' },
{ value: 2, text: '最新发布' },
{ value: 3, text: '疆外' },
]);
function choosePosition(index) {
state.tabIndex = index;
list.value = [];
if (index === 'all') {
pageState.search = {
...pageState.search,
jobTitle: '',
};
getJobList('refresh');
} else {
// const id = useUserStore().userInfo.jobTitleId.split(',')[index];
pageState.search.jobTitle = userInfo.value.jobTitle[index];
getJobList('refresh');
}
}
function openFilter() {
showFilter.value = true;
selectFilterModel.value?.open({
title: '筛选',
maskClick: true,
success: (values) => {
pageState.search = {
...pageState.search,
};
for (const [key, value] of Object.entries(values)) {
pageState.search[key] = value.join(',');
}
showFilter.value = false;
console.log(pageState.search);
getJobList('refresh');
},
cancel: () => {
showFilter.value = false;
},
});
}
async function loadData() {
try {
if (isLoaded.value) return;
const area = oneDictData('area')[0];
fromValue.area = area.value;
getJobList('refresh');
isLoaded.value = true;
} catch (err) {
isLoaded.value = false; // 重置状态允许重试
throw err;
}
}
function scrollBottom() {
getJobList();
loadmoreRef.value.change('loading');
}
// function choosePosition(index) {
// state.tabIndex = index;
// if (index === 'all') {
// pageState.search.jobTitle = '';
// } else {
// pageState.search.jobTitle = userInfo.jobTitle[index];
// }
// getJobList('refresh');
// }
function changeArea(area, item) {
fromValue.area = area;
// 切换区县时列表置顶
if (scrollViewRef.value) {
scrollViewRef.value.scrollTop = 0;
}
getJobList('refresh');
}
// 处理滚动事件,实现区县选择区域折叠
function handleScroll(e) {
const scrollTop = e.detail.scrollTop;
// 当滚动超过100rpx时折叠区县选择区域
isAreaCollapsed.value = scrollTop > 100;
console.log('scrollTop:', scrollTop, 'collapsed:', isAreaCollapsed.value);
}
function getJobList(type = 'add') {
if (type === 'add' && pageState.page < pageState.maxPage) {
pageState.page += 1;
}
if (type === 'refresh') {
pageState.page = 1;
pageState.maxPage = 2;
}
let params = {
current: pageState.page,
pageSize: pageState.pageSize,
countyIds: [fromValue.area],
...pageState.search,
};
if (fromValue.area === state.tabBxText) {
params.countyIds = [];
}
$api.createRequest('/app/job/countyJob', params, 'POST').then((resData) => {
const { rows, total } = resData;
if (type === 'add') {
const str = pageState.pageSize * (pageState.page - 1);
const end = list.value.length;
const reslist = rows;
list.value.splice(str, end, ...reslist);
} else {
list.value = rows;
}
pageState.total = resData.total;
pageState.maxPage = Math.ceil(pageState.total / pageState.pageSize);
if (loadmoreRef.value && typeof loadmoreRef.value.change === 'function') {
if (rows.length < pageState.pageSize) {
loadmoreRef.value.change('noMore');
} else {
loadmoreRef.value.change('more');
}
}
});
}
function handelHostestSearch(val) {
pageState.search.order = val.value;
getJobList('refresh');
}
function handleFilterConfirm(val) {
pageState.search = {
order: pageState.search.order,
};
for (const [key, value] of Object.entries(val)) {
pageState.search[key] = value.join(',');
}
getJobList('refresh');
}
defineExpose({ loadData, handleFilterConfirm });
</script>
<style lang="stylus" scoped>
.tabchecked
color: #4778EC !important
.nearby-container
// 确保容器占据整个可用视口高度,包括安全区域
height: calc(100vh - var(--window-top));
display: flex;
flex-direction: column;
overflow: hidden;
.nearby-scroll
// 使用flex布局让scroll-view自适应高度占据剩余空间
flex: 1;
overflow: hidden;
.two-head
margin: 22rpx;
display: flex;
flex-wrap: wrap
// grid-template-columns: repeat(4, 1fr);
transition: all 0.3s ease;
&.area-collapsed {
height: 0;
margin: 0;
padding: 0;
overflow: hidden;
}
// grid-column-gap: 10rpx;
// grid-row-gap: 24rpx;
border-radius: 17rpx 17rpx 17rpx 17rpx;
.head-item
margin: 10rpx
white-space: nowrap
min-width: 156rpx
line-height: 64rpx
text-align: center;
width: fit-content;
font-size: 21rpx;
font-weight: 400;
font-size: 28rpx;
color: #6C7282;
background: #F6F6F6;
border-radius: 12rpx 12rpx 12rpx 12rpx;
.active
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
color: #256BFA;
background: #E9F0FF;
border-radius: 12rpx 12rpx 12rpx 12rpx;
.nearby-list
border-top: 2rpx solid #EBEBEB;
.one-cards{
display: flex;
flex-direction: column;
padding: 0 20rpx 20rpx 20rpx;
background: #f4f4f4
}
// 筛选条件样式 - 顶级选择器
.nav-filter
padding: 16rpx 28rpx 0 28rpx
background: #ffffff
.filter-top
display: flex
justify-content: space-between;
.tab-scroll
flex: 1;
overflow: hidden;
margin-right: 20rpx
white-space: nowrap;
overflow: hidden;
text-overflow: clip;
-webkit-mask-image: linear-gradient(to right, black 60%, transparent);
mask-image: linear-gradient(to right, black 60%, transparent);
.jobs-left
display: flex
flex-wrap: nowrap
.job
font-weight: 400;
font-size: 36rpx;
color: #666D7F;
margin-right: 32rpx;
white-space: nowrap
.active
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
font-weight: 500;
font-size: 36rpx;
color: #000000;
.jobs-add
display: flex
align-items: center;
justify-content: center;
font-weight: 400;
font-size: 32rpx;
color: #666D7F;
line-height: 38rpx;
.filter-bottom
display: flex
justify-content: space-between
padding: 24rpx 0
.btm-left
display: flex
.filterbtm
font-weight: 400;
font-size: 32rpx;
color: #666D7F;
margin-right: 40rpx
.active
font-weight: 500;
font-size: 32rpx;
color: #256BFA;
.btm-right
font-weight: 400;
font-size: 32rpx;
color: #6C7282;
.right-sx
width: 26rpx;
height: 26rpx;
.active
transform: rotate(180deg)
</style>

View File

@@ -0,0 +1,136 @@
<template>
<AppLayout :use-scroll-view="false">
<template #headerleft>
<view class="btnback">
<image src="@/static/icon/back.png" @click="navBack"></image>
</view>
</template>
<view class="app-container">
<view class="nearby-head">
<view class="head-item" :class="{ actived: state.current === 0 }" @click="changeType(0)">附近工作</view>
<view class="head-item" :class="{ actived: state.current === 1 }" @click="changeType(1)">区县工作</view>
<view class="head-item" :class="{ actived: state.current === 2 }" @click="changeType(2)">公交周边</view>
<view class="head-item" :class="{ actived: state.current === 3 }" @click="changeType(3)">商圈附近</view>
</view>
<view class="nearby-content">
<swiper class="swiper" :current="state.current" @change="changeSwiperType">
<swiper-item class="swiper-item" v-for="(_, index) in 4" :key="index">
<!-- #ifndef MP-WEIXIN -->
<component :is="components[index]" :ref="(el) => handelComponentsRef(el, index)" />
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<oneComponent v-if="index === 0" :ref="(el) => handelComponentsRef(el, index)" />
<twoComponent v-if="index === 1" :ref="(el) => handelComponentsRef(el, index)" />
<threeComponent v-if="index === 2" :ref="(el) => handelComponentsRef(el, index)" />
<fourComponent v-if="index === 3" :ref="(el) => handelComponentsRef(el, index)" />
<!-- #endif -->
</swiper-item>
</swiper>
</view>
</view>
</AppLayout>
</template>
<script setup>
import oneComponent from './components/one.vue';
import twoComponent from './components/two.vue';
import threeComponent from './components/three.vue';
import fourComponent from './components/four.vue';
import { reactive, inject, watch, ref, onMounted } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
const { $api, debounce, throttle, navBack } = inject('globalFunction');
const loadedMap = reactive([false, false, false, false]);
const swiperRefs = [ref(null), ref(null), ref(null), ref(null)];
const components = [oneComponent, twoComponent, threeComponent, fourComponent];
const filterId = ref(0);
const showFilter = ref(false);
const showFilter1 = ref(false);
const showFilter2 = ref(false);
const showFilter3 = ref(false);
const state = reactive({
current: 0,
all: [{}],
});
onMounted(() => {
handleTabChange(state.current);
});
const handelComponentsRef = (el, index) => {
if (el) {
swiperRefs[index].value = el;
}
};
// 查看消息类型
function changeSwiperType(e) {
const index = e.detail.current;
state.current = index;
handleTabChange(index);
}
function changeType(index) {
state.current = index;
handleTabChange(index);
}
function handleTabChange(index) {
if (!loadedMap[index]) {
swiperRefs[index].value?.loadData();
loadedMap[index] = true;
}
}
</script>
<style lang="stylus" scoped>
.btnback{
width: 64rpx;
height: 64rpx;
}
.btn {
display: flex;
justify-content: space-between;
align-items: center;
width: 52rpx;
height: 52rpx;
}
image {
height: 100%;
width: 100%;
}
.app-container
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
.nearby-head
height: 63rpx;
line-height: 63rpx;
text-align: center;
display: flex;
align-items: center;
font-weight: 400;
font-size: 32rpx;
color: #666D7F;
.head-item
width: calc(100% / 4);
z-index: 9
.actived
// width: 169rpx;
// height: 63rpx;
// background: #13C57C;
// box-shadow: 0rpx 7rpx 7rpx 0rpx rgba(0,0,0,0.25);
// border-radius: 0rpx 0rpx 0rpx 0rpx;
font-weight: 500;
font-size: 32rpx;
color: #000000;
.nearby-content
flex: 1;
overflow: hidden;
.swiper
height: 100%;
.swiper-item
display: flex;
flex-direction: column;
height: 100%;
</style>

View File

@@ -472,7 +472,7 @@ function changeSkillName(index) {
// 将当前已选中的技能名称传递给查询页面
const selectedSkills = state.skills.map(skill => skill.name).filter(name => name);
uni.navigateTo({
url: `/pages/complete-info/skill-search?selected=${encodeURIComponent(JSON.stringify(selectedSkills))}`
url: `/packageA/pages/complete-info/skill-search?selected=${encodeURIComponent(JSON.stringify(selectedSkills))}`
});
}