企业信息补全页面开发

This commit is contained in:
冯辉
2025-10-21 22:58:47 +08:00
parent 968e6b4091
commit 8bb3c424e2
61 changed files with 2793375 additions and 812 deletions

View File

@@ -101,7 +101,7 @@ onLoad(() => {
onShow(() => {
nextTick(() => {
paging.value?.colseFile();
paging.value?.closeFile();
});
});

View File

@@ -255,6 +255,7 @@ import {
reactive,
computed,
watch,
getCurrentInstance,
} from 'vue';
import { storeToRefs } from 'pinia';
// import config from '@/config.js';
@@ -287,6 +288,9 @@ const {
const { speak, pause, resume, isSpeaking, isPaused, cancelAudio } = useTTSPlayer(config.speechSynthesis);
// 获取组件实例(用于小程序 SelectorQuery
const instance = getCurrentInstance();
// state
const queries = ref([]);
const guessList = ref([]);
@@ -336,18 +340,46 @@ onMounted(async () => {
});
const requestMicPermission = async () => {
// #ifdef H5
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
console.log('✅ 麦克风权限已授权');
if (typeof navigator !== 'undefined' && navigator.mediaDevices) {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
console.log('✅ 麦克风权限已授权');
// 立刻停止所有音轨,释放麦克风
stream.getTracks().forEach((track) => track.stop());
// 立刻停止所有音轨,释放麦克风
stream.getTracks().forEach((track) => track.stop());
return true;
return true;
} else {
console.warn('❌ 当前环境不支持麦克风');
return false;
}
} catch (err) {
console.warn('❌ 用户拒绝麦克风权限或不支持:', err);
return false;
}
// #endif
// #ifdef MP-WEIXIN
try {
// 微信小程序使用 uni.authorize 请求权限
const res = await uni.authorize({
scope: 'scope.record'
});
console.log('✅ 麦克风权限已授权');
return true;
} catch (err) {
console.warn('❌ 用户拒绝麦克风权限:', err);
// 用户拒绝授权,但不影响其他功能
return false;
}
// #endif
// #ifndef H5 || MP-WEIXIN
// 其他平台暂不支持
console.warn('❌ 当前平台不支持麦克风权限检测');
return false;
// #endif
};
function showControll(index) {
@@ -473,10 +505,20 @@ const delfile = (file) => {
const scrollToBottom = throttle(function () {
nextTick(() => {
try {
// #ifdef MP-WEIXIN
const query = uni.createSelectorQuery().in(instance);
// #endif
// #ifndef MP-WEIXIN
const query = uni.createSelectorQuery();
// #endif
query.select('.scrollView').boundingClientRect();
query.select('.list-content').boundingClientRect();
query.exec((res) => {
if (!res || !res[0] || !res[1]) {
console.warn('scrollToBottom: 元素未找到或尚未渲染');
return;
}
const scrollViewHeight = res[0].height;
const scrollContentHeight = res[1].height;
if (scrollContentHeight > scrollViewHeight) {
@@ -645,7 +687,7 @@ function changeShowFile() {
showfile.value = !showfile.value;
}
function colseFile() {
function closeFile() {
showfile.value = false;
}
@@ -771,7 +813,7 @@ function getRandomJobQueries(queries, count = 2) {
return shuffled.slice(0, count); // 取前 count 条
}
defineExpose({ scrollToBottom, closeGuess, colseFile, changeQueries, handleTouchCancel });
defineExpose({ scrollToBottom, closeGuess, closeFile, changeQueries, handleTouchCancel });
</script>
<style lang="stylus" scoped>

View File

@@ -0,0 +1,632 @@
<template>
<AppLayout title="企业信息">
<view class="company-info-container">
<!-- 头部信息 -->
<view class="header-info">
<view class="progress-text">企业信息完善度</view>
<view class="progress-value">{{ completionPercentage }}%</view>
</view>
<!-- 表单内容 -->
<view class="form-content">
<!-- 企业名称 -->
<view class="form-item">
<view class="label">企业名称</view>
<input
class="input-field"
v-model="formData.companyName"
placeholder="请输入企业名称"
@input="updateCompletion"
/>
</view>
<!-- 统一社会信用代码 -->
<view class="form-item">
<view class="label">统一社会信用代码</view>
<input
class="input-field"
v-model="formData.socialCreditCode"
placeholder="请输入统一社会信用代码"
maxlength="18"
@input="updateCompletion"
/>
</view>
<!-- 企业注册地点 -->
<view class="form-item clickable" @click="selectLocation">
<view class="label">企业注册地点</view>
<view class="input-content">
<text class="input-text" :class="{ placeholder: !formData.registeredAddress }">
{{ formData.registeredAddress || '请选择注册地点' }}
</text>
<uni-icons type="arrowright" size="16" color="#999"></uni-icons>
</view>
</view>
<!-- 企业信息介绍 -->
<view class="form-item clickable" @click="editCompanyIntro">
<view class="label">企业信息介绍</view>
<view class="input-content">
<text class="input-text intro-text" :class="{ placeholder: !formData.companyIntro }">
{{ formData.companyIntro || '请输入企业介绍' }}
</text>
<uni-icons type="arrowright" size="16" color="#999"></uni-icons>
</view>
</view>
<!-- 企业法人姓名 -->
<view class="form-item">
<view class="label">企业法人姓名</view>
<input
class="input-field"
v-model="formData.legalPersonName"
placeholder="请输入法人姓名"
@input="updateCompletion"
/>
</view>
<!-- 本地重点发展产业 -->
<view class="form-item clickable" @click="selectIndustry">
<view class="label">本地重点发展产业</view>
<view class="input-content">
<text class="input-text" :class="{ placeholder: !formData.industryType }">
{{ formData.industryType || '请选择产业类型' }}
</text>
<uni-icons type="arrowright" size="16" color="#999"></uni-icons>
</view>
</view>
<!-- 是否是本地企业 -->
<view class="form-item clickable" @click="selectLocalCompany">
<view class="label">是否是本地企业</view>
<view class="input-content">
<text class="input-text" :class="{ placeholder: formData.isLocalCompany === null }">
{{ formData.isLocalCompany === null ? '请选择' : (formData.isLocalCompany ? '是' : '否') }}
</text>
<uni-icons type="arrowright" size="16" color="#999"></uni-icons>
</view>
</view>
<!-- 联系人信息列表 -->
<view class="contact-section">
<view class="section-title">联系人信息</view>
<!-- 每个联系人作为一个分组 -->
<view
class="contact-group"
v-for="(contact, index) in formData.contacts"
:key="index"
>
<view class="group-header">联系人{{ index + 1 }}</view>
<view class="form-item">
<view class="label">联系人姓名</view>
<input
class="input-field"
v-model="contact.name"
placeholder="请输入联系人姓名"
@input="updateCompletion"
/>
</view>
<view class="form-item">
<view class="label">联系人电话</view>
<input
class="input-field"
v-model="contact.phone"
placeholder="请输入联系人电话"
@input="updateCompletion"
/>
</view>
</view>
</view>
<!-- 添加联系人按钮 -->
<view class="add-contact-btn" @click="addContact" v-if="formData.contacts.length < 3">
<uni-icons type="plus" size="20" color="#256BFA"></uni-icons>
<text>添加联系人</text>
</view>
</view>
<!-- 底部按钮 -->
<view class="bottom-actions">
<button class="cancel-btn" @click="cancel">取消</button>
<button class="confirm-btn" @click="confirm">确认</button>
</view>
</view>
<!-- 弹窗组件 -->
<uni-popup ref="popup" type="center">
<view class="popup-content">
<view class="popup-title">{{ popupTitle }}</view>
<input
v-if="popupType === 'text'"
class="popup-input"
v-model="popupValue"
:placeholder="popupPlaceholder"
/>
<textarea
v-if="popupType === 'textarea'"
class="popup-textarea"
v-model="popupValue"
:placeholder="popupPlaceholder"
maxlength="500"
></textarea>
<view class="popup-actions">
<button class="popup-cancel" @click="closePopup">取消</button>
<button class="popup-confirm" @click="confirmPopup">确定</button>
</view>
</view>
</uni-popup>
<!-- 地址选择器 -->
<area-cascade-picker ref="areaPicker"></area-cascade-picker>
</AppLayout>
</template>
<script setup>
import { ref, reactive, computed, inject } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import AreaCascadePicker from '@/components/area-cascade-picker/area-cascade-picker.vue'
const { $api } = inject('globalFunction')
// 表单数据
const formData = reactive({
companyName: '',
socialCreditCode: '',
registeredAddress: '',
registeredAddressName: '',
longitude: null,
latitude: null,
companyIntro: '',
legalPersonName: '',
industryType: '', // 是否是本地重点发展产业
isLocalCompany: null, // 是否是本地企业 (true/false/null)
contacts: [
{ name: '', phone: '' }
]
})
// 弹窗相关
const popup = ref(null)
const popupTitle = ref('')
const popupType = ref('text') // text | textarea
const popupValue = ref('')
const popupPlaceholder = ref('')
const currentEditField = ref('')
// 地址选择器引用
const areaPicker = ref(null)
// 产业类型选项数据
const industryOptions = [
'人工智能',
'生物医药',
'新能源',
'高端装备制造',
'其他'
]
// 完成度计算
const completionPercentage = computed(() => {
const fields = [
formData.companyName,
formData.socialCreditCode,
formData.registeredAddress,
formData.companyIntro,
formData.legalPersonName,
formData.industryType,
formData.isLocalCompany !== null ? 'filled' : ''
]
// 检查联系人信息
const hasContact = formData.contacts.some(contact => contact.name && contact.phone)
const filledFields = fields.filter(field => field && field.trim()).length + (hasContact ? 1 : 0)
const totalFields = fields.length + 1
return Math.round((filledFields / totalFields) * 100)
})
// 更新完成度
const updateCompletion = () => {
// 完成度会自动通过computed更新
}
// 选择注册地点
const selectLocation = () => {
// 打开五级联动地址选择器
areaPicker.value?.open({
title: '选择企业注册地点',
maskClick: true,
success: (addressData) => {
// addressData 包含: address, province, city, district, street, community
formData.registeredAddress = addressData.address
formData.registeredAddressName = addressData.address
// 可以保存详细的地址信息
formData.provinceCode = addressData.province?.code
formData.provinceName = addressData.province?.name
formData.cityCode = addressData.city?.code
formData.cityName = addressData.city?.name
formData.districtCode = addressData.district?.code
formData.districtName = addressData.district?.name
formData.streetCode = addressData.street?.code
formData.streetName = addressData.street?.name
formData.communityCode = addressData.community?.code
formData.communityName = addressData.community?.name
updateCompletion()
$api.msg('地址选择成功')
}
})
}
// 处理地图选择返回的地址
const handleLocationSelected = (locationData) => {
formData.registeredAddress = locationData.address
formData.registeredAddressName = locationData.name
formData.longitude = locationData.longitude
formData.latitude = locationData.latitude
updateCompletion()
}
// 编辑企业介绍
const editCompanyIntro = () => {
currentEditField.value = 'companyIntro'
popupTitle.value = '企业信息介绍'
popupType.value = 'textarea'
popupValue.value = formData.companyIntro
popupPlaceholder.value = '请输入企业介绍'
popup.value?.open()
}
// 选择产业类型
const selectIndustry = () => {
uni.showActionSheet({
itemList: industryOptions,
success: (res) => {
formData.industryType = industryOptions[res.tapIndex]
updateCompletion()
$api.msg('产业类型选择成功')
}
})
}
// 选择是否是本地企业
const selectLocalCompany = () => {
uni.showActionSheet({
itemList: ['是', '否'],
success: (res) => {
formData.isLocalCompany = res.tapIndex === 0
updateCompletion()
$api.msg('选择成功')
}
})
}
// 添加联系人
const addContact = () => {
if (formData.contacts.length < 3) {
formData.contacts.push({ name: '', phone: '' })
}
}
// 关闭弹窗
const closePopup = () => {
popup.value?.close()
popupValue.value = ''
}
// 确认弹窗
const confirmPopup = () => {
const field = currentEditField.value
if (field === 'companyIntro') {
formData.companyIntro = popupValue.value
}
updateCompletion()
closePopup()
}
// 取消
const cancel = () => {
uni.navigateBack()
}
// 确认提交
const confirm = () => {
// 验证必填字段
if (!formData.companyName.trim()) {
$api.msg('请输入企业名称')
return
}
if (!formData.socialCreditCode.trim()) {
$api.msg('请输入统一社会信用代码')
return
}
if (!formData.registeredAddress.trim()) {
$api.msg('请选择注册地点')
return
}
if (!formData.companyIntro.trim()) {
$api.msg('请输入企业介绍')
return
}
if (!formData.legalPersonName.trim()) {
$api.msg('请输入法人姓名')
return
}
if (!formData.industryType.trim()) {
$api.msg('请选择产业类型')
return
}
if (formData.isLocalCompany === null) {
$api.msg('请选择是否是本地企业')
return
}
// 验证至少有一个联系人
const hasValidContact = formData.contacts.some(contact =>
contact.name.trim() && contact.phone.trim()
)
if (!hasValidContact) {
$api.msg('请至少添加一个联系人信息')
return
}
// 验证联系人电话格式
const phoneRegex = /^1[3-9]\d{9}$/
for (let contact of formData.contacts) {
if (contact.name.trim() && contact.phone.trim()) {
if (!phoneRegex.test(contact.phone)) {
$api.msg('请输入正确的手机号码')
return
}
}
}
// 提交数据
uni.showLoading({ title: '保存中...' })
// 这里调用后端接口保存企业信息
const submitData = {
...formData,
contacts: formData.contacts.filter(contact => contact.name.trim() && contact.phone.trim())
}
$api.createRequest('/app/company/complete-info', submitData, 'post')
.then((resData) => {
uni.hideLoading()
$api.msg('企业信息保存成功')
// 跳转到首页或企业相关页面
uni.reLaunch({
url: '/pages/index/index'
})
})
.catch((err) => {
uni.hideLoading()
$api.msg(err.msg || '保存失败,请重试')
})
}
onLoad((options) => {
// 可以在这里加载已有的企业信息
console.log('企业信息补全页面加载')
})
// 暴露方法给其他页面调用
defineExpose({
handleLocationSelected
})
</script>
<style lang="stylus" scoped>
.company-info-container
min-height: 100vh
background: #f5f5f5
padding-bottom: 120rpx
.header-info
background: #fff
padding: 40rpx 32rpx
display: flex
justify-content: space-between
align-items: center
margin-bottom: 20rpx
.progress-text
font-size: 32rpx
color: #000000
font-weight: 500
.progress-value
font-size: 32rpx
color: #256BFA
font-weight: 600
.form-content
background: #fff
margin-bottom: 20rpx
.form-item
padding: 32rpx
border-bottom: 1rpx solid #f0f0f0
display: flex
justify-content: space-between
align-items: center
&:last-child
border-bottom: none
&.clickable
cursor: pointer
.label
font-size: 28rpx
color: #000000
min-width: 200rpx
.input-field
flex: 1
font-size: 28rpx
color: #333
text-align: right
&::placeholder
color: #999
font-size: 26rpx
.input-content
flex: 1
display: flex
justify-content: space-between
align-items: center
.input-text
font-size: 28rpx
color: #333
flex: 1
text-align: right
&.placeholder
color: #999
font-size: 26rpx
&.intro-text
max-height: 80rpx
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
.contact-section
margin-top: 20rpx
.section-title
padding: 32rpx
font-size: 32rpx
color: #333
font-weight: 500
background: #fff
border-bottom: 1rpx solid #f0f0f0
.contact-group
background: #fff
margin-bottom: 20rpx
border-radius: 16rpx
overflow: hidden
.group-header
padding: 24rpx 32rpx
font-size: 28rpx
color: #000000
background: #f8f9fa
font-weight: 500
border-bottom: 1rpx solid #e8e8e8
.form-item
border-bottom: 1rpx solid #f0f0f0
&:last-child
border-bottom: none
.add-contact-btn
padding: 32rpx
display: flex
align-items: center
justify-content: center
color: #256BFA
font-size: 28rpx
background: #fff
border-top: 1rpx solid #f0f0f0
text
margin-left: 12rpx
.bottom-actions
position: fixed
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)
.cancel-btn, .confirm-btn
flex: 1
height: 80rpx
border-radius: 40rpx
font-size: 32rpx
border: none
.cancel-btn
background: #f5f5f5
color: #666
.confirm-btn
background: #256BFA
color: #fff
// 弹窗样式
.popup-content
width: 600rpx
background: #fff
border-radius: 20rpx
padding: 40rpx
.popup-title
font-size: 36rpx
color: #333
font-weight: 500
margin-bottom: 30rpx
text-align: center
.popup-input, .popup-textarea
width: 100%
padding: 20rpx
border: 1rpx solid #e0e0e0
border-radius: 10rpx
font-size: 28rpx
margin-bottom: 30rpx
box-sizing: border-box
text-align: center
.popup-textarea
height: 200rpx
.popup-actions
display: flex
gap: 20rpx
.popup-cancel, .popup-confirm
flex: 1
height: 70rpx
border-radius: 35rpx
font-size: 28rpx
border: none
.popup-cancel
background: #f5f5f5
color: #666
.popup-confirm
background: #256BFA
color: #fff
// 按钮重置样式
button::after
border: none
</style>

View File

@@ -1,17 +1,26 @@
<template>
<AppLayout title="AI+就业服务程序">
<tabcontrolVue :current="tabCurrent">
<template v-slot:tab0>
<view class="login-content">
<image class="logo" src="@/static/logo.png"></image>
<view class="logo-title">就业</view>
</view>
<view class="btns">
<button class="wxlogin" @click="loginTest">内测登录</button>
<view class="wxaddress">{{ config.appInfo.areaName }}公共就业和人才服务中心</view>
</view>
</template>
<template v-slot:tab1>
<view class="tab-container">
<view class="uni-margin-wrap">
<swiper
class="swiper"
:current="tabCurrent"
:circular="false"
:indicator-dots="false"
:autoplay="false"
:duration="500"
>
<swiper-item @touchmove.stop="false">
<view class="login-content">
<image class="logo" src="@/static/logo.png"></image>
<view class="logo-title">就业</view>
</view>
<view class="btns">
<button class="wxlogin" @click="loginTest">内测登录</button>
<view class="wxaddress">{{ config.appInfo.areaName }}公共就业和人才服务中心</view>
</view>
</swiper-item>
<swiper-item @touchmove.stop="false">
<view class="content-one">
<view>
<view class="content-title">
@@ -69,10 +78,10 @@
<view v-if="fromValue.idcard && !idCardError" class="success-message"> 身份证格式正确</view>
</view>
</view>
<view class="next-btn" @tap="nextStep">下一步</view>
</view>
</template>
<template v-slot:tab2>
<view class="next-btn" @tap="nextStep">下一步</view>
</view>
</swiper-item>
<swiper-item @touchmove.stop="false">
<view class="content-one">
<view>
<view class="content-title">
@@ -116,17 +125,20 @@
/>
</view>
</view>
<view class="next-btn" @tap="complete">开启求职之旅</view>
</view>
</template>
</tabcontrolVue>
<view class="next-btn" @tap="complete">开启求职之旅</view>
</view>
</swiper-item>
</swiper>
</view>
</view>
<SelectJobs ref="selectJobsModel"></SelectJobs>
<SelectPopup ref="selectPopupRef"></SelectPopup>
</AppLayout>
</template>
<script setup>
import tabcontrolVue from './components/tabcontrol.vue';
import SelectJobs from '@/components/selectJobs/selectJobs.vue';
import SelectPopup from '@/components/selectPopup/selectPopup.vue';
import { reactive, inject, watch, ref, onMounted } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import useUserStore from '@/stores/useUserStore';
@@ -134,10 +146,29 @@ import useDictStore from '@/stores/useDictStore';
const { $api, navTo, config, IdCardValidator } = inject('globalFunction');
const { loginSetToken, getUserResume } = useUserStore();
const { getDictSelectOption, oneDictData } = useDictStore();
const openSelectPopup = inject('openSelectPopup');
// console.log(config.appInfo.areaName);
// #ifdef H5
const injectedOpenSelectPopup = inject('openSelectPopup', null);
// #endif
// status
const selectJobsModel = ref();
const selectPopupRef = ref();
// openSelectPopup
const openSelectPopup = (config) => {
// #ifdef MP-WEIXIN
if (selectPopupRef.value) {
selectPopupRef.value.open(config);
}
// #endif
// #ifdef H5
if (injectedOpenSelectPopup) {
injectedOpenSelectPopup(config);
}
// #endif
};
const tabCurrent = ref(1);
const salay = [2, 5, 10, 15, 20, 25, 30, 50, 80, 100];
const state = reactive({
@@ -348,6 +379,23 @@ function complete() {
</script>
<style lang="stylus" scoped>
.tab-container
height: 100%
width: 100%
display: flex
align-items: center
justify-content: center
flex-direction: row
.uni-margin-wrap
width: 100%
height: 100%
.swiper
width: 100%
height: 100%
.swiper-item
display: block;
width: 100%
height: 100%
.input-nx
position: relative
border-bottom: 2rpx solid #EBEBEB
@@ -545,3 +593,4 @@ function complete() {
text-align: center;
line-height: 90rpx
</style>

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

@@ -1,75 +0,0 @@
<template>
<view class="tab-container">
<view class="uni-margin-wrap">
<swiper
class="swiper"
:current="current"
:circular="false"
:indicator-dots="false"
:autoplay="false"
:duration="500"
>
<swiper-item @touchmove.stop="false">
<slot name="tab0"></slot>
</swiper-item>
<swiper-item @touchmove.stop="false">
<slot name="tab1"></slot>
</swiper-item>
<swiper-item @touchmove.stop="false">
<slot name="tab2"></slot>
</swiper-item>
<swiper-item @touchmove.stop="false">
<slot name="tab3"></slot>
</swiper-item>
<swiper-item @touchmove.stop="false">
<slot name="tab3"></slot>
</swiper-item>
<swiper-item @touchmove.stop="false">
<slot name="tab4"></slot>
</swiper-item>
<swiper-item @touchmove.stop="false">
<slot name="tab5"></slot>
</swiper-item>
<swiper-item @touchmove.stop="false">
<slot name="tab6"></slot>
</swiper-item>
</swiper>
</view>
</view>
</template>
<script>
export default {
name: 'tab',
data() {
return {};
},
props: {
current: {
type: Number,
default: 0,
},
},
};
</script>
<style lang="stylus" scoped>
.tab-container
// flex: 1
height: 100%
width: 100%
display: flex
align-items: center
justify-content: center
flex-direction: row
.uni-margin-wrap
width: 100%
height: 100%
.swiper
width: 100%
height: 100%
.swiper-item
display: block;
width: 100%
height: 100%
</style>