合并 智慧就业第一版

This commit is contained in:
2025-10-30 11:29:57 +08:00
parent 577b20661a
commit 6579abe021
166 changed files with 2818496 additions and 367 deletions

View File

@@ -83,7 +83,7 @@
<empty v-else pdTop="200"></empty>
</scroll-view>
</view>
<Tabbar :currentpage="1"></Tabbar>
<!-- 统一使用系统tabBar -->
</view>
</view>
</template>
@@ -91,7 +91,6 @@
<script setup>
import { reactive, inject, watch, ref, onMounted } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import Tabbar from '@/components/tabbar/midell-box.vue';
import useLocationStore from '@/stores/useLocationStore';
import { storeToRefs } from 'pinia';
const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());

View File

@@ -64,7 +64,7 @@
</view>
<!-- 自定义tabbar -->
<view class="chatmain-footer" v-show="!isDrawerOpen">
<Tabbar :currentpage="2"></Tabbar>
<!-- 统一使用系统tabBar -->
</view>
</view>
</view>
@@ -74,7 +74,6 @@
import { ref, inject, nextTick, computed } from 'vue';
const { $api, navTo, insertSortData, config } = inject('globalFunction');
import { onLoad, onShow, onHide } from '@dcloudio/uni-app';
import Tabbar from '@/components/tabbar/midell-box.vue';
import useChatGroupDBStore from '@/stores/userChatGroupStore';
import useUserStore from '@/stores/useUserStore';
import aiPaging from './components/ai-paging.vue';
@@ -102,7 +101,7 @@ onLoad(() => {
onShow(() => {
nextTick(() => {
paging.value?.colseFile();
paging.value?.closeFile();
});
});

View File

@@ -249,14 +249,13 @@ import {
ref,
inject,
nextTick,
defineProps,
defineEmits,
onMounted,
onUnmounted,
toRaw,
reactive,
computed,
watch,
getCurrentInstance,
} from 'vue';
import { storeToRefs } from 'pinia';
// import config from '@/config.js';
@@ -289,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([]);
@@ -338,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) {
@@ -475,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) {
@@ -647,7 +687,7 @@ function changeShowFile() {
showfile.value = !showfile.value;
}
function colseFile() {
function closeFile() {
showfile.value = false;
}
@@ -773,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

@@ -37,7 +37,7 @@
</template>
<script setup>
import { ref, inject, defineEmits } from 'vue';
import { ref, inject } from 'vue';
const emit = defineEmits(['onSend']);
const { $api } = inject('globalFunction');
const popup = ref(null);

View File

@@ -0,0 +1,900 @@
<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="selectEnterpriseType">
<view class="label">企业类型</view>
<view class="input-content">
<input class="input-con" v-model="formData.natureText" disabled placeholder="请选择企业类型" />
<uni-icons type="arrowright" size="16" color="#999"></uni-icons>
</view>
</view>
<!-- 是否是就业见习基地 -->
<view class="form-item clickable" @click="selectEmploymentBase">
<view class="label">是否是就业见习基地</view>
<view class="input-content">
<text class="input-text" :class="{ placeholder: formData.enterpriseType === null }">
{{ formData.enterpriseType === null ? '请选择' : (formData.enterpriseType ? '是' : '否') }}
</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.legalIdCard"
placeholder="请输入法人身份证号"
maxlength="18"
@input="updateCompletion"
/>
</view>
<!-- 法人联系方式 -->
<view class="form-item">
<view class="label">法人联系方式</view>
<input
class="input-field"
v-model="formData.legalPhone"
placeholder="请输入法人联系方式"
maxlength="11"
@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.companyContactList"
:key="index"
>
<view class="group-header">
<text>联系人{{ index + 1 }}</text>
<view
class="delete-btn"
@click="deleteContact(index)"
v-if="formData.companyContactList.length > 1"
>
<uni-icons type="trash" size="16" color="#ff4757"></uni-icons>
<text class="delete-text">删除</text>
</view>
</view>
<view class="form-item">
<view class="label">联系人姓名</view>
<input
class="input-field"
v-model="contact.contactPerson"
placeholder="请输入联系人姓名"
@input="updateCompletion"
/>
</view>
<view class="form-item">
<view class="label">联系人电话</view>
<input
class="input-field"
v-model="contact.contactPersonPhone"
placeholder="请输入联系人电话"
@input="updateCompletion"
/>
</view>
</view>
</view>
<!-- 添加联系人按钮 -->
<view class="add-contact-btn" @click="addContact" v-if="formData.companyContactList.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>
<!-- 滚动选择器 -->
<SelectPopup ref="selectPopupRef"></SelectPopup>
</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'
import SelectPopup from '@/components/selectPopup/selectPopup.vue'
import useDictStore from '@/stores/useDictStore'
const { $api } = inject('globalFunction')
const dictStore = useDictStore()
// 表单数据
const formData = reactive({
companyName: '',
socialCreditCode: '',
registeredAddress: '',
registeredAddressName: '',
longitude: null,
latitude: null,
companyIntro: '',
legalPersonName: '',
nature: '', // 企业类型
natureText: '', // 企业类型显示文本
enterpriseType: null, // 是否是就业见习基地 (true/false/null)
legalIdCard: '', // 法人身份证号
legalPhone: '', // 法人联系方式
industryType: '', // 是否是本地重点发展产业
isLocalCompany: null, // 是否是本地企业 (true/false/null)
companyContactList: [
{ contactPerson: '', contactPersonPhone: '' }
]
})
// 弹窗相关
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 selectPopupRef = ref(null)
// 创建本地的 openSelectPopup 函数
const openSelectPopup = (config) => {
if (selectPopupRef.value) {
selectPopupRef.value.open(config);
}
}
// 产业类型选项数据
const industryOptions = [
'人工智能',
'生物医药',
'新能源',
'高端装备制造',
'其他'
]
// 备用企业类型选项(当字典数据加载失败时使用)
const fallbackEnterpriseTypes = [
{ label: '有限责任公司', value: '1' },
{ label: '股份有限公司', value: '2' },
{ label: '个人独资企业', value: '3' },
{ label: '合伙企业', value: '4' },
{ label: '外商投资企业', value: '5' },
{ label: '其他', value: '6' }
]
// 企业类型选项数据从字典中获取
const enterpriseTypeOptions = computed(() => {
const natureData = dictStore.state?.nature || []
console.log('企业类型选项数据:', natureData)
return natureData
})
// 完成度计算
const completionPercentage = computed(() => {
const fields = [
formData.companyName,
formData.socialCreditCode,
formData.registeredAddress,
formData.companyIntro,
formData.legalPersonName,
formData.nature,
formData.enterpriseType !== null ? 'filled' : '',
formData.legalIdCard,
formData.legalPhone,
formData.industryType,
formData.isLocalCompany !== null ? 'filled' : ''
]
// 检查联系人信息
const hasContact = formData.companyContactList.some(contact => contact.contactPerson && contact.contactPersonPhone)
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 selectEnterpriseType = () => {
console.log('点击企业类型,当前数据:', enterpriseTypeOptions.value)
console.log('字典store状态:', dictStore.state.nature)
// 获取企业类型选项,优先使用字典数据,失败时使用备用数据
let options = enterpriseTypeOptions.value
if (!options || !options.length) {
console.log('企业类型数据为空,尝试重新加载')
// 尝试重新加载字典数据
dictStore.getDictData().then(() => {
if (dictStore.state.nature && dictStore.state.nature.length > 0) {
selectEnterpriseType() // 递归调用
} else {
// 使用备用数据
console.log('使用备用企业类型数据')
options = fallbackEnterpriseTypes
showEnterpriseTypeSelector(options)
}
}).catch(() => {
// 使用备用数据
console.log('字典加载失败,使用备用企业类型数据')
options = fallbackEnterpriseTypes
showEnterpriseTypeSelector(options)
})
return
}
showEnterpriseTypeSelector(options)
}
// 显示企业类型选择器
const showEnterpriseTypeSelector = (options) => {
console.log('企业类型选项列表:', options)
openSelectPopup({
title: '企业类型',
maskClick: true,
data: [options],
success: (_, [value]) => {
console.log('选择的企业类型:', value)
formData.nature = value.value
formData.natureText = value.label
updateCompletion()
$api.msg('企业类型选择成功')
}
})
}
// 选择是否是就业见习基地
const selectEmploymentBase = () => {
uni.showActionSheet({
itemList: ['是', '否'],
success: (res) => {
formData.enterpriseType = res.tapIndex === 0
updateCompletion()
$api.msg('选择成功')
}
})
}
// 选择是否是本地企业
const selectLocalCompany = () => {
uni.showActionSheet({
itemList: ['是', '否'],
success: (res) => {
formData.isLocalCompany = res.tapIndex === 0
updateCompletion()
$api.msg('选择成功')
}
})
}
// 添加联系人
const addContact = () => {
if (formData.companyContactList.length < 3) {
formData.companyContactList.push({ contactPerson: '', contactPersonPhone: '' })
}
}
// 删除联系人
const deleteContact = (index) => {
if (formData.companyContactList.length <= 1) {
$api.msg('至少需要保留一个联系人')
return
}
uni.showModal({
title: '确认删除',
content: '确定要删除这个联系人吗?',
success: (res) => {
if (res.confirm) {
formData.companyContactList.splice(index, 1)
updateCompletion()
$api.msg('联系人已删除')
}
}
})
}
// 关闭弹窗
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.nature.trim()) {
$api.msg('请选择企业类型')
return
}
if (formData.enterpriseType === null) {
$api.msg('请选择是否是就业见习基地')
return
}
if (!formData.legalIdCard.trim()) {
$api.msg('请输入法人身份证号')
return
}
if (!formData.legalPhone.trim()) {
$api.msg('请输入法人联系方式')
return
}
if (!formData.industryType.trim()) {
$api.msg('请选择产业类型')
return
}
if (formData.isLocalCompany === null) {
$api.msg('请选择是否是本地企业')
return
}
// 验证身份证号格式
const idCardRegex = /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/
if (!idCardRegex.test(formData.legalIdCard)) {
$api.msg('请输入正确的身份证号')
return
}
// 验证法人电话格式
const phoneRegex = /^1[3-9]\d{9}$/
if (!phoneRegex.test(formData.legalPhone)) {
$api.msg('请输入正确的法人联系方式')
return
}
// 验证至少有一个联系人
const hasValidContact = formData.companyContactList.some(contact =>
contact.contactPerson.trim() && contact.contactPersonPhone.trim()
)
if (!hasValidContact) {
$api.msg('请至少添加一个联系人信息')
return
}
// 验证联系人电话格式
for (let contact of formData.companyContactList) {
if (contact.contactPerson.trim() && contact.contactPersonPhone.trim()) {
if (!phoneRegex.test(contact.contactPersonPhone)) {
$api.msg('请输入正确的手机号码')
return
}
}
}
// 提交数据
uni.showLoading({ title: '保存中...' })
// 构建提交数据,按照要求的字段映射
const companyData = {
name: formData.companyName,
code: formData.socialCreditCode,
registeredAddress: formData.registeredAddress,
description: formData.companyIntro,
legalPerson: formData.legalPersonName,
nature: formData.nature,
enterpriseType: formData.enterpriseType,
legalIdCard: formData.legalIdCard,
legalPhone: formData.legalPhone,
industryType: formData.industryType,
isLocalCompany: formData.isLocalCompany,
companyContactList: formData.companyContactList.filter(contact => contact.contactPerson.trim() && contact.contactPersonPhone.trim())
}
// 调用新的接口地址数据格式为company数组
const submitData = {
company: companyData
}
$api.createRequest('/app/user/registerUser', submitData, 'post')
.then((resData) => {
uni.hideLoading()
$api.msg('企业信息保存成功')
// 跳转到首页或企业相关页面
uni.reLaunch({
url: '/pages/index/index'
})
})
.catch((err) => {
uni.hideLoading()
$api.msg(err.msg || '保存失败,请重试')
})
}
onLoad(async (options) => {
console.log('企业信息补全页面开始加载')
try {
// 初始化字典数据
await dictStore.getDictData()
console.log('字典数据加载完成:', {
nature: dictStore.state.nature,
complete: dictStore.complete
})
} catch (error) {
console.error('字典数据加载失败:', error)
$api.msg('数据加载失败,请重试')
}
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-con
flex: 1
font-size: 28rpx
color: #333
text-align: right
background: transparent
border: none
outline: none
&::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
display: flex
justify-content: space-between
align-items: center
.delete-btn
display: flex
align-items: center
padding: 8rpx 16rpx
background: #fff5f5
border: 1rpx solid #ff4757
border-radius: 8rpx
cursor: pointer
.delete-text
margin-left: 8rpx
font-size: 24rpx
color: #ff4757
.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

@@ -0,0 +1,596 @@
<template>
<AppLayout title="AI+就业服务程序">
<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">
<view class="title-lf">
<view class="lf-h1">请您完善求职名片</view>
<view class="lf-text">个人信息仅用于推送优质内容</view>
</view>
<view class="title-ri">
<text style="color: #256bfa">1</text>
<text>/2</text>
</view>
</view>
<view class="content-input" @click="changeExperience">
<view class="input-titile">工作经验</view>
<input
class="input-con"
v-model="state.experienceText"
disabled
placeholder="请选择您的工作经验"
/>
</view>
<view class="content-sex">
<view class="sex-titile">求职区域</view>
<view class="sext-ri">
<view
class="sext-box"
:class="{ 'sext-boxactive': fromValue.sex === 0 }"
@click="changeSex(0)"
>
</view>
<view
class="sext-box"
:class="{ 'sext-boxactive': fromValue.sex === 1 }"
@click="changeSex(1)"
>
</view>
</view>
</view>
<view class="content-input" @click="changeEducation">
<view class="input-titile">学历</view>
<input class="input-con" v-model="state.educationText" disabled placeholder="本科" />
</view>
<view class="content-input" :class="{ 'input-error': idCardError }">
<view class="input-titile">身份证</view>
<input
class="input-con2"
v-model="fromValue.idcard"
maxlength="18"
placeholder="请输入身份证号码"
@input="validateIdCard"
/>
<view v-if="idCardError" class="error-message">{{ idCardError }}</view>
<view v-if="fromValue.idcard && !idCardError" class="success-message"> 身份证格式正确</view>
</view>
</view>
<view class="next-btn" @tap="nextStep">下一步</view>
</view>
</swiper-item>
<swiper-item @touchmove.stop="false">
<view class="content-one">
<view>
<view class="content-title">
<view class="title-lf">
<view class="lf-h1">请您完善求职名片</view>
<view class="lf-text">个人信息仅用于推送优质内容</view>
</view>
<view class="title-ri">
<text style="color: #256bfa">2</text>
<text>/2</text>
</view>
</view>
<view class="content-input" @click="changeArea">
<view class="input-titile">求职区域</view>
<input
class="input-con"
v-model="state.areaText"
disabled
placeholder="请选择您的求职区域"
/>
</view>
<view class="content-input" @click="changeJobs">
<view class="input-titile">求职岗位</view>
<input
class="input-con"
disabled
v-if="!state.jobsText.length"
placeholder="请选择您的求职岗位"
/>
<view class="input-nx" @click="changeJobs" v-else>
<view class="nx-item" v-for="item in state.jobsText">{{ item }}</view>
</view>
</view>
<view class="content-input" @click="changeSalay">
<view class="input-titile">期望薪资</view>
<input
class="input-con"
v-model="state.salayText"
disabled
placeholder="请选择您的期望薪资"
/>
</view>
</view>
<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 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';
import useDictStore from '@/stores/useDictStore';
const { $api, navTo, config, IdCardValidator } = inject('globalFunction');
const { loginSetToken, getUserResume } = useUserStore();
const { getDictSelectOption, oneDictData } = useDictStore();
// #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({
station: [],
stationCateLog: 1,
lfsalay: [2, 5, 10, 15, 20, 25, 30, 50],
risalay: JSON.parse(JSON.stringify(salay)),
areaText: '',
educationText: '',
experienceText: '',
salayText: '',
jobsText: [],
});
const fromValue = reactive({
sex: 1,
education: '4',
salaryMin: 2000,
salaryMax: 2000,
area: 0,
jobTitleId: '',
experience: '1',
idcard: '',
});
// 身份证校验相关
const idCardError = ref('');
onLoad((parmas) => {
getTreeselect();
});
onMounted(() => {});
function changeSex(sex) {
fromValue.sex = sex;
}
// 身份证实时校验
function validateIdCard() {
const idCard = fromValue.idcard.trim();
// 如果为空,清除错误信息
if (!idCard) {
idCardError.value = '';
return;
}
// 使用身份证校验器进行校验
const result = IdCardValidator.validate(idCard);
if (result.valid) {
idCardError.value = '';
} else {
idCardError.value = result.message;
}
}
function changeExperience() {
openSelectPopup({
title: '工作经验',
maskClick: true,
data: [oneDictData('experience')],
success: (_, [value]) => {
fromValue.experience = value.value;
state.experienceText = value.label;
},
change(_, [value]) {
// this.setColunm(1, [123, 123]);
console.log(this);
},
});
}
function changeEducation() {
openSelectPopup({
title: '学历',
maskClick: true,
data: [oneDictData('education')],
success: (_, [value]) => {
fromValue.area = value.value;
state.educationText = value.label;
},
});
}
function changeArea() {
openSelectPopup({
title: '区域',
maskClick: true,
data: [oneDictData('area')],
success: (_, [value]) => {
fromValue.area = value.value;
state.areaText = config.appInfo.areaName + '-' + value.label;
},
});
}
function changeSalay() {
let leftIndex = 0;
openSelectPopup({
title: '薪资',
maskClick: true,
data: [state.lfsalay, state.risalay],
unit: 'k',
success: (_, [min, max]) => {
fromValue.salaryMin = min.value * 1000;
fromValue.salaryMax = max.value * 1000;
state.salayText = `${fromValue.salaryMin}-${fromValue.salaryMax}`;
},
change(e) {
const salayData = e.detail.value;
if (leftIndex !== salayData[0]) {
const copyri = JSON.parse(JSON.stringify(salay));
const [lf, ri] = e.detail.value;
const risalay = copyri.slice(lf, copyri.length);
this.setColunm(1, risalay);
leftIndex = salayData[0];
}
},
});
}
function changeJobs() {
selectJobsModel.value?.open({
title: '添加岗位',
success: (ids, labels) => {
fromValue.jobTitleId = ids;
state.jobsText = labels.split(',');
},
});
}
function nextStep() {
// 校验身份证号码
const idCard = fromValue.idcard.trim();
if (!idCard) {
$api.msg('请输入身份证号码');
return;
}
const result = IdCardValidator.validate(idCard);
if (!result.valid) {
$api.msg(result.message);
return;
}
tabCurrent.value += 1;
}
// 获取职位
function getTreeselect() {
$api.createRequest('/app/common/jobTitle/treeselect', {}, 'GET').then((resData) => {
state.station = resData.data;
});
}
// 登录
function loginTest() {
// uni.share({
// provider: 'weixin',
// scene: 'WXSceneSession',
// type: 2,
// imageUrl: 'https://qiniu-web-assets.dcloud.net.cn/unidoc/zh/uni@2x.png',
// success: function (res) {
// console.log('success:' + JSON.stringify(res));
// },
// fail: function (err) {
// console.log('fail:' + JSON.stringify(err));
// },
// });
const params = {
username: 'test',
password: 'test',
};
$api.createRequest('/app/login', params, 'post').then((resData) => {
$api.msg('模拟帐号密码测试登录成功');
loginSetToken(resData.token).then((resume) => {
if (resume.data.jobTitleId) {
// 设置推荐列表,每次退出登录都需要更新
useUserStore().initSeesionId();
uni.reLaunch({
url: '/pages/index/index',
});
} else {
nextStep();
}
});
});
}
function complete() {
const result = IdCardValidator.validate(fromValue.idcard);
if (result.valid) {
$api.createRequest('/app/user/resume', fromValue, 'post').then((resData) => {
$api.msg('完成');
// 获取用户信息并存储到store中
getUserResume().then((userInfo) => {
console.log('用户信息已存储到store:', userInfo);
uni.reLaunch({
url: '/pages/index/index',
});
});
});
} else {
$api.msg('身份证校验失败');
console.log('验证失败:', result.message);
}
}
</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
padding-bottom: 30rpx
display: flex
flex-wrap: wrap
.nx-item
padding: 20rpx 28rpx
width: fit-content
border-radius: 12rpx 12rpx 12rpx 12rpx;
border: 2rpx solid #E8EAEE;
margin-right: 24rpx
margin-top: 24rpx
.nx-item::before
position: absolute;
right: 20rpx;
top: 60rpx;
content: '';
width: 4rpx;
height: 18rpx;
border-radius: 2rpx
background: #697279;
transform: translate(0, -50%) rotate(-45deg) ;
.nx-item::after
position: absolute;
right: 20rpx;
top: 61rpx;
content: '';
width: 4rpx;
height: 18rpx;
border-radius: 2rpx
background: #697279;
transform: rotate(45deg)
.container
// background: linear-gradient(#4778EC, #002979);
width: 100%;
height: calc(100vh - var(--window-top) - var(--status-bar-height) - var(--window-bottom));
position: fixed;
background: url('@/static/icon/background2.png') 0 0 no-repeat;
background-size: 100% 728rpx;
display: flex;
flex-direction: column
.container-hader
height: 88rpx;
text-align: center;
line-height: 88rpx;
color: #000000;
font-weight: bold
font-size: 32rpx
.login-content
position: absolute;
left: 50%;
top: 40%;
transform: translate(-50%, -50%);
display: flex;
align-items: flex-end;
flex-wrap: nowrap;
.logo
width: 266rpx;
height: 182rpx;
.logo-title
font-size: 88rpx;
color: #22c984;
width: 180rpx;
.btns
position: absolute;
top: 70%;
left: 50%;
transform: translate(-50%, 0)
.wxlogin
width: 562rpx;
height: 140rpx;
border-radius: 70rpx;
background-color: #13C57C;
color: #FFFFFF;
text-align: center;
line-height: 140rpx;
font-size: 70rpx;
.wxaddress
color: #BBBBBB;
margin-top: 70rpx;
text-align: center;
.content-one
padding: 60rpx 28rpx;
display: flex;
flex-direction: column;
justify-content: space-between
height: calc(100% - 120rpx)
.content-title
display: flex
justify-content: space-between;
align-items: center
margin-bottom: 70rpx
.title-lf
font-size: 44rpx;
color: #000000;
font-weight: 600;
.lf-text
font-weight: 400;
font-size: 28rpx;
color: #6C7282;
.title-ri
font-size: 36rpx;
color: #000000;
font-weight: 600;
.content-input
margin-bottom: 52rpx
.input-titile
font-weight: 400;
font-size: 28rpx;
color: #6A6A6A;
.input-con2
font-weight: 400;
font-size: 32rpx;
color: #333333;
line-height: 80rpx;
height: 80rpx;
border-bottom: 2rpx solid #EBEBEB
.input-con
font-weight: 400;
font-size: 32rpx;
color: #333333;
line-height: 80rpx;
height: 80rpx;
border-bottom: 2rpx solid #EBEBEB
position: relative;
.input-con::before
position: absolute;
right: 20rpx;
top: calc(50% - 2rpx);
content: '';
width: 4rpx;
height: 18rpx;
border-radius: 2rpx
background: #697279;
transform: translate(0, -50%) rotate(-45deg) ;
.input-con::after
position: absolute;
right: 20rpx;
top: 50%;
content: '';
width: 4rpx;
height: 18rpx;
border-radius: 2rpx
background: #697279;
transform: rotate(45deg)
.error-message
color: #ff4757;
font-size: 24rpx;
margin-top: 10rpx;
line-height: 1.4;
.success-message
color: #2ed573;
font-size: 24rpx;
margin-top: 10rpx;
line-height: 1.4;
.input-error
.input-con2
border-bottom-color: #ff4757;
.content-sex
height: 110rpx;
display: flex
justify-content: space-between;
align-items: flex-start;
border-bottom: 2rpx solid #EBEBEB
margin-bottom: 52rpx
.sex-titile
line-height: 80rpx;
.sext-ri
display: flex
align-items: center;
.sext-box
height: 76rpx;
width: 152rpx;
text-align: center;
line-height: 80rpx;
border-radius: 12rpx 12rpx 12rpx 12rpx
border: 2rpx solid #E8EAEE;
margin-left: 28rpx
font-weight: 400;
font-size: 28rpx;
.sext-boxactive
color: #256BFA
background: rgba(37,107,250,0.1);
border: 2rpx solid #256BFA;
.next-btn
width: 100%;
height: 90rpx;
background: #256BFA;
border-radius: 12rpx 12rpx 12rpx 12rpx;
font-weight: 500;
font-size: 32rpx;
color: #FFFFFF;
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

@@ -0,0 +1,272 @@
<template>
<view class="demo-page">
<view class="demo-title">阿里图标库使用示例</view>
<!-- 方式一直接使用 iconfont -->
<view class="demo-section">
<view class="section-title">方式一直接使用</view>
<view class="icon-list">
<view class="icon-item">
<text class="iconfont icon-home"></text>
<text class="icon-name">icon-home</text>
</view>
<view class="icon-item">
<text class="iconfont icon-user"></text>
<text class="icon-name">icon-user</text>
</view>
<view class="icon-item">
<text class="iconfont icon-search"></text>
<text class="icon-name">icon-search</text>
</view>
</view>
</view>
<!-- 方式二使用封装的组件 -->
<view class="demo-section">
<view class="section-title">方式二使用封装组件</view>
<view class="icon-list">
<view class="icon-item">
<IconfontIcon name="home" :size="48" color="#13C57C" />
<text class="icon-name">绿色 48rpx</text>
</view>
<view class="icon-item">
<IconfontIcon name="user" :size="36" color="#256BFA" />
<text class="icon-name">蓝色 36rpx</text>
</view>
<view class="icon-item">
<IconfontIcon name="search" :size="32" color="#FF9800" />
<text class="icon-name">橙色 32rpx</text>
</view>
</view>
</view>
<!-- 方式三使用配置常量 -->
<view class="demo-section">
<view class="section-title">方式三使用配置常量</view>
<view class="icon-list">
<view class="icon-item">
<IconfontIcon
:name="ICONS.PHONE"
:size="ICON_SIZES.LARGE"
:color="ICON_COLORS.PRIMARY"
/>
<text class="icon-name">电话</text>
</view>
<view class="icon-item">
<IconfontIcon
:name="ICONS.MESSAGE"
:size="ICON_SIZES.LARGE"
:color="ICON_COLORS.SECONDARY"
/>
<text class="icon-name">消息</text>
</view>
<view class="icon-item">
<IconfontIcon
:name="ICONS.LOCATION"
:size="ICON_SIZES.LARGE"
:color="ICON_COLORS.DANGER"
/>
<text class="icon-name">位置</text>
</view>
</view>
</view>
<!-- 方式四按钮中使用 -->
<view class="demo-section">
<view class="section-title">方式四在按钮中使用</view>
<button class="demo-button primary">
<IconfontIcon name="phone" :size="32" color="#FFFFFF" />
<text>手机号登录</text>
</button>
<button class="demo-button secondary">
<IconfontIcon name="user" :size="32" color="#256BFA" />
<text>个人中心</text>
</button>
<button class="demo-button success">
<IconfontIcon name="star" :size="32" color="#FFFFFF" />
<text>收藏职位</text>
</button>
</view>
<!-- 方式五列表中使用 -->
<view class="demo-section">
<view class="section-title">方式五在列表中使用</view>
<view class="demo-list">
<view class="list-item" v-for="item in menuList" :key="item.id">
<IconfontIcon :name="item.icon" :size="40" :color="item.color" />
<view class="item-content">
<view class="item-title">{{ item.title }}</view>
<view class="item-desc">{{ item.desc }}</view>
</view>
<IconfontIcon name="arrow-right" :size="28" color="#999" />
</view>
</view>
</view>
<!-- 注意事项 -->
<view class="demo-section">
<view class="section-title"> 注意事项</view>
<view class="tips-box">
<view class="tip-item">1. 确保已从阿里图标库下载字体文件到 static/iconfont/ 目录</view>
<view class="tip-item">2. 确保已在 App.vue 中引入 iconfont.css</view>
<view class="tip-item">3. 图标名称需要与阿里图标库中的类名保持一致</view>
<view class="tip-item">4. 推荐使用封装的 IconfontIcon 组件便于统一管理</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref } from 'vue'
import IconfontIcon from '@/components/IconfontIcon/IconfontIcon.vue'
import { ICONS, ICON_SIZES, ICON_COLORS } from '@/config/icons'
const menuList = ref([
{
id: 1,
icon: 'home',
title: '首页',
desc: '查看推荐职位',
color: '#13C57C'
},
{
id: 2,
icon: 'search',
title: '搜索',
desc: '搜索心仪职位',
color: '#256BFA'
},
{
id: 3,
icon: 'message',
title: '消息',
desc: '查看聊天消息',
color: '#FF9800'
},
{
id: 4,
icon: 'user',
title: '我的',
desc: '个人中心',
color: '#9C27B0'
}
])
</script>
<style lang="stylus" scoped>
.demo-page
padding: 40rpx
background: #F5F5F5
min-height: 100vh
.demo-title
font-size: 48rpx
font-weight: bold
color: #333
text-align: center
margin-bottom: 40rpx
.demo-section
background: #FFFFFF
border-radius: 16rpx
padding: 32rpx
margin-bottom: 32rpx
.section-title
font-size: 32rpx
font-weight: 600
color: #333
margin-bottom: 24rpx
padding-bottom: 16rpx
border-bottom: 2rpx solid #F0F0F0
.icon-list
display: flex
flex-wrap: wrap
gap: 32rpx
.icon-item
display: flex
flex-direction: column
align-items: center
gap: 12rpx
min-width: 120rpx
.iconfont
font-size: 48rpx
color: #333
.icon-name
font-size: 24rpx
color: #666
text-align: center
.demo-button
width: 100%
height: 88rpx
border-radius: 44rpx
display: flex
align-items: center
justify-content: center
gap: 12rpx
font-size: 32rpx
margin-bottom: 20rpx
border: none
&.primary
background: linear-gradient(135deg, #13C57C 0%, #0FA368 100%)
color: #FFFFFF
&.secondary
background: #F7F8FA
color: #256BFA
&.success
background: linear-gradient(135deg, #FF9800 0%, #F57C00 100%)
color: #FFFFFF
.demo-list
display: flex
flex-direction: column
gap: 20rpx
.list-item
display: flex
align-items: center
padding: 24rpx
background: #F7F8FA
border-radius: 12rpx
gap: 20rpx
.item-content
flex: 1
.item-title
font-size: 30rpx
color: #333
font-weight: 500
margin-bottom: 8rpx
.item-desc
font-size: 24rpx
color: #999
.tips-box
padding: 24rpx
background: #FFF3E0
border-radius: 12rpx
.tip-item
font-size: 26rpx
color: #E65100
line-height: 1.8
margin-bottom: 12rpx
&:last-child
margin-bottom: 0
// 按钮重置样式
button::after
border: none
</style>

View File

@@ -1,6 +1,14 @@
<template>
<view class="app-container">
<view class="nav-hidden hidden-animation" :class="{ 'hidden-height': isScrollingDown }">
<!-- #ifdef MP-WEIXIN -->
<!-- 小程序背景图片 -->
<image class="mp-background" src="/static/icon/background2.png" mode="aspectFill"></image>
<!-- #endif -->
<view
class="nav-hidden hidden-animation"
:class="{ 'hidden-height': shouldHideTop }"
>
<view class="container-search">
<view class="search-input button-click" @click="navTo('/pages/search/search')">
<uni-icons class="iconsearch" color="#666666" type="search" size="18"></uni-icons>
@@ -8,18 +16,87 @@
</view>
<!-- <view class="chart button-click">职业图谱</view> -->
</view>
<view class="cards" v-if="userInfo.isCompanyUser">
<view class="card press-button" @click="navTo('/pages/nearby/nearby')">
<view class="cards" v-if="userInfo.userType !== 0">
<view class="card press-button" @click="handleNearbyClick">
<view class="card-title">附近工作</view>
<view class="card-text">好岗职等你来</view>
</view>
<view class="card press-button" @click="navTo('/packageA/pages/choiceness/choiceness')">
<!-- <view class="card press-button" @click="navTo('/packageA/pages/choiceness/choiceness')">
<view class="card-title">精选企业</view>
<view class="card-text">优选职得信赖</view>
</view> -->
</view>
<!-- 服务功能网格 -->
<view class="service-grid" v-if="userInfo.userType !== 0">
<view class="service-item press-button" @click="handleServiceClick('service-guidance')">
<view class="service-icon service-icon-1">
<uni-icons type="auth-filled" size="32" color="#FFFFFF"></uni-icons>
</view>
<view class="service-title">服务指导</view>
</view>
<view class="service-item press-button" @click="handleServiceClick('public-recruitment')">
<view class="service-icon service-icon-2">
<IconfontIcon name="zhengfulou" :size="48" color="#FFFFFF" />
</view>
<view class="service-title">事业单位招录</view>
</view>
<view class="service-item press-button" @click="handleServiceClick('resume-creation')">
<view class="service-icon service-icon-3">
<IconfontIcon name="jianli" :size="48" color="#FFFFFF" />
</view>
<view class="service-title">简历制作</view>
</view>
<view class="service-item press-button" @click="handleServiceClick('labor-policy')">
<view class="service-icon service-icon-4">
<IconfontIcon name="zhengce" :size="48" color="#FFFFFF" />
</view>
<view class="service-title">劳动政策指引</view>
</view>
<view class="service-item press-button" @click="handleServiceClick('skill-training')">
<view class="service-icon service-icon-5">
<IconfontIcon name="jinengpeixun" :size="48" color="#FFFFFF" />
</view>
<view class="service-title">技能培训信息</view>
</view>
<view class="service-item press-button" @click="handleServiceClick('skill-evaluation')">
<view class="service-icon service-icon-6">
<IconfontIcon name="jinengpingjia" :size="48" color="#FFFFFF" />
</view>
<view class="service-title">技能评价指引</view>
</view>
<view class="service-item press-button" @click="handleServiceClick('question-bank')">
<view class="service-icon service-icon-7">
<IconfontIcon name="suzhicepingtiku" :size="48" color="#FFFFFF" />
</view>
<view class="service-title">题库和考试</view>
</view>
<view class="service-item press-button" @click="handleServiceClick('quality-assessment')">
<view class="service-icon service-icon-8">
<IconfontIcon name="suzhicepingtiku" :size="48" color="#FFFFFF" />
</view>
<view class="service-title">素质测评</view>
</view>
<view class="service-item press-button" @click="handleServiceClick('ai-interview')">
<view class="service-icon service-icon-9">
<IconfontIcon name="ai" :size="68" color="#FFFFFF" />
</view>
<view class="service-title">AI智能面试</view>
</view>
<view class="service-item press-button" @click="navTo('/packageB/pages/search/search')">
<view class="service-icon service-icon-9">
<IconfontIcon name="ai" :size="68" color="#FFFFFF" />
</view>
<view class="service-title">AI智慧就业服务</view>
</view>
</view>
</view>
<view class="nav-filter" v-if="userInfo.isCompanyUser">
<!-- 吸顶筛选区域占位 -->
<view class="filter-placeholder" v-if="shouldStickyFilter && userInfo.userType !== 0"></view>
<view class="nav-filter" :class="{ 'sticky-filter': shouldStickyFilter }" v-if="userInfo.userType !== 0">
<view class="filter-top" @touchmove.stop.prevent>
<scroll-view :scroll-x="true" :show-scrollbar="false" class="tab-scroll">
<view class="jobs-left">
@@ -65,8 +142,94 @@
</view>
</view>
<view class="table-list">
<scroll-view :scroll-y="true" class="falls-scroll" @scroll="handleScroll" @scrolltolower="scrollBottom">
<scroll-view
:scroll-y="true"
class="falls-scroll"
@scroll="handleScroll"
@scrolltolower="scrollBottom"
:enable-back-to-top="false"
:scroll-with-animation="false"
>
<view class="falls" v-if="list.length">
<!-- #ifdef MP-WEIXIN -->
<!-- 小程序使用具名插槽 -->
<custom-waterfalls-flow
:column="columnCount"
:columnSpace="columnSpace"
ref="waterfallsFlowRef"
:value="list"
>
<view v-for="(job, index) in list" :key="index" :slot="`slot${index}`">
<view class="item btn-feel" v-if="!job.recommend">
<view class="falls-card" @click="nextDetail(job)">
<view class="falls-card-pay">
<view class="pay-text">
<Salary-Expectation
:max-salary="job.maxSalary"
:min-salary="job.minSalary"
:is-month="true"
></Salary-Expectation>
</view>
<image v-if="job.isHot" class="flame" src="/static/icon/flame.png"></image>
</view>
<view class="falls-card-title">{{ job.jobTitle }}</view>
<view class="fl_box fl_warp">
<view class="falls-card-education mar_ri10" v-if="job.education">
<dict-Label dictType="education" :value="job.education"></dict-Label>
</view>
<view class="falls-card-experience" v-if="job.experience">
<dict-Label dictType="experience" :value="job.experience"></dict-Label>
</view>
</view>
<view class="falls-card-company" v-show="isShowJw !== 3">
{{ config.appInfo.areaName }}
<!-- {{ job.jobLocation }} -->
<dict-Label dictType="area" :value="job.jobLocationAreaCode"></dict-Label>
</view>
<view class="falls-card-pepleNumber">
<view>
<image class="point2" src="/static/icon/pintDate.png"></image>
<view class="fl_1">
{{ job.postingDate || '发布日期' }}
</view>
</view>
<view>
<image class="point3" src="/static/icon/pointpeople.png"></image>
<view class="fl_1">
{{ vacanciesTo(job.vacancies) }}
</view>
</view>
</view>
<view class="falls-card-company2">
<image class="point3" src="/static/icon/point3.png"></image>
<view class="fl_1">
{{ job.companyName }}
</view>
</view>
<!-- <view class="falls-card-matchingrate">
<view class=""><matchingDegree :job="job"></matchingDegree></view>
<uni-icons type="star" size="30"></uni-icons>
</view> -->
</view>
</view>
<view class="item" :class="{ isBut: job.isBut }" v-else>
<view class="recommend-card">
<view class="card-content">
<view class="recommend-card-title">在找{{ job.jobCategory }}工作吗</view>
<view class="recommend-card-tip">{{ job.tip }}</view>
<view class="recommend-card-line"></view>
<view class="recommend-card-controll">
<view class="controll-no" @click="clearfindJob(job)">不是</view>
<view class="controll-yes" @click="findJob(job)">是的</view>
</view>
</view>
</view>
</view>
</view>
</custom-waterfalls-flow>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<!-- H5/App使用作用域插槽 -->
<custom-waterfalls-flow
:column="columnCount"
:columnSpace="columnSpace"
@@ -141,6 +304,7 @@
</view>
</template>
</custom-waterfalls-flow>
<!-- #endif -->
<loadmore ref="loadmoreRef"></loadmore>
</view>
<empty v-else pdTop="200"></empty>
@@ -149,6 +313,9 @@
<!-- 筛选 -->
<select-filter ref="selectFilterModel"></select-filter>
<!-- 微信授权登录弹窗 -->
<WxAuthLogin ref="wxAuthLoginRef" @success="handleLoginSuccess"></WxAuthLogin>
<!-- <view class="maskFristEntry" v-if="maskFristEntry">
<view class="entry-content">
<text class="text1">左滑查看视频</text>
@@ -161,14 +328,14 @@
</template>
<script setup>
import { reactive, inject, watch, ref, onMounted, watchEffect, nextTick } from 'vue';
import { reactive, inject, watch, ref, onMounted, onUnmounted, watchEffect, nextTick } from 'vue';
import img from '@/static/icon/filter.png';
import dictLabel from '@/components/dict-Label/dict-Label.vue';
const { $api, navTo, vacanciesTo, formatTotal, config } = inject('globalFunction');
import { onLoad, onShow } from '@dcloudio/uni-app';
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
const { userInfo } = storeToRefs(useUserStore());
const { userInfo, hasLogin, token } = storeToRefs(useUserStore());
import useDictStore from '@/stores/useDictStore';
const { getTransformChildren, oneDictData } = useDictStore();
import useLocationStore from '@/stores/useLocationStore';
@@ -176,15 +343,60 @@ import selectFilter from '@/components/selectFilter/selectFilter.vue';
import { useRecommedIndexedDBStore, jobRecommender } from '@/stores/useRecommedIndexedDBStore.js';
import { useScrollDirection } from '@/hook/useScrollDirection';
import { useColumnCount } from '@/hook/useColumnCount';
const { isScrollingDown, handleScroll } = useScrollDirection();
import WxAuthLogin from '@/components/WxAuthLogin/WxAuthLogin.vue';
import IconfontIcon from '@/components/IconfontIcon/IconfontIcon.vue'
// 滚动状态管理
const shouldHideTop = ref(false);
const shouldStickyFilter = ref(false);
const lastScrollTop = ref(0);
const scrollTop = ref(0);
// 当用户与筛选/导航交互时,临时锁定头部显示状态,避免因数据刷新导致回弹显示
const isInteractingWithFilter = ref(false);
// 滚动阈值配置
const HIDE_THRESHOLD = 50; // 隐藏顶部区域的滚动阈值(降低阈值,更容易触发)
const SHOW_THRESHOLD = 5; // 显示顶部区域的滚动阈值(接近顶部)
const STICKY_THRESHOLD = 80; // 筛选区域吸顶的滚动阈值
// 简化的滚动处理函数
function handleScroll(e) {
const currentScrollTop = e.detail.scrollTop || 0;
// 简单的滚动逻辑:向下滚动超过阈值就隐藏,滚动到顶部就显示
if (currentScrollTop > HIDE_THRESHOLD) {
if (!shouldHideTop.value) {
shouldHideTop.value = true;
}
} else if (currentScrollTop <= SHOW_THRESHOLD) {
// 仅在非交互期才允许自动显示顶部区域
if (shouldHideTop.value && !isInteractingWithFilter.value) {
shouldHideTop.value = false;
}
}
// 控制筛选区域吸顶
if (currentScrollTop > STICKY_THRESHOLD) {
if (!shouldStickyFilter.value) {
shouldStickyFilter.value = true;
}
} else {
if (shouldStickyFilter.value) {
shouldStickyFilter.value = false;
}
}
// 更新最后的滚动位置
lastScrollTop.value = currentScrollTop;
scrollTop.value = currentScrollTop;
}
const recommedIndexDb = useRecommedIndexedDBStore();
const emits = defineEmits(['onShowTabbar']);
console.log(userInfo.value);
const waterfallsFlowRef = ref(null);
const loadmoreRef = ref(null);
const conditionSearch = ref({});
const waterfallcolumn = ref(2);
const maskFristEntry = ref(false);
const wxAuthLoginRef = ref(null);
const state = reactive({
tabIndex: 'all',
});
@@ -219,6 +431,42 @@ const { columnCount, columnSpace } = useColumnCount(() => {
});
});
// 组件初始化时加载数据
onMounted(() => {
getJobRecommend('refresh');
});
// 登录检查函数
const checkLogin = () => {
const tokenValue = uni.getStorageSync('token') || '';
if (!tokenValue || !hasLogin.value) {
// 未登录,打开授权弹窗
wxAuthLoginRef.value?.open();
return false;
}
return true;
};
// 登录成功回调
const handleLoginSuccess = () => {
// 登录成功后刷新数据
getJobRecommend('refresh');
};
// 处理附近工作点击
const handleNearbyClick = () => {
if (checkLogin()) {
navTo('/pages/nearby/nearby');
}
};
// 处理服务功能点击
const handleServiceClick = (serviceType) => {
if (checkLogin()) {
navToService(serviceType);
}
};
async function loadData() {
try {
if (isLoaded.value) return;
@@ -289,15 +537,48 @@ function clearfindJob(job) {
}
function nextDetail(job) {
// 记录岗位类型,用作数据分析
if (job.jobCategory) {
const recordData = recommedIndexDb.JobParameter(job);
recommedIndexDb.addRecord(recordData);
// 登录检查
if (checkLogin()) {
// 记录岗位类型,用作数据分析
if (job.jobCategory) {
const recordData = recommedIndexDb.JobParameter(job);
recommedIndexDb.addRecord(recordData);
}
navTo(`/packageA/pages/post/post?jobId=${encodeURIComponent(job.jobId)}`);
}
}
function navToService(serviceType) {
// 根据服务类型跳转到不同页面
const serviceRoutes = {
'service-guidance': '/pages/service/guidance',
'public-recruitment': '/pages/service/public-recruitment',
'resume-creation': '/packageA/pages/myResume/myResume',
'labor-policy': '/pages/service/labor-policy',
'skill-training': '/pages/service/skill-training',
'skill-evaluation': '/pages/service/skill-evaluation',
'question-bank': '/pages/service/question-bank',
'quality-assessment': '/pages/service/quality-assessment',
'ai-interview': '/pages/chat/chat',
'job-search': '/pages/search/search',
'career-planning': '/pages/service/career-planning',
'salary-query': '/pages/service/salary-query',
'company-info': '/pages/service/company-info',
'interview-tips': '/pages/service/interview-tips',
'employment-news': '/pages/service/employment-news',
'more-services': '/pages/service/more-services'
};
const route = serviceRoutes[serviceType];
if (route) {
navTo(route);
} else {
$api.msg('功能开发中,敬请期待');
}
navTo(`/packageA/pages/post/post?jobId=${btoa(job.jobId)}`);
}
function openFilter() {
isInteractingWithFilter.value = true;
showFilter.value = true;
emits('onShowTabbar', false);
selectFilterModel.value?.open({
@@ -308,23 +589,32 @@ function openFilter() {
...pageState.search,
};
for (const [key, value] of Object.entries(values)) {
pageState.search[key] = value.join(',');
// 特殊处理岗位类型,直接传递数字值
if (key === 'jobType') {
pageState.search.type = value.join(',');
} else {
pageState.search[key] = value.join(',');
}
}
showFilter.value = false;
getJobList('refresh');
// 短暂延迟后解除交互锁,避免数据刷新导致顶部区域回弹
setTimeout(() => { isInteractingWithFilter.value = false; }, 400);
},
cancel: () => {
showFilter.value = false;
emits('onShowTabbar', true);
setTimeout(() => { isInteractingWithFilter.value = false; }, 200);
},
});
}
function handleFilterConfirm(e) {
console.log(e);
// 处理筛选确认
}
function choosePosition(index) {
isInteractingWithFilter.value = true;
state.tabIndex = index;
list.value = [];
if (index === 'all') {
@@ -339,10 +629,11 @@ function choosePosition(index) {
inputText.value = '';
getJobList('refresh');
}
nextTick(() => { setTimeout(() => { isInteractingWithFilter.value = false; }, 300); });
}
const isShowJw = ref(0);
function handelHostestSearch(val) {
console.log(val.value);
isInteractingWithFilter.value = true;
isShowJw.value = val.value;
pageState.search.order = val.value;
pageState.search.jobType = val.value === 3 ? 1 : 0;
@@ -351,6 +642,7 @@ function handelHostestSearch(val) {
} else {
getJobList('refresh');
}
nextTick(() => { setTimeout(() => { isInteractingWithFilter.value = false; }, 300); });
}
function getJobRecommend(type = 'add') {
@@ -392,7 +684,8 @@ function getJobRecommend(type = 'add') {
list.value.push(...reslist);
});
} else {
list.value = dataToImg(data);
const reslist = dataToImg(data);
list.value = reslist;
}
// 切换状态
if (loadmoreRef.value && typeof loadmoreRef.value.change === 'function') {
@@ -451,11 +744,12 @@ function getJobList(type = 'add') {
}
function dataToImg(data) {
return data.map((item) => ({
const result = data.map((item) => ({
...item,
image: img,
hide: true,
}));
return result;
}
defineExpose({ loadData });
@@ -544,20 +838,46 @@ defineExpose({ loadData });
.app-container
width: 100%;
height: 100vh;
/* #ifdef H5 */
height: calc(100vh - var(--window-top) - var(--status-bar-height) - var(--window-bottom));
background: url('@/static/icon/background2.png') 0 0 no-repeat;
background-size: 100% 728rpx;
/* #endif */
/* #ifdef MP-WEIXIN */
/* 小程序不支持CSS中的本地图片使用image标签替代 */
position: relative;
/* #endif */
background-color: #FFFFFF;
display: flex;
flex-direction: column
.hidden-animation
max-height: 1000px;
transition: all 0.3s ease;
overflow: hidden;
.hidden-height
max-height: 0;
padding-top: 0;
padding-bottom: 0;
flex-direction: column;
overflow: hidden;
/* #ifdef MP-WEIXIN */
.mp-background
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 728rpx;
z-index: 0;
/* #endif */
.hidden-animation
transition: all 0.3s cubic-bezier(0.4, 0.0, 0.2, 1);
overflow: hidden;
opacity: 1;
transform: translateY(0);
will-change: transform, opacity;
.hidden-height
opacity: 0 !important;
transform: translateY(-100%) !important;
pointer-events: none !important;
max-height: 0 !important;
padding-top: 0 !important;
padding-bottom: 0 !important;
margin-top: 0 !important;
margin-bottom: 0 !important;
.container-search
padding: 16rpx 24rpx
display: flex
@@ -596,7 +916,7 @@ defineExpose({ loadData });
padding: 10rpx 28rpx
display: grid
grid-gap: 38rpx;
grid-template-columns: 1fr 1fr;
grid-template-columns: 1fr;
.card
height: calc(158rpx - 40rpx);
padding: 22rpx 26rpx
@@ -624,16 +944,217 @@ defineExpose({ loadData });
background-size: cover;
background-position: center;
// 服务功能网格样式
.service-grid
padding: 20rpx 28rpx
display: grid
grid-template-columns: 1fr 1fr 1fr 1fr
grid-gap: 20rpx
.service-item
display: flex
flex-direction: column
align-items: center
justify-content: center
height: 120rpx
background: transparent
padding: 10px 0px
.service-icon
width: 88rpx
height: 88rpx
border-radius: 12rpx
margin-bottom: 8rpx
flex-shrink: 0
.service-icon-1
background: linear-gradient(180deg, #FF8E8E 0%, #E53E3E 100%)
position: relative
display: flex
align-items: center
justify-content: center
.service-icon-2
background: linear-gradient(180deg, #6ED5CE 0%, #38B2AC 100%)
position: relative
display: flex
align-items: center
justify-content: center
.service-icon-3
background: linear-gradient(180deg, #6BC5D8 0%, #3182CE 100%)
position: relative
display: flex
align-items: center
justify-content: center
.service-icon-4
background: linear-gradient(180deg, #FFB74D 0%, #ED8936 100%)
position: relative
display: flex
align-items: center
justify-content: center
.service-icon-5
background: linear-gradient(180deg, #F06292 0%, #C2185B 100%)
position: relative
display: flex
align-items: center
justify-content: center
.service-icon-6
background: linear-gradient(180deg, #FFB74D 0%, #ED8936 100%)
position: relative
display: flex
align-items: center
justify-content: center
.service-icon-7
background: linear-gradient(180deg, #6BC5D8 0%, #3182CE 100%)
position: relative
display: flex
align-items: center
justify-content: center
.service-icon-8
background: linear-gradient(180deg, #81C784 0%, #4CAF50 100%)
position: relative
display: flex
align-items: center
justify-content: center
.service-icon-9
background: linear-gradient(180deg, #6BC5D8 0%, #3182CE 100%)
position: relative
display: flex
align-items: center
justify-content: center
.service-icon-10
background: linear-gradient(135deg, #9C27B0 0%, #BA68C8 100%)
position: relative
&::before
content: '🔍'
position: absolute
top: 50%
left: 50%
transform: translate(-50%, -50%)
font-size: 32rpx
.service-icon-11
background: linear-gradient(135deg, #FF9800 0%, #FFB74D 100%)
position: relative
&::before
content: '📈'
position: absolute
top: 50%
left: 50%
transform: translate(-50%, -50%)
font-size: 32rpx
.service-icon-12
background: linear-gradient(135deg, #4CAF50 0%, #81C784 100%)
position: relative
&::before
content: '💰'
position: absolute
top: 50%
left: 50%
transform: translate(-50%, -50%)
font-size: 32rpx
.service-icon-13
background: linear-gradient(135deg, #607D8B 0%, #90A4AE 100%)
position: relative
&::before
content: '🏢'
position: absolute
top: 50%
left: 50%
transform: translate(-50%, -50%)
font-size: 32rpx
.service-icon-14
background: linear-gradient(135deg, #E91E63 0%, #F06292 100%)
position: relative
&::before
content: '💡'
position: absolute
top: 50%
left: 50%
transform: translate(-50%, -50%)
font-size: 32rpx
.service-icon-15
background: linear-gradient(135deg, #795548 0%, #A1887F 100%)
position: relative
&::before
content: '📰'
position: absolute
top: 50%
left: 50%
transform: translate(-50%, -50%)
font-size: 32rpx
.service-icon-16
background: linear-gradient(135deg, #424242 0%, #757575 100%)
position: relative
&::before
content: '⚙️'
position: absolute
top: 50%
left: 50%
transform: translate(-50%, -50%)
font-size: 32rpx
.service-title
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif
font-weight: 500
font-size: 24rpx
color: #333333
text-align: center
line-height: 1.2
white-space: nowrap
overflow: hidden
text-overflow: ellipsis
width: 100%
max-width: 100%
// 吸顶筛选区域占位
.filter-placeholder
height: 140rpx; // 根据筛选区域的实际高度调整包含padding
width: 100%;
/* #ifdef H5 */
height: 0; // H5使用sticky不需要占位
/* #endif */
/* #ifdef MP-WEIXIN */
height: 140rpx; // 小程序需要占位
/* #endif */
.nav-filter
padding: 16rpx 28rpx 0 28rpx
padding: 16rpx 28rpx
box-sizing: border-box;
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
transition: box-shadow 0.3s cubic-bezier(0.4, 0.0, 0.2, 1),
transform 0.3s cubic-bezier(0.4, 0.0, 0.2, 1);
background: #FFFFFF;
z-index: 10;
&.sticky-filter
position: fixed;
top: 0;
left: 0;
right: 0;
width: 100%;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
z-index: 100;
will-change: transform;
transform: translateZ(0);
-webkit-transform: translateZ(0);
/* #ifdef H5 */
position: sticky;
top: 0;
z-index: 100;
/* #endif */
/* #ifdef MP-WEIXIN */
position: fixed;
top: 0;
padding: 16rpx 28rpx;
padding-top: calc(16rpx + env(safe-area-inset-top));
padding-left: calc(28rpx + env(safe-area-inset-left));
padding-right: calc(28rpx + env(safe-area-inset-right));
/* #endif */
.filter-top
display: flex
justify-content: space-between;
.tab-scroll
flex: 1;
overflow: hidden;
margin-right: 20rpx
margin-right: 20rpx;
/* #ifdef MP-WEIXIN */
margin-right: 0;
/* #endif */
white-space: nowrap;
overflow: hidden;
text-overflow: clip;
@@ -661,6 +1182,9 @@ defineExpose({ loadData });
font-size: 32rpx;
color: #666D7F;
line-height: 38rpx;
min-width: 80rpx;
padding: 8rpx 12rpx;
white-space: nowrap;
.filter-bottom
display: flex
justify-content: space-between
@@ -692,9 +1216,12 @@ defineExpose({ loadData });
background: #F4F4F4
flex: 1
overflow: hidden
height: 0; // 确保flex容器正确计算高度
.falls-scroll
width: 100%
height: 100%
// 确保滚动容器可以正常滚动
-webkit-overflow-scrolling: touch;
.falls
padding: 28rpx 28rpx;
.item

View File

@@ -151,7 +151,7 @@ function nextDetail(job) {
const recordData = recommedIndexDb.JobParameter(job);
recommedIndexDb.addRecord(recordData);
}
navTo(`/packageA/pages/post/post?jobId=${btoa(job.jobId)}`);
navTo(`/packageA/pages/post/post?jobId=${encodeURIComponent(job.jobId)}`);
}
function nextVideo(job) {

View File

@@ -6,14 +6,13 @@
<IndexOne @onShowTabbar="changeShowTabbar" />
</view>
<Tabbar :currentpage="0"></Tabbar>
<!-- 统一使用系统tabBar -->
</view>
</view>
</template>
<script setup>
import { reactive, inject, watch, ref, onMounted } from 'vue';
import Tabbar from '@/components/tabbar/midell-box.vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import IndexOne from './components/index-one.vue';
// import IndexTwo from './components/index-two.vue';
@@ -22,7 +21,7 @@ import { useReadMsg } from '@/stores/useReadMsg';
const { unreadCount } = storeToRefs(useReadMsg());
onLoad(() => {
useReadMsg().fetchMessages();
// useReadMsg().fetchMessages();
});
</script>

454
pages/job/publishJob.vue Normal file
View File

@@ -0,0 +1,454 @@
<template>
<view class="publish-job-page">
<!-- 头部导航 -->
<view class="header">
<view class="header-left" @click="goBack">
<image src="@/static/icon/back.png" class="back-icon"></image>
</view>
<view class="header-title">发布岗位</view>
<view class="header-right"></view>
</view>
<!-- 主要内容 -->
<view class="content">
<!-- 岗位基本信息 -->
<view class="section">
<view class="section-title">岗位基本信息</view>
<view class="form-group">
<view class="label">岗位名称 *</view>
<input
class="input"
placeholder="请输入岗位名称"
v-model="formData.jobTitle"
/>
</view>
<view class="form-group">
<view class="label">岗位类型 *</view>
<picker
mode="selector"
:range="jobTypes"
@change="onJobTypeChange"
class="picker"
>
<view class="picker-text">{{ selectedJobType || '请选择岗位类型' }}</view>
</picker>
</view>
<view class="form-group">
<view class="label">工作地点 *</view>
<input
class="input"
placeholder="请输入工作地点"
v-model="formData.workLocation"
/>
</view>
</view>
<!-- 薪资待遇 -->
<view class="section">
<view class="section-title">薪资待遇</view>
<view class="salary-row">
<view class="form-group">
<view class="label">最低薪资</view>
<input
class="input salary-input"
placeholder="0"
type="number"
v-model="formData.minSalary"
/>
</view>
<view class="salary-separator">-</view>
<view class="form-group">
<view class="label">最高薪资</view>
<input
class="input salary-input"
placeholder="0"
type="number"
v-model="formData.maxSalary"
/>
</view>
</view>
<view class="form-group">
<view class="label">薪资单位</view>
<picker
mode="selector"
:range="salaryUnits"
@change="onSalaryUnitChange"
class="picker"
>
<view class="picker-text">{{ selectedSalaryUnit || '请选择薪资单位' }}</view>
</picker>
</view>
</view>
<!-- 任职要求 -->
<view class="section">
<view class="section-title">任职要求</view>
<view class="form-group">
<view class="label">学历要求</view>
<picker
mode="selector"
:range="educationLevels"
@change="onEducationChange"
class="picker"
>
<view class="picker-text">{{ selectedEducation || '请选择学历要求' }}</view>
</picker>
</view>
<view class="form-group">
<view class="label">工作经验</view>
<picker
mode="selector"
:range="experienceLevels"
@change="onExperienceChange"
class="picker"
>
<view class="picker-text">{{ selectedExperience || '请选择工作经验' }}</view>
</picker>
</view>
<view class="form-group">
<view class="label">招聘人数</view>
<input
class="input"
placeholder="请输入招聘人数"
type="number"
v-model="formData.recruitCount"
/>
</view>
</view>
<!-- 岗位描述 -->
<view class="section">
<view class="section-title">岗位描述</view>
<view class="form-group">
<view class="label">岗位职责</view>
<textarea
class="textarea"
placeholder="请详细描述岗位职责和工作内容"
v-model="formData.jobDescription"
></textarea>
</view>
<view class="form-group">
<view class="label">任职要求</view>
<textarea
class="textarea"
placeholder="请描述对候选人的具体要求"
v-model="formData.jobRequirements"
></textarea>
</view>
</view>
<!-- 联系方式 -->
<view class="section">
<view class="section-title">联系方式</view>
<view class="form-group">
<view class="label">联系人</view>
<input
class="input"
placeholder="请输入联系人姓名"
v-model="formData.contactPerson"
/>
</view>
<view class="form-group">
<view class="label">联系电话</view>
<input
class="input"
placeholder="请输入联系电话"
v-model="formData.contactPhone"
/>
</view>
<view class="form-group">
<view class="label">联系邮箱</view>
<input
class="input"
placeholder="请输入联系邮箱"
v-model="formData.contactEmail"
/>
</view>
</view>
</view>
<!-- 底部操作按钮 -->
<view class="footer">
<view class="btn-group">
<button class="btn btn-cancel" @click="goBack">取消</button>
<button class="btn btn-publish" @click="publishJob">发布岗位</button>
</view>
</view>
</view>
</template>
<script setup>
import { ref, reactive } from 'vue';
// 表单数据
const formData = reactive({
jobTitle: '',
workLocation: '',
minSalary: '',
maxSalary: '',
recruitCount: '',
jobDescription: '',
jobRequirements: '',
contactPerson: '',
contactPhone: '',
contactEmail: ''
});
// 选择器数据
const jobTypes = ['技术类', '销售类', '管理类', '服务类', '其他'];
const salaryUnits = ['元/月', '元/年', '元/小时'];
const educationLevels = ['不限', '高中', '大专', '本科', '硕士', '博士'];
const experienceLevels = ['不限', '应届毕业生', '1-3年', '3-5年', '5-10年', '10年以上'];
// 选中的值
const selectedJobType = ref('');
const selectedSalaryUnit = ref('');
const selectedEducation = ref('');
const selectedExperience = ref('');
// 选择器事件处理
const onJobTypeChange = (e) => {
selectedJobType.value = jobTypes[e.detail.value];
};
const onSalaryUnitChange = (e) => {
selectedSalaryUnit.value = salaryUnits[e.detail.value];
};
const onEducationChange = (e) => {
selectedEducation.value = educationLevels[e.detail.value];
};
const onExperienceChange = (e) => {
selectedExperience.value = experienceLevels[e.detail.value];
};
// 返回上一页
const goBack = () => {
uni.navigateBack();
};
// 发布岗位
const publishJob = () => {
// 简单验证
if (!formData.jobTitle) {
uni.showToast({
title: '请输入岗位名称',
icon: 'none'
});
return;
}
if (!formData.workLocation) {
uni.showToast({
title: '请输入工作地点',
icon: 'none'
});
return;
}
// 模拟发布成功
uni.showLoading({
title: '发布中...'
});
setTimeout(() => {
uni.hideLoading();
uni.showToast({
title: '发布成功',
icon: 'success'
});
// 延迟返回
setTimeout(() => {
goBack();
}, 1500);
}, 2000);
};
</script>
<style lang="scss" scoped>
.publish-job-page {
min-height: 100vh;
background-color: #f5f5f5;
padding-bottom: 120rpx;
}
.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-icon {
width: 40rpx;
height: 40rpx;
}
}
.header-title {
font-size: 36rpx;
font-weight: 600;
color: #333;
}
.header-right {
width: 60rpx;
}
}
.content {
padding: 0;
}
.section {
background: #fff;
border-radius: 0;
padding: 30rpx;
margin-bottom: 20rpx;
width: 100%;
&:last-child {
margin-bottom: 0;
}
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 30rpx;
padding-bottom: 20rpx;
border-bottom: 2rpx solid #f0f0f0;
}
}
.form-group {
margin-bottom: 30rpx;
&:last-child {
margin-bottom: 0;
}
.label {
font-size: 28rpx;
color: #333;
margin-bottom: 15rpx;
font-weight: 500;
}
.input {
width: 100%;
height: 80rpx;
background: #fff;
border: none;
border-bottom: 2rpx solid #e0e0e0;
border-radius: 0;
padding: 0 0 10rpx 0;
font-size: 28rpx;
color: #333;
&:focus {
border-bottom-color: #256BFA;
background: #fff;
}
}
.textarea {
width: 100%;
min-height: 120rpx;
background: #fff;
border: none;
border-bottom: 2rpx solid #e0e0e0;
border-radius: 0;
padding: 20rpx 0 10rpx 0;
font-size: 28rpx;
color: #333;
&:focus {
border-bottom-color: #256BFA;
background: #fff;
}
}
.picker {
width: 100%;
height: 80rpx;
background: #fff;
border: none;
border-bottom: 2rpx solid #e0e0e0;
border-radius: 0;
display: flex;
align-items: center;
padding: 0 0 10rpx 0;
.picker-text {
font-size: 28rpx;
color: #333;
}
}
}
.salary-row {
display: flex;
align-items: center;
gap: 20rpx;
.form-group {
flex: 1;
margin-bottom: 0;
}
.salary-separator {
font-size: 32rpx;
color: #666;
margin-top: 40rpx;
}
.salary-input {
text-align: center;
}
}
.footer {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: #fff;
padding: 20rpx 30rpx;
border-top: 1rpx solid #eee;
.btn-group {
display: flex;
gap: 20rpx;
.btn {
flex: 1;
height: 80rpx;
border-radius: 12rpx;
font-size: 32rpx;
font-weight: 500;
border: none;
&.btn-cancel {
background: #f5f5f5;
color: #666;
}
&.btn-publish {
background: #256BFA;
color: #fff;
}
}
}
}
</style>

View File

@@ -97,7 +97,7 @@
</uni-popup>
</view>
<template #footer>
<Tabbar :currentpage="4"></Tabbar>
<!-- 统一使用系统tabBar -->
</template>
</AppLayout>
</template>
@@ -105,7 +105,6 @@
<script setup>
import { reactive, inject, watch, ref, onMounted } from 'vue';
import { storeToRefs } from 'pinia';
import Tabbar from '@/components/tabbar/midell-box.vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
const { $api, navTo } = inject('globalFunction');
import useUserStore from '@/stores/useUserStore';

View File

@@ -28,7 +28,7 @@
</swiper>
</view>
<Tabbar :currentpage="3"></Tabbar>
<!-- 统一使用系统tabBar -->
</view>
</view>
</template>

View File

@@ -166,7 +166,7 @@ onLoad(() => {
});
function navToPost(jobId) {
navTo(`/packageA/pages/post/post?jobId=${btoa(jobId)}`);
navTo(`/packageA/pages/post/post?jobId=${encodeURIComponent(jobId)}`);
}
async function loadData() {

View File

@@ -1,5 +1,5 @@
<template>
<view class="container">
<view class="container safe-area-top">
<view>
<view class="top">
<image class="btnback button-click" src="@/static/icon/back.png" @click="navBack"></image>
@@ -135,7 +135,7 @@ function nextDetail(job) {
const recordData = recommedIndexDb.JobParameter(job);
recommedIndexDb.addRecord(recordData);
}
navTo(`/packageA/pages/post/post?jobId=${btoa(job.jobId)}`);
navTo(`/packageA/pages/post/post?jobId=${encodeURIComponent(job.jobId)}`);
}
function nextVideo(job) {

218
pages/test/userTypeTest.vue Normal file
View File

@@ -0,0 +1,218 @@
<template>
<view class="test-page">
<view class="header">
<text class="title">用户类型测试页面</text>
</view>
<view class="content">
<view class="current-info">
<text class="label">当前用户类型</text>
<text class="value">{{ getCurrentTypeLabel() }}</text>
</view>
<view class="type-switcher">
<text class="section-title">切换用户类型</text>
<view class="buttons">
<button
v-for="(type, index) in userTypes"
:key="index"
:class="['btn', { active: currentUserType === type.value }]"
@click="switchUserType(type.value)"
>
{{ type.label }}
</button>
</view>
</view>
<view class="navigation-info">
<text class="section-title">底部导航栏配置</text>
<view class="nav-items">
<view
v-for="(item, index) in tabbarConfig"
:key="index"
class="nav-item"
>
<text class="nav-text">{{ item.text || 'AI助手' }}</text>
</view>
</view>
</view>
<view class="description">
<text class="desc-title">说明</text>
<text class="desc-text"> 企业用户userType=0显示"发布岗位"导航隐藏"招聘会"</text>
<text class="desc-text"> 其他用户userType=1,2,3显示"招聘会"导航</text>
<text class="desc-text"> 切换用户类型后底部导航栏会自动更新</text>
<text class="desc-text"> 企业用户模式下"招聘会"导航项完全隐藏</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue';
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
const { userInfo } = storeToRefs(useUserStore());
const userTypes = [
{ value: 0, label: '企业用户' },
{ value: 1, label: '求职者' },
{ value: 2, label: '网格员' },
{ value: 3, label: '政府人员' }
];
const currentUserType = computed(() => userInfo.value?.userType || 0);
const switchUserType = (userType) => {
userInfo.value.userType = userType;
uni.setStorageSync('userInfo', userInfo.value);
uni.showToast({
title: `已切换到${getCurrentTypeLabel()}`,
icon: 'success'
});
};
const getCurrentTypeLabel = () => {
const type = userTypes.find(t => t.value === currentUserType.value);
return type ? type.label : '未知';
};
const tabbarConfig = computed(() => {
const baseItems = [
{ text: '首页' },
{ text: currentUserType.value === 0 ? '发布岗位' : '招聘会' },
{ text: '' }, // AI助手
{ text: '消息' },
{ text: '我的' }
];
return baseItems;
});
</script>
<style lang="scss" scoped>
.test-page {
padding: 40rpx;
background: #f5f5f5;
min-height: 100vh;
}
.header {
text-align: center;
margin-bottom: 40rpx;
.title {
font-size: 36rpx;
font-weight: 600;
color: #333;
}
}
.content {
.current-info {
background: #fff;
padding: 30rpx;
border-radius: 10rpx;
margin-bottom: 30rpx;
.label {
font-size: 28rpx;
color: #666;
}
.value {
font-size: 28rpx;
font-weight: 600;
color: #256BFA;
margin-left: 10rpx;
}
}
.type-switcher {
background: #fff;
padding: 30rpx;
border-radius: 10rpx;
margin-bottom: 30rpx;
.section-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 20rpx;
display: block;
}
.buttons {
display: flex;
flex-wrap: wrap;
gap: 15rpx;
.btn {
padding: 15rpx 30rpx;
border: 2rpx solid #ddd;
border-radius: 8rpx;
background: #fff;
font-size: 24rpx;
color: #666;
&.active {
background: #256BFA;
color: #fff;
border-color: #256BFA;
}
}
}
}
.navigation-info {
background: #fff;
padding: 30rpx;
border-radius: 10rpx;
margin-bottom: 30rpx;
.section-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 20rpx;
display: block;
}
.nav-items {
display: flex;
justify-content: space-around;
.nav-item {
text-align: center;
.nav-text {
font-size: 24rpx;
color: #666;
}
}
}
}
.description {
background: #fff;
padding: 30rpx;
border-radius: 10rpx;
.desc-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 15rpx;
display: block;
}
.desc-text {
font-size: 24rpx;
color: #666;
line-height: 1.6;
display: block;
margin-bottom: 10rpx;
}
}
}
</style>