岗位发布开发

This commit is contained in:
冯辉
2025-10-23 17:16:16 +08:00
parent 20f2038f8c
commit b72b7dd7d6
25 changed files with 3402 additions and 327 deletions

View File

@@ -3,17 +3,20 @@ import { reactive, inject, onMounted } from 'vue';
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app';
import useUserStore from './stores/useUserStore';
import useDictStore from './stores/useDictStore';
import { tabbarManager } from './utils/tabbarManager';
const { $api, navTo, appendScriptTagElement } = inject('globalFunction');
import config from '@/config.js';
onLaunch((options) => {
useUserStore().initSeesionId(); //更新
useDictStore().getDictData();
// uni.hideTabBar();
// 尝试从缓存恢复用户信息
// 尝试从缓存恢复用户信息
const restored = useUserStore().restoreUserInfo();
// 用户信息恢复后再初始化自定义tabbar
tabbarManager.initTabBar();
if (restored) {
// 如果成功恢复用户信息验证token是否有效
let token = uni.getStorageSync('token') || '';

View File

@@ -0,0 +1,281 @@
<template>
<view class="custom-tabbar">
<view
class="tabbar-item"
v-for="(item, index) in tabbarList"
:key="index"
@click="switchTab(item, index)"
>
<view class="tabbar-icon">
<image
:src="currentItem === item.id ? item.selectedIconPath : item.iconPath"
mode="aspectFit"
/>
</view>
<view class="badge" v-if="item.badge && item.badge > 0">{{ item.badge }}</view>
<view class="tabbar-text" :class="{ 'active': currentItem === item.id }">
{{ item.text }}
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, watch, onMounted } from 'vue';
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
import { useReadMsg } from '@/stores/useReadMsg';
const props = defineProps({
currentPage: {
type: Number,
default: 0
}
});
const userStore = useUserStore();
const { userInfo } = storeToRefs(userStore);
const readMsg = useReadMsg();
const currentItem = ref(props.currentPage);
// 监听props变化
watch(() => props.currentPage, (newPage) => {
currentItem.value = newPage;
});
// 生成tabbar配置的函数
const generateTabbarList = () => {
const baseItems = [
{
id: 0,
text: '职位',
path: '/pages/index/index',
iconPath: '/static/tabbar/calendar.png',
selectedIconPath: '/static/tabbar/calendared.png',
centerItem: false,
badge: readMsg.badges[0]?.count || 0,
},
{
id: 2,
text: 'AI+',
path: '/pages/chat/chat',
iconPath: '/static/tabbar/logo3.png',
selectedIconPath: '/static/tabbar/logo3.png',
centerItem: true,
badge: readMsg.badges[2]?.count || 0,
},
{
id: 3,
text: '消息',
path: '/pages/msglog/msglog',
iconPath: '/static/tabbar/chat4.png',
selectedIconPath: '/static/tabbar/chat4ed.png',
centerItem: false,
badge: readMsg.badges[3]?.count || 0,
},
{
id: 4,
text: '我的',
path: '/pages/mine/mine',
iconPath: '/static/tabbar/mine.png',
selectedIconPath: '/static/tabbar/mined.png',
centerItem: false,
badge: readMsg.badges[4]?.count || 0,
},
];
// 获取用户类型统一使用isCompanyUser字段0=企业用户1=求职者, 3=网格员)
// 优先从store获取如果为空则直接从缓存获取
const cachedUserInfo = uni.getStorageSync('userInfo') || {};
console.log('完整userInfo对象:', userInfo.value);
console.log('缓存中的userInfo:', cachedUserInfo);
// 获取isCompanyUser字段
const storeIsCompanyUser = userInfo.value?.isCompanyUser;
const cachedIsCompanyUser = cachedUserInfo.isCompanyUser;
console.log('store中的isCompanyUser:', storeIsCompanyUser);
console.log('缓存中的isCompanyUser:', cachedIsCompanyUser);
// 获取用户类型的逻辑:
// 1. 优先使用store中的isCompanyUser
// 2. 如果store中没有使用缓存中的isCompanyUser
// 3. 最后默认为1求职者
const userType = Number(storeIsCompanyUser !== undefined ? storeIsCompanyUser : (cachedIsCompanyUser !== undefined ? cachedIsCompanyUser : 1));
if (userType === 0 || userType === 2) {
// 企业用户:显示发布岗位
baseItems.splice(1, 0, {
id: 1,
text: '发布岗位',
path: '/pages/job/publishJob',
iconPath: '/static/tabbar/post.png',
selectedIconPath: '/static/tabbar/posted.png',
centerItem: false,
badge: 0,
});
} else {
// 求职者用户(包括未登录状态):显示招聘会
baseItems.splice(1, 0, {
id: 1,
text: '招聘会',
path: '/pages/careerfair/careerfair',
iconPath: '/static/tabbar/post.png',
selectedIconPath: '/static/tabbar/posted.png',
centerItem: false,
badge: readMsg.badges[1]?.count || 0,
});
}
return baseItems;
};
// 根据用户类型生成不同的导航栏配置
const tabbarList = computed(() => {
return generateTabbarList();
});
// 强制刷新tabbar的方法
const forceRefresh = () => {
// 触发响应式更新
const cachedUserInfo = uni.getStorageSync('userInfo') || {};
const currentUserType = userInfo.value?.isCompanyUser !== undefined ? userInfo.value.isCompanyUser : (cachedUserInfo.isCompanyUser !== undefined ? cachedUserInfo.isCompanyUser : 1);
console.log('强制刷新tabbar当前用户类型:', currentUserType);
};
// 监听用户类型变化只监听isCompanyUser字段
watch(() => userInfo.value?.isCompanyUser, (newIsCompanyUser, oldIsCompanyUser) => {
console.log('用户类型变化监听:', {
newIsCompanyUser,
oldIsCompanyUser,
userInfo: userInfo.value
});
if (newIsCompanyUser !== oldIsCompanyUser) {
console.log('用户类型发生变化重新生成tabbar:', newIsCompanyUser);
// 强制触发computed重新计算
forceRefresh();
}
}, { immediate: true });
// 监听用户信息变化(包括登录状态)
watch(() => userInfo.value, (newUserInfo, oldUserInfo) => {
console.log('用户信息变化监听:', { newUserInfo, oldUserInfo });
if (newUserInfo !== oldUserInfo) {
console.log('用户信息发生变化重新生成tabbar');
// 强制触发computed重新计算
forceRefresh();
}
}, { immediate: true, deep: true });
// 切换tab
const switchTab = (item, index) => {
// 判断是否为 tabBar 页面
const tabBarPages = [
'/pages/index/index',
'/pages/careerfair/careerfair',
'/pages/chat/chat',
'/pages/msglog/msglog',
'/pages/mine/mine'
];
if (tabBarPages.includes(item.path)) {
// tabBar 页面使用 switchTab
uni.navigateTo({
url: item.path,
});
} else {
// 非 tabBar 页面使用 navigateTo
uni.navigateTo({
url: item.path,
});
}
currentItem.value = item.id;
};
onMounted(() => {
currentItem.value = props.currentPage;
// 调试信息显示当前用户状态和tabbar配置
console.log('CustomTabBar mounted, 用户信息:', userInfo.value);
console.log('当前tabbar配置:', tabbarList.value);
forceRefresh();
});
</script>
<style lang="scss" scoped>
.custom-tabbar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 88rpx;
background-color: #ffffff;
border-top: 1rpx solid #e5e5e5;
display: flex;
align-items: center;
padding-bottom: env(safe-area-inset-bottom);
z-index: 999;
box-shadow: 0 -2rpx 10rpx rgba(0, 0, 0, 0.1);
}
.tabbar-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
color: #5E5F60;
font-size: 22rpx;
position: relative;
cursor: pointer;
}
.tabbar-icon {
width: 44rpx;
height: 44rpx;
margin-bottom: 4rpx;
position: relative;
}
.tabbar-icon image {
width: 100%;
height: 100%;
}
.tabbar-text {
font-size: 20rpx;
line-height: 1;
transition: color 0.3s ease;
}
.tabbar-text.active {
color: #256BFA;
font-weight: 500;
}
.badge {
position: absolute;
top: 4rpx;
right: 20rpx;
min-width: 30rpx;
height: 30rpx;
background-color: #ff4444;
color: #fff;
font-size: 18rpx;
border-radius: 15rpx;
text-align: center;
line-height: 30rpx;
padding: 0 10rpx;
transform: scale(0.8);
}
/* 中间按钮特殊样式 */
.tabbar-item:has(.center-item) {
.tabbar-icon {
width: 60rpx;
height: 60rpx;
margin-bottom: 0;
}
}
</style>

View File

@@ -31,13 +31,13 @@ const userTypes = [
{ value: 3, label: '政府人员' }
];
const currentUserType = computed(() => userInfo.value?.userType || 0);
const currentUserType = computed(() => userInfo.value?.isCompanyUser !== undefined ? userInfo.value.isCompanyUser : 0);
const switchUserType = (userType) => {
console.log('切换用户类型:', userType);
console.log('切换前 userInfo:', userInfo.value);
userInfo.value.userType = userType;
userInfo.value.isCompanyUser = userType;
console.log('切换后 userInfo:', userInfo.value);

View File

@@ -13,7 +13,30 @@
<view class="btn-confirm" @click="confirm">确认</view>
</view>
<view class="popup-list">
<!-- 多选模式 -->
<view v-if="multiSelect" class="multi-select-list">
<view v-if="!processedListData[0] || processedListData[0].length === 0" class="empty-tip">
暂无数据
</view>
<view
v-else
class="skill-tags-container"
>
<view
v-for="(item, index) in processedListData[0]"
:key="index"
class="skill-tag"
:class="{ 'skill-tag-active': selectedValues.includes(item[this.rowKey]) }"
@click.stop="toggleSelect(item)"
@touchstart.stop="toggleSelect(item)"
>
<text class="skill-tag-text">{{ getLabel(item) }}</text>
</view>
</view>
</view>
<!-- 单选模式 -->
<picker-view
v-else
indicator-style="height: 84rpx;"
:value="selectedIndex"
@change="bindChange"
@@ -54,6 +77,8 @@ export default {
rowKey: 'value',
selectedItems: [],
unit: '',
multiSelect: false,
selectedValues: [],
};
},
computed: {
@@ -66,6 +91,13 @@ export default {
});
});
},
// 计算选中的项目
computedSelectedItems() {
if (!this.multiSelect) return this.selectedItems;
return this.processedListData[0] ? this.processedListData[0].filter(item =>
this.selectedValues.includes(item[this.rowKey])
) : [];
},
},
methods: {
open(newConfig = {}) {
@@ -80,7 +112,10 @@ export default {
rowKey = 'value',
maskClick = false,
defaultIndex = [],
multiSelect = false,
defaultValues = [],
} = newConfig;
this.reset();
if (title) this.title = title;
if (typeof success === 'function') this.confirmCallback = success;
@@ -92,10 +127,16 @@ export default {
this.rowKey = rowKey;
this.maskClick = maskClick;
this.unit = unit;
this.multiSelect = multiSelect;
this.selectedIndex =
defaultIndex.length === this.listData.length ? defaultIndex : new Array(this.listData.length).fill(0);
this.selectedItems = this.selectedIndex.map((val, index) => this.processedListData[index][val]);
if (multiSelect) {
this.selectedValues = defaultValues || [];
} else {
this.selectedIndex =
defaultIndex.length === this.listData.length ? defaultIndex : new Array(this.listData.length).fill(0);
this.selectedItems = this.selectedIndex.map((val, index) => this.processedListData[index][val]);
}
this.$nextTick(() => {
this.$refs.popup.open();
});
@@ -117,6 +158,22 @@ export default {
getLabel(item) {
return item?.[this.rowLabel] ?? '';
},
toggleSelect(item) {
if (!item || !this.rowKey || !item[this.rowKey]) {
return;
}
const value = item[this.rowKey];
const index = this.selectedValues.indexOf(value);
if (index > -1) {
// 取消选中
this.selectedValues.splice(index, 1);
} else {
// 选中
this.selectedValues.push(value);
}
},
setColunm(index, list) {
if (index > this.listData.length) {
return console.warn('最长' + this.listData.length);
@@ -135,7 +192,14 @@ export default {
}
try {
const result = await callback(this.selectedIndex, this.selectedItems); // 无论是 async 还是返回 Promise 的函数都可以 await
let result;
if (this.multiSelect) {
// 多选模式:传递 selectedValues 和 selectedItems
result = await callback(this.selectedValues, this.computedSelectedItems);
} else {
// 单选模式:传递 selectedIndex 和 selectedItems
result = await callback(this.selectedIndex, this.selectedItems);
}
if (result !== false) {
this.$refs.popup.close();
}
@@ -154,6 +218,8 @@ export default {
this.rowKey = 'value';
this.selectedItems = [];
this.unit = '';
this.multiSelect = false;
this.selectedValues = [];
},
},
};
@@ -224,10 +290,83 @@ export default {
color: #666d7f;
line-height: 38rpx;
}
.btn-confirm {
.btn-confirm {
font-weight: 400;
font-size: 32rpx;
color: #256bfa;
}
}
.multi-select-list {
padding: 20rpx 30rpx;
max-height: calc(60vh - 120rpx);
overflow-y: auto;
}
.empty-tip {
text-align: center;
padding: 60rpx 0;
color: #999999;
font-size: 28rpx;
}
.skill-tags-container {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
align-items: flex-start;
}
.skill-tag {
display: inline-flex;
align-items: center;
padding: 12rpx 20rpx;
border-radius: 20rpx;
background-color: #f8f9fa;
border: 2rpx solid #e8eaee;
cursor: pointer;
transition: all 0.3s ease;
font-size: 24rpx;
color: #333333;
white-space: nowrap;
user-select: none;
&:hover {
background-color: #e9ecef;
border-color: #d0d0d0;
transform: translateY(-1rpx);
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
&:active {
transform: translateY(0);
background-color: #dee2e6;
}
.skill-tag-text {
font-size: 24rpx;
color: inherit;
line-height: 1.2;
font-weight: 400;
font-size: 32rpx;
color: #256bfa;
}
&.skill-tag-active {
background-color: #256bfa;
border-color: #256bfa;
color: #ffffff;
box-shadow: 0 2rpx 8rpx rgba(37, 107, 250, 0.3);
.skill-tag-text {
color: #ffffff;
font-weight: 500;
}
&:hover {
background-color: #1e5ce6;
border-color: #1e5ce6;
transform: translateY(-1rpx);
box-shadow: 0 4rpx 12rpx rgba(37, 107, 250, 0.4);
}
}
}
</style>

View File

@@ -10,7 +10,7 @@
>
<image :src="currentItem == item.id ? item.selectedIconPath : item.iconPath"></image>
</view>
<view class="badge" v-if="item.badge">{{ item.badge }}</view>
<view class="badge" v-if="item.badge && item.badge > 0">{{ item.badge }}</view>
<view class="item-bottom" :class="[currentItem == item.id ? 'item-active' : '']">
<text>{{ item.text }}</text>
</view>
@@ -19,7 +19,7 @@
</template>
<script setup>
import { ref, onMounted, computed } from 'vue';
import { ref, onMounted, computed, watch, nextTick } from 'vue';
import { useReadMsg } from '@/stores/useReadMsg';
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
@@ -36,35 +36,60 @@ const readMsg = useReadMsg();
const { userInfo } = storeToRefs(useUserStore());
const currentItem = ref(0);
// 根据用户类型生成不同的导航栏配置
const tabbarList = computed(() => {
// 监听用户类型变化重新生成tabbar配置
watch(() => userInfo.value?.isCompanyUser, (newIsCompanyUser, oldIsCompanyUser) => {
console.log('midell-box用户类型变化监听:', { newIsCompanyUser, oldIsCompanyUser, userInfo: userInfo.value });
if (newIsCompanyUser !== oldIsCompanyUser) {
console.log('用户类型发生变化重新生成tabbar:', newIsCompanyUser);
// tabbarList是computed会自动重新计算
// 强制触发响应式更新
nextTick(() => {
console.log('tabbar配置已更新:', tabbarList.value);
});
}
}, { immediate: true });
// 监听用户信息变化(包括登录状态)
watch(() => userInfo.value, (newUserInfo, oldUserInfo) => {
console.log('midell-box用户信息变化监听:', { newUserInfo, oldUserInfo });
if (newUserInfo !== oldUserInfo) {
console.log('用户信息发生变化重新生成tabbar');
// 强制触发响应式更新
nextTick(() => {
console.log('tabbar配置已更新:', tabbarList.value);
});
}
}, { immediate: true, deep: true });
// 生成tabbar配置的函数
const generateTabbarList = () => {
const baseItems = [
{
id: 0,
text: '首页',
text: '职位2',
path: '/pages/index/index',
iconPath: '../../static/tabbar/calendar.png',
selectedIconPath: '../../static/tabbar/calendared.png',
centerItem: false,
badge: readMsg.badges[0].count,
badge: readMsg.badges[0]?.count || 0,
},
{
id: 2,
text: '',
text: 'AI+',
path: '/pages/chat/chat',
iconPath: '../../static/tabbar/logo3.png',
selectedIconPath: '../../static/tabbar/logo3.png',
centerItem: true,
badge: readMsg.badges[2].count,
badge: readMsg.badges[2]?.count || 0,
},
{
id: 3,
text: '消息',
text: '消息2',
path: '/pages/msglog/msglog',
iconPath: '../../static/tabbar/chat4.png',
selectedIconPath: '../../static/tabbar/chat4ed.png',
centerItem: false,
badge: readMsg.badges[3].count,
badge: readMsg.badges[3]?.count || 0,
},
{
id: 4,
@@ -73,12 +98,12 @@ const tabbarList = computed(() => {
iconPath: '../../static/tabbar/mine.png',
selectedIconPath: '../../static/tabbar/mined.png',
centerItem: false,
badge: readMsg.badges[4].count,
badge: readMsg.badges[4]?.count || 0,
},
];
// 根据用户类型添加不同的导航项
const userType = userInfo.value?.userType || 0;
// 根据用户类型添加不同的导航项,未登录时默认为求职者
const userType = userInfo.value?.isCompanyUser !== undefined ? userInfo.value.isCompanyUser : 1;
if (userType === 0) {
// 企业用户:显示发布岗位,隐藏招聘会
@@ -86,13 +111,13 @@ const tabbarList = computed(() => {
id: 1,
text: '发布岗位',
path: '/pages/job/publishJob',
iconPath: '../../static/tabbar/publish-job.svg',
selectedIconPath: '../../static/tabbar/publish-job-selected.svg',
iconPath: '../../static/tabbar/post.png',
selectedIconPath: '../../static/tabbar/posted.png',
centerItem: false,
badge: 0,
});
} else {
// 普通用户、网格员、政府人员:显示招聘会
// 求职者用户(包括未登录状态):显示招聘会
baseItems.splice(1, 0, {
id: 1,
text: '招聘会',
@@ -100,11 +125,16 @@ const tabbarList = computed(() => {
iconPath: '../../static/tabbar/post.png',
selectedIconPath: '../../static/tabbar/posted.png',
centerItem: false,
badge: readMsg.badges[1].count,
badge: readMsg.badges[1]?.count || 0,
});
}
return baseItems;
};
// 根据用户类型生成不同的导航栏配置
const tabbarList = computed(() => {
return generateTabbarList();
});
onMounted(() => {

View File

@@ -102,6 +102,7 @@
<script setup>
import { ref, inject } from 'vue';
import useUserStore from '@/stores/useUserStore';
import { tabbarManager } from '@/utils/tabbarManager';
const { $api } = inject('globalFunction');
const { loginSetToken } = useUserStore();
@@ -162,22 +163,33 @@ const getPhoneNumber = (e) => {
userType: userType.value
}, 'post').then((resData) => {
uni.hideLoading();
console.log(resData, 'resume.idCard');
if (resData.token) {
// 登录成功存储token
loginSetToken(resData.token).then((resume) => {
// 更新用户类型到缓存
if (resData.isCompanyUser !== undefined) {
console.log(resData.isCompanyUser, 'resData.isCompanyUser');
const userInfo = uni.getStorageSync('userInfo') || {};
userInfo.isCompanyUser = Number(resData.isCompanyUser); // 0-企业用户1-求职者
uni.setStorageSync('userInfo', userInfo);
}
$api.msg('登录成功');
// 刷新tabbar以显示正确的用户类型
tabbarManager.refreshTabBar();
close();
emit('success');
// 根据用户类型跳转到不同的信息补全页面
if (!resume.data.jobTitleId) {
if (userType.value === 1) {
if (!resume.jobTitleId) {
console.log(resume, 'resume.idCard');
if (userType.value === 1 && !resData.idCard) {
// 求职者跳转到个人信息补全页面
uni.navigateTo({
url: '/pages/complete-info/complete-info?step=1'
});
} else if (userType.value === 0) {
} else if (userType.value === 0 && !resData.idCard) {
// 招聘者跳转到企业信息补全页面
uni.navigateTo({
url: '/pages/complete-info/company-info'
@@ -233,6 +245,15 @@ const wxLogin = () => {
if (resData.token) {
loginSetToken(resData.token).then((resume) => {
console.log(resData, 'resData.isCompanyUser');
// 更新用户类型到缓存
if (resData.isCompanyUser) {
console.log(resData.isCompanyUser, 'resData.isCompanyUser');
const userInfo = uni.getStorageSync('userInfo') || {};
userInfo.isCompanyUser = Number(resData.isCompanyUser); // 0-企业用户1-求职者
uni.setStorageSync('userInfo', userInfo);
}
$api.msg('登录成功');
close();
emit('success');
@@ -285,7 +306,16 @@ const wxLogin = () => {
}, 'post').then((resData) => {
if (resData.token) {
loginSetToken(resData.token).then((resume) => {
// 更新用户类型到缓存
if (resData.isCompanyUser !== undefined) {
const userInfo = uni.getStorageSync('userInfo') || {};
userInfo.isCompanyUser = resData.isCompanyUser ? 0 : 1; // 0-企业用户1-求职者
uni.setStorageSync('userInfo', userInfo);
}
$api.msg('登录成功');
// 刷新tabbar以显示正确的用户类型
tabbarManager.refreshTabBar();
close();
emit('success');
@@ -330,7 +360,16 @@ const testLogin = () => {
uni.hideLoading();
loginSetToken(resData.token).then((resume) => {
// 更新用户类型到缓存
if (resData.isCompanyUser !== undefined) {
const userInfo = uni.getStorageSync('userInfo') || {};
userInfo.isCompanyUser = resData.isCompanyUser ? 0 : 1; // 0-企业用户1-求职者
uni.setStorageSync('userInfo', userInfo);
}
$api.msg('测试登录成功');
// 刷新tabbar以显示正确的用户类型
tabbarManager.refreshTabBar();
close();
emit('success');

View File

@@ -0,0 +1,187 @@
# 企业搜索功能实现说明
## 功能概述
根据用户类型对发布岗位页面的"招聘公司"输入框进行不同的交互处理:
- **企业用户**:直接输入公司名称
- **网格员**:点击输入框跳转到企业搜索页面,支持模糊查询
## 实现细节
### 1. 用户类型判断
通过 `userInfo.isCompanyUser` 字段判断用户类型:
- `0`: 企业用户
- `1`: 求职者
- `2`: 网格员
- `3`: 政府人员
### 2. 页面修改
#### 发布岗位页面 (`pages/job/publishJob.vue`)
**模板修改:**
```vue
<!-- 企业用户直接输入 -->
<input
v-if="isCompanyUser"
class="input"
placeholder="请输入公司名称"
v-model="formData.companyName"
/>
<!-- 网格员点击跳转到搜索页面 -->
<view
v-else
class="company-selector"
@click="openCompanySearch"
>
<view class="selector-text" :class="{ 'placeholder': !formData.companyName }">
{{ formData.companyName || '请选择企业' }}
</view>
<view class="selector-icon">
<view class="arrow-icon">></view>
</view>
</view>
```
**脚本修改:**
- 添加用户类型判断逻辑
- 添加打开企业搜索页面的方法
- 添加页面显示时处理返回数据的逻辑
#### 企业搜索页面 (`pages/job/companySearch.vue`)
**功能特性:**
- 搜索框支持实时输入
- 防抖节流500ms延迟执行搜索
- 调用接口:`/app/company/likeList`,参数:`name`
- 支持企业选择和数据回传
- 空状态和加载状态处理
**核心代码:**
```javascript
// 防抖搜索
const onSearchInput = () => {
if (debounceTimer) {
clearTimeout(debounceTimer);
}
debounceTimer = setTimeout(() => {
if (searchKeyword.value.trim()) {
searchCompanies();
} else {
searchResults.value = [];
}
}, 500);
};
// 搜索企业
const searchCompanies = async () => {
const response = await createRequest('/app/company/likeList', {
name: searchKeyword.value.trim()
}, 'GET', false);
if (response.code === 200) {
searchResults.value = response.data || [];
}
};
// 选择企业
const selectCompany = (company) => {
uni.navigateBack({
success: () => {
getApp().globalData = getApp().globalData || {};
getApp().globalData.selectedCompany = company;
}
});
};
```
### 3. 数据传递
使用全局数据传递选中的企业信息:
**企业搜索页面:**
```javascript
// 选择企业后设置全局数据
getApp().globalData.selectedCompany = company;
```
**发布岗位页面:**
```javascript
// 页面显示时检查全局数据
onShow(() => {
const app = getApp();
if (app.globalData && app.globalData.selectedCompany) {
const selectedCompany = app.globalData.selectedCompany;
formData.companyName = selectedCompany.name;
formData.companyId = selectedCompany.id;
// 清除全局数据
app.globalData.selectedCompany = null;
}
});
```
### 4. 页面配置
`pages.json` 中添加了企业搜索页面配置:
```json
{
"path": "pages/job/companySearch",
"style": {
"navigationBarTitleText": "选择企业",
"navigationStyle": "custom",
"disableScroll": false,
"enablePullDownRefresh": false,
"backgroundColor": "#f5f5f5"
}
}
```
### 5. 测试页面
创建了测试页面 `pages/test/company-search-test.vue` 用于验证功能:
- 用户类型切换
- 功能说明展示
- 直接跳转到发布岗位页面测试
## 使用说明
### 企业用户
1. 进入发布岗位页面
2. 招聘公司输入框为普通输入框
3. 直接输入公司名称
### 网格员
1. 进入发布岗位页面
2. 点击"招聘公司"输入框
3. 跳转到企业搜索页面
4. 输入企业名称进行搜索(支持防抖)
5. 选择企业后自动返回
6. 企业名称显示在输入框中
## 技术特点
1. **防抖节流**搜索输入500ms延迟避免频繁请求
2. **用户类型判断**:根据 `isCompanyUser` 字段动态显示不同交互
3. **数据传递**:使用全局数据实现页面间数据传递
4. **响应式设计**:支持不同屏幕尺寸
5. **错误处理**:完善的错误提示和空状态处理
## 接口说明
**搜索企业接口:**
- 地址:`/app/company/likeList`
- 方法:`GET`
- 参数:`name` (企业名称)
- 返回:企业列表数据
## 注意事项
1. 确保用户类型字段 `isCompanyUser` 正确设置
2. 搜索接口需要支持模糊查询
3. 企业数据需要包含 `id``name` 字段
4. 防抖时间可根据实际需求调整

View File

@@ -0,0 +1,129 @@
# 自定义TabBar使用说明
## 功能概述
本项目实现了基于用户类型的动态自定义TabBar支持根据用户登录状态和类型显示不同的导航项
- **未登录状态**默认显示求职者tabbar职位 + 招聘会 + AI+ + 消息 + 我的)
- **企业用户userType=0**:显示"发布岗位"导航
- **求职者用户userType=1,2,3**:显示"招聘会"导航
## 实现方案
### 1. 微信小程序原生自定义TabBar
`custom-tab-bar/` 目录下创建了微信小程序原生自定义TabBar组件
- `index.js` - 组件逻辑根据用户类型动态生成TabBar配置
- `index.wxml` - 模板文件
- `index.wxss` - 样式文件
- `index.json` - 组件配置文件
### 2. UniApp兼容的自定义TabBar组件
创建了 `components/CustomTabBar/CustomTabBar.vue` 组件,支持多端兼容:
- 支持微信小程序、H5、APP等多端
- 响应式设计,根据用户类型动态显示
- 支持消息徽章显示
- 支持页面跳转逻辑
### 3. 配置修改
`pages.json` 中启用了自定义TabBar
```json
"tabBar": {
"custom": true,
// ... 其他配置
}
```
## 使用方法
### 1. 在页面中引入自定义TabBar
```vue
<template>
<view class="page">
<!-- 页面内容 -->
<!-- 自定义tabbar -->
<CustomTabBar :currentPage="0" />
</view>
</template>
```
### 2. 用户类型判断
组件会自动从 `useUserStore` 中获取用户信息,根据用户登录状态和 `userInfo.userType` 字段判断用户类型:
```javascript
// 用户类型说明
// 未登录: 默认显示求职者tabbar
// 0: 企业用户 - 显示"发布岗位"
// 1: 求职者 - 显示"招聘会"
// 2: 网格员 - 显示"招聘会"
// 3: 政府人员 - 显示"招聘会"
```
### 3. 动态切换用户类型
当用户登录状态或类型发生变化时TabBar会自动更新
```javascript
// 未登录状态自动显示求职者tabbar
// 登录后根据用户类型显示对应tabbar
// 切换用户类型
userInfo.value.userType = 1; // 切换到求职者
uni.setStorageSync('userInfo', userInfo.value);
// 登出时清除用户信息,自动回到未登录状态
userStore.logOut(false);
```
## 页面配置
### 已配置的页面
- `pages/index/index.vue` - 首页currentPage: 0
- `pages/careerfair/careerfair.vue` - 招聘会页面currentPage: 1
- `pages/chat/chat.vue` - AI+页面currentPage: 2
- `pages/msglog/msglog.vue` - 消息页面currentPage: 3
- `pages/mine/mine.vue` - 我的页面currentPage: 4
### 测试页面
- `pages/test/tabbar-test.vue` - TabBar功能测试页面
## 技术特点
1. **响应式设计**:根据用户类型动态显示不同的导航项
2. **多端兼容**支持微信小程序、H5、APP等平台
3. **消息徽章**:支持显示未读消息数量
4. **页面跳转**智能判断tabBar页面和普通页面的跳转方式
5. **用户类型监听**实时监听用户类型变化并更新TabBar
## 注意事项
1. 确保在 `pages.json` 中设置了 `"custom": true`
2. 每个页面的 `currentPage` 参数需要正确设置
3. 用户类型存储在 `userInfo.userType` 字段中
4. 组件会自动监听用户类型变化并更新显示
## 测试方法
1. 访问测试页面:`/pages/test/tabbar-test`
2. 切换不同的用户类型
3. 观察底部TabBar的变化
4. 测试页面跳转功能
## 故障排除
如果TabBar不显示或显示异常
1. 检查 `pages.json` 中的 `custom: true` 配置
2. 确认用户信息是否正确存储
3. 检查组件是否正确引入
4. 查看控制台是否有错误信息

View File

@@ -68,11 +68,53 @@
"navigationStyle": "custom"
}
},
{
"path": "pages/test/tabbar-test",
"style": {
"navigationBarTitleText": "TabBar测试",
"navigationStyle": "custom"
}
},
{
"path": "pages/test/homepage-test",
"style": {
"navigationBarTitleText": "首页内容测试",
"navigationStyle": "custom"
}
},
{
"path": "pages/test/tabbar-user-type-test",
"style": {
"navigationBarTitleText": "TabBar用户类型测试",
"navigationStyle": "custom"
}
},
{
"path": "pages/test/company-search-test",
"style": {
"navigationBarTitleText": "企业搜索功能测试",
"navigationStyle": "custom"
}
},
{
"path": "pages/job/publishJob",
"style": {
"navigationBarTitleText": "发布岗位",
"navigationStyle": "custom"
"navigationStyle": "custom",
"disableScroll": false,
"enablePullDownRefresh": false,
"onReachBottomDistance": 50,
"backgroundColor": "#f5f5f5"
}
},
{
"path": "pages/job/companySearch",
"style": {
"navigationBarTitleText": "选择企业",
"navigationStyle": "custom",
"disableScroll": false,
"enablePullDownRefresh": false,
"backgroundColor": "#f5f5f5"
}
},
{
@@ -119,8 +161,7 @@
"style": {
"navigationBarTitleText": "职位详情",
"navigationBarBackgroundColor": "#4778EC",
"navigationBarTextStyle": "white",
"navigationStyle": "custom"
"navigationBarTextStyle": "white"
}
}, {
"path": "pages/UnitDetails/UnitDetails",
@@ -254,43 +295,44 @@
}
]
}],
"tabBar": {
"color": "#5E5F60",
"selectedColor": "#256BFA",
"borderStyle": "black",
"backgroundColor": "#ffffff",
"list": [{
"pagePath": "pages/index/index",
"iconPath": "static/tabbar/calendar.png",
"selectedIconPath": "static/tabbar/calendared.png",
"text": "职位"
},
{
"pagePath": "pages/careerfair/careerfair",
"iconPath": "static/tabbar/post.png",
"selectedIconPath": "static/tabbar/posted.png",
"text": "招聘会"
},
{
"pagePath": "pages/chat/chat",
"iconPath": "static/tabbar/logo3.png",
"selectedIconPath": "static/tabbar/logo3.png",
"text": "AI+"
},
{
"pagePath": "pages/msglog/msglog",
"iconPath": "static/tabbar/chat4.png",
"selectedIconPath": "static/tabbar/chat4ed.png",
"text": "消息"
},
{
"pagePath": "pages/mine/mine",
"iconPath": "static/tabbar/mine.png",
"selectedIconPath": "static/tabbar/mined.png",
"text": "我的"
}
]
},
// "tabBar": {
// "custom": true,
// "color": "#5E5F60",
// "selectedColor": "#256BFA",
// "borderStyle": "black",
// "backgroundColor": "#ffffff",
// "list": [{
// "pagePath": "pages/index/index",
// "iconPath": "static/tabbar/calendar.png",
// "selectedIconPath": "static/tabbar/calendared.png",
// "text": "职1"
// },
// {
// "pagePath": "pages/careerfair/careerfair",
// "iconPath": "static/tabbar/post.png",
// "selectedIconPath": "static/tabbar/posted.png",
// "text": "招聘会"
// },
// {
// "pagePath": "pages/chat/chat",
// "iconPath": "static/tabbar/logo3.png",
// "selectedIconPath": "static/tabbar/logo3.png",
// "text": "AI+"
// },
// {
// "pagePath": "pages/msglog/msglog",
// "iconPath": "static/tabbar/chat4.png",
// "selectedIconPath": "static/tabbar/chat4ed.png",
// "text": "消息"
// },
// {
// "pagePath": "pages/mine/mine",
// "iconPath": "static/tabbar/mine.png",
// "selectedIconPath": "static/tabbar/mined.png",
// "text": "我的"
// }
// ]
// },
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "uni-app",
@@ -305,7 +347,8 @@
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue",
"^IconfontIcon$": "@/components/IconfontIcon/IconfontIcon.vue",
"^WxAuthLogin$": "@/components/WxAuthLogin/WxAuthLogin.vue",
"^AppLayout$": "@/components/AppLayout/AppLayout.vue"
"^AppLayout$": "@/components/AppLayout/AppLayout.vue",
"^CustomTabBar$": "@/components/CustomTabBar/CustomTabBar.vue"
}
},
"uniIdRouter": {}

View File

@@ -83,7 +83,8 @@
<empty v-else pdTop="200"></empty>
</scroll-view>
</view>
<!-- 统一使用系统tabBar -->
<!-- 自定义tabbar -->
<CustomTabBar :currentPage="1" />
</view>
</view>
</template>
@@ -93,6 +94,7 @@ import { reactive, inject, watch, ref, onMounted } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import useLocationStore from '@/stores/useLocationStore';
import { storeToRefs } from 'pinia';
import { tabbarManager } from '@/utils/tabbarManager';
const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
const { $api, navTo, cloneDeep } = inject('globalFunction');
const weekList = ref([]);
@@ -124,6 +126,11 @@ onLoad(() => {
getFair('refresh');
});
onShow(() => {
// 更新自定义tabbar选中状态
tabbarManager.updateSelected(1);
});
function toSelectDate() {
navTo('/packageA/pages/selectDate/selectDate', {
query: {

View File

@@ -1,5 +1,7 @@
<template>
<view class="container">
<!-- 自定义tabbar -->
<CustomTabBar :currentPage="2" />
<!-- 抽屉遮罩层 -->
<view v-if="isDrawerOpen" class="overlay" @click="toggleDrawer"></view>
@@ -76,6 +78,7 @@ const { $api, navTo, insertSortData, config } = inject('globalFunction');
import { onLoad, onShow, onHide } from '@dcloudio/uni-app';
import useChatGroupDBStore from '@/stores/userChatGroupStore';
import useUserStore from '@/stores/useUserStore';
import { tabbarManager } from '@/utils/tabbarManager';
import aiPaging from './components/ai-paging.vue';
import { storeToRefs } from 'pinia';
const { isTyping, tabeList, chatSessionID } = storeToRefs(useChatGroupDBStore());
@@ -103,6 +106,8 @@ onShow(() => {
nextTick(() => {
paging.value?.closeFile();
});
// 更新自定义tabbar选中状态
tabbarManager.updateSelected(2);
});
onHide(() => {

View File

@@ -79,7 +79,7 @@
<view class="label">是否是就业见习基地</view>
<view class="input-content">
<text class="input-text" :class="{ placeholder: formData.enterpriseType === null }">
{{ formData.enterpriseType === null ? '请选择' : (formData.enterpriseType ? '是' : '否') }}
{{ formData.enterpriseType === null ? '请选择' : (formData.enterpriseType === 0 ? '是' : '否') }}
</text>
<uni-icons type="arrowright" size="16" color="#999"></uni-icons>
</view>
@@ -241,7 +241,7 @@ const formData = reactive({
legalPersonName: '',
nature: '', // 企业类型
natureText: '', // 企业类型显示文本
enterpriseType: null, // 是否是就业见习基地 (true/false/null)
enterpriseType: null, // 是否是就业见习基地 (0=是, 1=否, null=未选择)
legalIdCard: '', // 法人身份证号
legalPhone: '', // 法人联系方式
industryType: '', // 是否是本地重点发展产业
@@ -445,7 +445,7 @@ const selectEmploymentBase = () => {
uni.showActionSheet({
itemList: ['是', '否'],
success: (res) => {
formData.enterpriseType = res.tapIndex === 0
formData.enterpriseType = res.tapIndex === 0 ? 0 : 1
updateCompletion()
$api.msg('选择成功')
}

View File

@@ -20,7 +20,7 @@
<view class="wxaddress">{{ config.appInfo.areaName }}公共就业和人才服务中心</view>
</view>
</swiper-item>
<swiper-item @touchmove.stop="false">
<swiper-item @touchmove.stop="false">
<view class="content-one">
<view>
<view class="content-title">
@@ -33,17 +33,19 @@
<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 class="content-input" :class="{ 'input-error': nameError }">
<view class="input-titile">姓名</view>
<input
class="input-con2"
v-model="fromValue.name"
maxlength="18"
placeholder="请输入姓名"
@input="validateName"
/>
<view v-if="nameError" class="error-message">{{ nameError }}</view>
</view>
<view class="content-sex">
<view class="sex-titile">求职区域</view>
<view class="content-sex" :class="{ 'input-error': sexError }">
<view class="sex-titile">性别</view>
<view class="sext-ri">
<view
class="sext-box"
@@ -61,6 +63,28 @@
</view>
</view>
</view>
<view v-if="sexError" class="error-message">{{ sexError }}</view>
<view class="content-input" :class="{ 'input-error': ageError }">
<view class="input-titile">年龄</view>
<input
class="input-con2"
v-model="fromValue.age"
maxlength="3"
placeholder="请输入年龄"
@input="validateAge"
/>
<view v-if="ageError" class="error-message">{{ ageError }}</view>
</view>
<view class="content-input" :class="{ 'input-error': experienceError }" @click="changeExperience">
<view class="input-titile">工作经验</view>
<input
class="input-con"
v-model="state.workExperience"
disabled
placeholder="请选择您的工作经验"
/>
<view v-if="experienceError" class="error-message">{{ experienceError }}</view>
</view>
<view class="content-input" @click="changeEducation">
<view class="input-titile">学历</view>
<input class="input-con" v-model="state.educationText" disabled placeholder="本科" />
@@ -69,19 +93,19 @@
<view class="input-titile">身份证</view>
<input
class="input-con2"
v-model="fromValue.idcard"
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 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="next-btn" @tap="nextStep">下一步</view>
</view>
</swiper-item>
<swiper-item @touchmove.stop="false">
<view class="content-one">
<view>
<view class="content-title">
@@ -115,6 +139,27 @@
<view class="nx-item" v-for="item in state.jobsText">{{ item }}</view>
</view>
</view>
<view class="content-input" @click="changeSkillLevel">
<view class="input-titile">技能等级</view>
<input
class="input-con"
v-model="state.skillLevelText"
disabled
placeholder="请选择您的技能等级"
/>
</view>
<view class="content-input" @click="changeSkills">
<view class="input-titile">技能名称</view>
<input
class="input-con"
disabled
v-if="!state.skillsText.length"
placeholder="请选择您的技能名称"
/>
<view class="input-nx" @click="changeSkills" v-else>
<view class="nx-item" v-for="(item, index) in state.skillsText" :key="index">{{ item }}</view>
</view>
</view>
<view class="content-input" @click="changeSalay">
<view class="input-titile">期望薪资</view>
<input
@@ -178,23 +223,33 @@ const state = reactive({
risalay: JSON.parse(JSON.stringify(salay)),
areaText: '',
educationText: '',
experienceText: '',
workExperience: '',
salayText: '',
jobsText: [],
skillLevelText: '',
skillsText: [],
});
const fromValue = reactive({
sex: 1,
sex: null,
education: '4',
salaryMin: 2000,
salaryMax: 2000,
area: 0,
jobTitleId: '',
experience: '1',
idcard: '',
workExperience: '1',
idCard: '',
name: '',
age: '',
skillLevel: '',
skills: '',
});
// 身份证校验相关
// 输入校验相关
const idCardError = ref('');
const nameError = ref('');
const ageError = ref('');
const sexError = ref('');
const experienceError = ref('');
onLoad((parmas) => {
getTreeselect();
@@ -204,11 +259,40 @@ onMounted(() => {});
function changeSex(sex) {
fromValue.sex = sex;
// 选择后清除性别错误
sexError.value = '';
}
// 姓名实时校验中文2-18或英文2-30
function validateName() {
const name = (fromValue.name || '').trim();
if (!name) {
nameError.value = '请输入姓名';
return;
}
const cn = /^[\u4e00-\u9fa5·]{2,18}$/;
const en = /^[A-Za-z\s]{2,30}$/;
nameError.value = cn.test(name) || en.test(name) ? '' : '姓名格式不正确';
}
// 年龄实时校验16-65的整数
function validateAge() {
const ageStr = String(fromValue.age || '').trim();
if (!ageStr) {
ageError.value = '请输入年龄';
return;
}
const num = Number(ageStr);
if (!/^\d{1,3}$/.test(ageStr) || Number.isNaN(num)) {
ageError.value = '年龄必须为数字';
return;
}
ageError.value = num >= 16 && num <= 65 ? '' : '年龄需在16-65之间';
}
// 身份证实时校验
function validateIdCard() {
const idCard = fromValue.idcard.trim();
const idCard = (fromValue.idCard || '').trim();
// 如果为空,清除错误信息
if (!idCard) {
@@ -231,8 +315,10 @@ function changeExperience() {
maskClick: true,
data: [oneDictData('experience')],
success: (_, [value]) => {
fromValue.experience = value.value;
state.experienceText = value.label;
fromValue.workExperience = value.value;
state.workExperience = value.label;
// 选择后清除工作经验错误
experienceError.value = '';
},
change(_, [value]) {
// this.setColunm(1, [123, 123]);
@@ -300,20 +386,154 @@ function changeJobs() {
});
}
// 技能等级选择
function changeSkillLevel() {
const skillLevels = [
{ label: '初级', value: '1' },
{ label: '中级', value: '2' },
{ label: '高级', value: '3' }
];
openSelectPopup({
title: '技能等级',
maskClick: true,
data: [skillLevels],
success: (_, [value]) => {
fromValue.skillLevel = value.value;
state.skillLevelText = value.label;
},
});
}
// 技能名称选择
function changeSkills() {
const skills = [
// 前端开发
{ label: 'HTML', value: 'html' },
{ label: 'CSS', value: 'css' },
{ label: 'JavaScript', value: 'javascript' },
{ label: 'TypeScript', value: 'typescript' },
{ label: 'React', value: 'react' },
{ label: 'Vue', value: 'vue' },
{ label: 'Angular', value: 'angular' },
{ label: 'jQuery', value: 'jquery' },
{ label: 'Bootstrap', value: 'bootstrap' },
{ label: 'Sass/Less', value: 'sass' },
{ label: 'Webpack', value: 'webpack' },
{ label: 'Vite', value: 'vite' },
// 后端开发
{ label: 'Java', value: 'java' },
{ label: 'Python', value: 'python' },
{ label: 'Node.js', value: 'nodejs' },
{ label: 'PHP', value: 'php' },
{ label: 'C#', value: 'csharp' },
{ label: 'Go', value: 'go' },
{ label: 'Ruby', value: 'ruby' },
{ label: 'Spring Boot', value: 'springboot' },
{ label: 'Django', value: 'django' },
{ label: 'Express', value: 'express' },
{ label: 'Laravel', value: 'laravel' },
// 数据库
{ label: 'MySQL', value: 'mysql' },
{ label: 'PostgreSQL', value: 'postgresql' },
{ label: 'MongoDB', value: 'mongodb' },
{ label: 'Redis', value: 'redis' },
{ label: 'Oracle', value: 'oracle' },
{ label: 'SQL Server', value: 'sqlserver' },
// 移动开发
{ label: 'React Native', value: 'reactnative' },
{ label: 'Flutter', value: 'flutter' },
{ label: 'iOS开发', value: 'ios' },
{ label: 'Android开发', value: 'android' },
{ label: '微信小程序', value: 'miniprogram' },
{ label: 'uni-app', value: 'uniapp' },
// 云计算与运维
{ label: 'Docker', value: 'docker' },
{ label: 'Kubernetes', value: 'kubernetes' },
{ label: 'AWS', value: 'aws' },
{ label: '阿里云', value: 'aliyun' },
{ label: 'Linux', value: 'linux' },
{ label: 'Nginx', value: 'nginx' },
// 设计工具
{ label: 'Photoshop', value: 'photoshop' },
{ label: 'Figma', value: 'figma' },
{ label: 'Sketch', value: 'sketch' },
{ label: 'Adobe XD', value: 'adobexd' },
// 其他技能
{ label: 'Git', value: 'git' },
{ label: 'Jenkins', value: 'jenkins' },
{ label: 'Jira', value: 'jira' },
{ label: '项目管理', value: 'projectmanagement' },
{ label: '数据分析', value: 'dataanalysis' },
{ label: '人工智能', value: 'ai' },
{ label: '机器学习', value: 'machinelearning' }
];
// 获取当前已选中的技能
const currentSelectedValues = fromValue.skills ? fromValue.skills.split(',') : [];
openSelectPopup({
title: '技能名称',
maskClick: true,
data: [skills],
multiSelect: true,
rowLabel: 'label',
rowKey: 'value',
defaultValues: currentSelectedValues,
success: (selectedValues, selectedItems) => {
const selectedSkills = selectedItems.map(item => item.value);
const selectedLabels = selectedItems.map(item => item.label);
fromValue.skills = selectedSkills.join(',');
state.skillsText = selectedLabels;
},
});
}
function nextStep() {
// 校验身份证号码
const idCard = fromValue.idcard.trim();
if (!idCard) {
$api.msg('请输入身份证号码');
// 统一必填与格式校验
validateName();
validateAge();
validateIdCard();
if (fromValue.sex !== 0 && fromValue.sex !== 1) {
sexError.value = '请选择性别';
}
// 工作经验校验
if (!state.workExperience) {
experienceError.value = '请选择您的工作经验';
} else {
experienceError.value = '';
}
// 学历校验
if (!state.educationText) {
$api.msg('请选择您的学历');
return;
}
const result = IdCardValidator.validate(idCard);
// 检查所有错误状态
if (nameError.value) return;
if (sexError.value) return;
if (ageError.value) return;
if (experienceError.value) return;
if (idCardError.value || !fromValue.idCard) {
if (!fromValue.idCard) $api.msg('请输入身份证号码');
return;
}
const result = IdCardValidator.validate(fromValue.idCard);
if (!result.valid) {
$api.msg(result.message);
return;
}
tabCurrent.value += 1;
}
@@ -359,9 +579,41 @@ function loginTest() {
}
function complete() {
const result = IdCardValidator.validate(fromValue.idcard);
const result = IdCardValidator.validate(fromValue.idCard);
if (result.valid) {
$api.createRequest('/app/user/resume', fromValue, 'post').then((resData) => {
// 构建 experiencesList 数组
const experiencesList = [];
if (fromValue.skills && fromValue.skillLevel) {
const skillsArray = fromValue.skills.split(',');
skillsArray.forEach(skill => {
if (skill.trim()) {
experiencesList.push({
name: skill.trim(),
levels: fromValue.skillLevel
});
}
});
}
// 构建符合要求的请求数据experiencesList 与 appUser 同级)
const requestData = {
appUser: {
name: fromValue.name,
isCompanyUser: 1,
age: fromValue.age,
sex: fromValue.sex,
workExperience: fromValue.workExperience,
education: fromValue.education,
idCard: fromValue.idCard,
area: fromValue.area,
jobTitleId: fromValue.jobTitleId,
salaryMin: fromValue.salaryMin,
salaryMax: fromValue.salaryMax
},
experiencesList: experiencesList
};
$api.createRequest('/app/user/registerUser', requestData, 'post').then((resData) => {
$api.msg('完成');
// 获取用户信息并存储到store中
getUserResume().then((userInfo) => {
@@ -403,32 +655,22 @@ function complete() {
display: flex
flex-wrap: wrap
.nx-item
padding: 20rpx 28rpx
padding: 16rpx 24rpx
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)
border-radius: 20rpx
border: 2rpx solid #E8EAEE
background-color: #f8f9fa
margin-right: 16rpx
margin-top: 16rpx
font-size: 28rpx
color: #333333
transition: all 0.2s ease
&:hover
background-color: #e9ecef
border-color: #256bfa
color: #256bfa
// 移除技能标签的箭头样式,因为技能标签不需要箭头指示
.container
// background: linear-gradient(#4778EC, #002979);
width: 100%;

View File

@@ -16,7 +16,7 @@
</view>
<!-- <view class="chart button-click">职业图谱</view> -->
</view>
<view class="cards" v-if="userInfo.userType !== 0">
<view class="cards" v-if="shouldShowJobSeekerContent">
<view class="card press-button" @click="handleNearbyClick">
<view class="card-title">附近工作</view>
<view class="card-text">好岗职等你来</view>
@@ -27,9 +27,8 @@
</view> -->
</view>
<!-- 服务功能网格 -->
<view class="service-grid" v-if="userInfo.userType !== 0">
<view class="service-grid" v-if="shouldShowJobSeekerContent">
<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>
@@ -84,13 +83,55 @@
</view>
<view class="service-title">AI智能面试</view>
</view>
<view class="service-item press-button" @click="navToTestPage">
<view class="service-icon service-icon-10">
<uni-icons type="gear-filled" size="32" color="#FFFFFF"></uni-icons>
</view>
<view class="service-title">测试页面</view>
</view>
</view>
</view>
<!-- 企业用户内容 -->
<view class="company-content" v-if="shouldShowCompanyContent">
<view class="company-header">
<text class="company-title">企业服务</text>
<text class="company-subtitle">为您提供专业的企业招聘服务</text>
</view>
<view class="company-grid">
<view class="company-item press-button" @click="navTo('/pages/job/publishJob')">
<view class="company-icon company-icon-1">
<uni-icons type="plus-filled" size="32" color="#FFFFFF"></uni-icons>
</view>
<view class="company-title">发布岗位</view>
</view>
<view class="company-item press-button" @click="handleServiceClick('company-management')">
<view class="company-icon company-icon-2">
<uni-icons type="settings-filled" size="32" color="#FFFFFF"></uni-icons>
</view>
<view class="company-title">企业管理</view>
</view>
<view class="company-item press-button" @click="handleServiceClick('recruitment-data')">
<view class="company-icon company-icon-3">
<uni-icons type="bar-chart-filled" size="32" color="#FFFFFF"></uni-icons>
</view>
<view class="company-title">招聘数据</view>
</view>
<view class="company-item press-button" @click="handleServiceClick('talent-pool')">
<view class="company-icon company-icon-4">
<uni-icons type="person-filled" size="32" color="#FFFFFF"></uni-icons>
</view>
<view class="company-title">人才库</view>
</view>
</view>
</view>
<!-- 吸顶筛选区域占位 -->
<view class="filter-placeholder" v-if="shouldStickyFilter && userInfo.userType !== 0"></view>
<view class="filter-placeholder" v-if="shouldStickyFilter && shouldShowJobSeekerContent"></view>
<view class="nav-filter" :class="{ 'sticky-filter': shouldStickyFilter }" v-if="userInfo.userType !== 0">
<view class="nav-filter" :class="{ 'sticky-filter': shouldStickyFilter }" v-if="shouldShowJobSeekerContent">
<view class="filter-top" @touchmove.stop.prevent>
<scroll-view :scroll-x="true" :show-scrollbar="false" class="tab-scroll">
<view class="jobs-left">
@@ -322,7 +363,7 @@
</template>
<script setup>
import { reactive, inject, watch, ref, onMounted, onUnmounted, watchEffect, nextTick } from 'vue';
import { reactive, inject, watch, ref, onMounted, onUnmounted, watchEffect, nextTick, computed } 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');
@@ -330,6 +371,30 @@ import { onLoad, onShow } from '@dcloudio/uni-app';
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
const { userInfo, hasLogin, token } = storeToRefs(useUserStore());
// 计算是否显示求职者内容
const shouldShowJobSeekerContent = computed(() => {
// 未登录时默认显示求职者内容
if (!hasLogin.value) {
return true;
}
// 登录后根据用户类型判断
const userType = userInfo.value?.isCompanyUser;
// 企业用户(isCompanyUser=0)不显示求职者内容,其他用户类型显示
return userType !== 0;
});
// 计算是否显示企业用户内容
const shouldShowCompanyContent = computed(() => {
// 未登录时不显示企业内容
if (!hasLogin.value) {
return false;
}
// 只有企业用户(isCompanyUser=0)才显示企业内容
const userType = userInfo.value?.isCompanyUser;
return userType === 0;
});
import useDictStore from '@/stores/useDictStore';
const { getTransformChildren, oneDictData } = useDictStore();
import useLocationStore from '@/stores/useLocationStore';
@@ -346,7 +411,6 @@ const lastScrollTop = ref(0);
const scrollTop = ref(0);
// 当用户与筛选/导航交互时,临时锁定头部显示状态,避免因数据刷新导致回弹显示
const isInteractingWithFilter = ref(false);
// 滚动阈值配置
const HIDE_THRESHOLD = 50; // 隐藏顶部区域的滚动阈值(降低阈值,更容易触发)
const SHOW_THRESHOLD = 5; // 显示顶部区域的滚动阈值(接近顶部)
@@ -461,6 +525,11 @@ const handleServiceClick = (serviceType) => {
}
};
// 跳转到测试页面
const navToTestPage = () => {
navTo('/pages/test/homepage-test');
};
async function loadData() {
try {
if (isLoaded.value) return;
@@ -1015,13 +1084,9 @@ defineExpose({ loadData });
.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
display: flex
align-items: center
justify-content: center
.service-icon-11
background: linear-gradient(135deg, #FF9800 0%, #FFB74D 100%)
position: relative
@@ -1340,6 +1405,71 @@ defineExpose({ loadData });
background-size contain
pointer-events none
filter: blur(3rpx)
// 企业用户内容样式
.company-content
padding: 40rpx 30rpx
background: #ffffff
margin: 20rpx 30rpx
border-radius: 20rpx
box-shadow: 0rpx 4rpx 12rpx 0rpx rgba(0, 0, 0, 0.1)
.company-header
text-align: center
margin-bottom: 40rpx
.company-title
font-size: 36rpx
font-weight: bold
color: #333333
display: block
margin-bottom: 10rpx
.company-subtitle
font-size: 24rpx
color: #666666
display: block
.company-grid
display: grid
grid-template-columns: 1fr 1fr
gap: 30rpx
.company-item
display: flex
flex-direction: column
align-items: center
padding: 30rpx 20rpx
background: #f8f9fa
border-radius: 15rpx
transition: all 0.3s ease
&:active
transform: scale(0.95)
background: #e9ecef
.company-icon
width: 60rpx
height: 60rpx
border-radius: 50%
display: flex
align-items: center
justify-content: center
margin-bottom: 15rpx
&.company-icon-1
background: #256BFA
&.company-icon-2
background: #52c41a
&.company-icon-3
background: #fa8c16
&.company-icon-4
background: #eb2f96
.company-title
font-size: 24rpx
color: #333333
font-weight: 500
.recommend-card
padding 36rpx 24rpx
background: linear-gradient( 360deg, #DFE9FF 0%, #FFFFFF 52%, #FFFFFF 100%);

View File

@@ -6,7 +6,8 @@
<IndexOne @onShowTabbar="changeShowTabbar" />
</view>
<!-- 统一使用系统tabBar -->
<!-- 自定义tabbar -->
<CustomTabBar :currentPage="0" />
</view>
</view>
</template>
@@ -18,11 +19,17 @@ import IndexOne from './components/index-one.vue';
// import IndexTwo from './components/index-two.vue';
import { storeToRefs } from 'pinia';
import { useReadMsg } from '@/stores/useReadMsg';
import { tabbarManager } from '@/utils/tabbarManager';
const { unreadCount } = storeToRefs(useReadMsg());
onLoad(() => {
// useReadMsg().fetchMessages();
});
onShow(() => {
// 更新自定义tabbar选中状态
tabbarManager.updateSelected(0);
});
</script>
<style lang="stylus" scoped>

412
pages/job/companySearch.vue Normal file
View File

@@ -0,0 +1,412 @@
<template>
<view class="company-search-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="search-container">
<view class="search-box">
<view class="search-icon">🔍</view>
<input
class="search-input"
placeholder="请输入企业名称进行搜索"
v-model="searchKeyword"
@input="onSearchInput"
@confirm="onSearchConfirm"
/>
<view class="clear-btn" v-if="searchKeyword" @click="clearSearch">
<view class="clear-icon"></view>
</view>
</view>
</view>
<!-- 搜索结果 -->
<scroll-view class="content" scroll-y="true" :style="{ height: scrollViewHeight }">
<!-- 加载状态 -->
<view class="loading-container" v-if="loading">
<view class="loading-text">搜索中...</view>
</view>
<!-- 搜索结果列表 -->
<view class="result-list" v-else-if="searchResults.length > 0">
<view
class="result-item"
v-for="(company, index) in searchResults"
:key="company.id || index"
@click="selectCompany(company)"
>
<view class="company-info">
<view class="company-name">{{ company.name }}</view>
<view class="company-detail" v-if="company.address">
<text class="detail-label">地址</text>
<text class="detail-value">{{ company.address }}</text>
</view>
<view class="company-detail" v-if="company.contact">
<text class="detail-label">联系人</text>
<text class="detail-value">{{ company.contact }}</text>
</view>
</view>
<view class="select-icon">
<view class="arrow-icon">></view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-container" v-else-if="searchKeyword && !loading">
<view class="empty-icon">🔍</view>
<view class="empty-text">未找到相关企业</view>
<view class="empty-tip">请尝试其他关键词</view>
</view>
<!-- 初始状态 -->
<view class="initial-container" v-else-if="!searchKeyword">
<view class="initial-icon">🔍</view>
<view class="initial-text">请输入企业名称进行搜索</view>
</view>
<!-- 底部安全区域 -->
<view class="bottom-safe-area"></view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue';
import { createRequest } from '@/utils/request';
// 搜索相关状态
const searchKeyword = ref('');
const searchResults = ref([]);
const loading = ref(false);
const scrollViewHeight = ref('calc(100vh - 200rpx)');
// 防抖定时器
let debounceTimer = null;
// 计算滚动视图高度
const calculateScrollViewHeight = () => {
const systemInfo = uni.getSystemInfoSync();
const windowHeight = systemInfo.windowHeight;
const headerHeight = 100; // 头部高度
const searchHeight = 100; // 搜索框高度
const scrollHeight = windowHeight - headerHeight - searchHeight;
scrollViewHeight.value = `${scrollHeight}px`;
};
// 页面加载时计算高度
onMounted(() => {
calculateScrollViewHeight();
});
// 搜索输入处理(防抖)
const onSearchInput = () => {
// 清除之前的定时器
if (debounceTimer) {
clearTimeout(debounceTimer);
}
// 设置新的定时器500ms后执行搜索
debounceTimer = setTimeout(() => {
if (searchKeyword.value.trim()) {
searchCompanies();
} else {
searchResults.value = [];
}
}, 500);
};
// 搜索确认
const onSearchConfirm = () => {
if (searchKeyword.value.trim()) {
searchCompanies();
}
};
// 搜索企业
const searchCompanies = async () => {
if (!searchKeyword.value.trim()) {
searchResults.value = [];
return;
}
try {
loading.value = true;
const response = await createRequest('/app/company/likeList', {
name: searchKeyword.value.trim()
}, 'GET', false);
if (response.code === 200) {
searchResults.value = response.data || [];
} else {
uni.showToast({
title: response.msg || '搜索失败',
icon: 'none'
});
searchResults.value = [];
}
} catch (error) {
console.error('搜索企业失败:', error);
uni.showToast({
title: '搜索失败,请重试',
icon: 'none'
});
searchResults.value = [];
} finally {
loading.value = false;
}
};
// 选择企业
const selectCompany = (company) => {
// 返回上一页并传递选中的企业信息
uni.navigateBack({
success: () => {
// 通过事件总线或全局数据传递选中的企业信息
getApp().globalData = getApp().globalData || {};
getApp().globalData.selectedCompany = company;
}
});
};
// 清除搜索
const clearSearch = () => {
searchKeyword.value = '';
searchResults.value = [];
if (debounceTimer) {
clearTimeout(debounceTimer);
}
};
// 返回上一页
const goBack = () => {
uni.navigateBack();
};
</script>
<style lang="scss" scoped>
.company-search-page {
height: 100vh;
background-color: #f5f5f5;
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
}
.header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 30rpx;
background: #fff;
border-bottom: 1rpx solid #eee;
.header-left {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
.back-icon {
width: 40rpx;
height: 40rpx;
}
}
.header-title {
font-size: 36rpx;
font-weight: 600;
color: #333;
}
.header-right {
width: 60rpx;
}
}
.search-container {
padding: 20rpx 30rpx;
background: #fff;
border-bottom: 1rpx solid #eee;
.search-box {
display: flex;
align-items: center;
background: #f8f8f8;
border-radius: 12rpx;
padding: 0 20rpx;
height: 80rpx;
.search-icon {
font-size: 32rpx;
color: #999;
margin-right: 20rpx;
}
.search-input {
flex: 1;
height: 80rpx;
background: transparent;
border: none;
font-size: 28rpx;
color: #333;
&::placeholder {
color: #999;
}
}
.clear-btn {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
margin-left: 20rpx;
.clear-icon {
font-size: 24rpx;
color: #999;
}
}
}
}
.content {
flex: 1;
padding: 0;
overflow: hidden;
}
.loading-container {
display: flex;
justify-content: center;
align-items: center;
height: 200rpx;
.loading-text {
font-size: 28rpx;
color: #999;
}
}
.result-list {
background: #fff;
.result-item {
display: flex;
align-items: center;
padding: 30rpx;
border-bottom: 1rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
}
&:active {
background: #f8f8f8;
}
.company-info {
flex: 1;
.company-name {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 10rpx;
}
.company-detail {
font-size: 24rpx;
color: #666;
margin-bottom: 5rpx;
&:last-child {
margin-bottom: 0;
}
.detail-label {
color: #999;
}
.detail-value {
color: #666;
}
}
}
.select-icon {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
.arrow-icon {
font-size: 24rpx;
color: #999;
}
}
}
}
.empty-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 400rpx;
.empty-icon {
font-size: 120rpx;
color: #ccc;
margin-bottom: 30rpx;
}
.empty-text {
font-size: 32rpx;
color: #333;
margin-bottom: 10rpx;
}
.empty-tip {
font-size: 24rpx;
color: #999;
}
}
.initial-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 400rpx;
.initial-icon {
font-size: 120rpx;
color: #ccc;
margin-bottom: 30rpx;
}
.initial-text {
font-size: 28rpx;
color: #999;
}
}
.bottom-safe-area {
height: 120rpx;
background: transparent;
}
</style>

View File

@@ -10,12 +10,11 @@
</view>
<!-- 主要内容 -->
<view class="content">
<!-- 岗位基本信息 -->
<view class="section">
<view class="section-title">岗位基本信息</view>
<scroll-view class="content" scroll-y="true" :style="{ height: scrollViewHeight }" :scroll-with-animation="true">
<!-- 基本信息区块 -->
<view class="form-block">
<view class="form-group">
<view class="label">岗位名称 *</view>
<view class="label">岗位名称</view>
<input
class="input"
placeholder="请输入岗位名称"
@@ -23,75 +22,41 @@
/>
</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>
<view class="label">招聘会公司</view>
<input
class="input"
placeholder="请输入工作地点"
v-model="formData.workLocation"
placeholder="请输入公司名称"
v-model="formData.companyName"
/>
</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 class="form-group">
<view class="label">最小薪资 (/)</view>
<input
class="input"
placeholder="请输入最小薪资"
type="number"
v-model="formData.minSalary"
/>
</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 class="label">最大薪资 (/)</view>
<input
class="input"
placeholder="请输入最大薪资"
type="number"
v-model="formData.maxSalary"
/>
</view>
</view>
<!-- 任职要求 -->
<view class="section">
<view class="section-title">任职要求</view>
<view class="form-group">
<view class="label">学历要求</view>
<picker
mode="selector"
:range="educationLevels"
range-key="label"
@change="onEducationChange"
class="picker"
>
<view class="picker-text">{{ selectedEducation || '请选择学历要求' }}</view>
<view class="picker-text" data-placeholder="请选择学历要求">{{ selectedEducation || '请选择学历要求' }}</view>
</picker>
</view>
<view class="form-group">
@@ -99,10 +64,23 @@
<picker
mode="selector"
:range="experienceLevels"
range-key="label"
@change="onExperienceChange"
class="picker"
>
<view class="picker-text">{{ selectedExperience || '请选择工作经验' }}</view>
<view class="picker-text" data-placeholder="请选择工作经验">{{ selectedExperience || '请选择工作经验' }}</view>
</picker>
</view>
<view class="form-group">
<view class="label">工作区县</view>
<picker
mode="selector"
:range="workDistricts"
range-key="label"
@change="onWorkDistrictChange"
class="picker"
>
<view class="picker-text" data-placeholder="请选择工作区县">{{ selectedWorkDistrict || '请选择工作区县' }}</view>
</picker>
</view>
<view class="form-group">
@@ -114,21 +92,49 @@
v-model="formData.recruitCount"
/>
</view>
<view class="form-group">
<view class="label">工作地点</view>
<view class="location-input-container">
<input
class="input location-input"
placeholder="请输入具体工作地址"
v-model="formData.jobLocation"
/>
<view class="location-btn" @click="chooseLocation">
<text class="location-btn-text">选择位置</text>
</view>
</view>
</view>
<view class="form-group">
<view class="label">岗位分类</view>
<picker
mode="selector"
:range="jobCategories"
range-key="label"
@change="onJobCategoryChange"
class="picker"
>
<view class="picker-text" data-placeholder="请选择岗位分类">{{ selectedJobCategory || '请选择岗位分类' }}</view>
</picker>
</view>
</view>
<!-- 岗位描述 -->
<view class="section">
<!-- 岗位描述区块 -->
<view class="form-block">
<view class="section-title">岗位描述</view>
<view class="form-group">
<view class="label">岗位职责</view>
<textarea
class="textarea"
placeholder="请详细描述岗位职责和工作内容"
v-model="formData.jobDescription"
v-model="formData.description"
></textarea>
</view>
</view>
<!-- 任职要求区块 -->
<view class="form-block">
<view class="section-title">任职要求</view>
<view class="form-group">
<view class="label">任职要求</view>
<textarea
class="textarea"
placeholder="请描述对候选人的具体要求"
@@ -137,35 +143,52 @@
</view>
</view>
<!-- 联系方式 -->
<view class="section">
<!-- 联系方式区块 -->
<view class="form-block">
<view class="section-title">联系方式</view>
<view class="form-group">
<view class="label">联系人</view>
<input
class="input"
placeholder="请输入联系人姓名"
v-model="formData.contactPerson"
/>
<view class="contacts-container">
<view
class="contact-item"
v-for="(contact, index) in formData.contacts"
:key="index"
>
<view class="contact-header">
<view class="contact-title">联系人 {{ index + 1 }}</view>
<view
class="delete-btn"
v-if="formData.contacts.length > 1"
@click="removeContact(index)"
>
删除
</view>
</view>
<view class="form-group">
<view class="label">联系人姓名</view>
<input
class="input"
placeholder="请输入联系人姓名"
v-model="contact.name"
/>
</view>
<view class="form-group">
<view class="label">联系电话</view>
<input
class="input"
placeholder="请输入联系电话"
v-model="contact.phone"
/>
</view>
</view>
</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 class="add-contact-btn" v-if="formData.contacts.length < 3" @click="addContact">
<view class="add-icon">+</view>
<view class="add-text">添加联系人</view>
</view>
</view>
</view>
<!-- 底部安全区域 -->
<view class="bottom-safe-area"></view>
</scroll-view>
<!-- 底部操作按钮 -->
<view class="footer">
@@ -178,49 +201,171 @@
</template>
<script setup>
import { ref, reactive } from 'vue';
import { ref, reactive, onMounted } from 'vue';
import { createRequest } from '@/utils/request';
import useDictStore from '@/stores/useDictStore';
import useUserStore from '@/stores/useUserStore';
// 表单数据
const formData = reactive({
jobTitle: '',
workLocation: '',
companyName: '',
minSalary: '',
maxSalary: '',
recruitCount: '',
jobDescription: '',
recruitCount: '', // 对应接口字段 idCardPictureBackUrl
description: '', // 对应接口字段 description
jobRequirements: '',
contactPerson: '',
contactPhone: '',
contactEmail: ''
jobCategory: '', // 新增:岗位分类
companyId: '', // 新增企业id
latitude: '', // 新增:纬度
longitude: '', // 新增:经度
jobLocation: '', // 新增:工作地点
jobLocationAreaCode: '', // 新增:工作地点区县字典代码
education: '', // 新增:学历要求字典值
experience: '', // 新增:工作经验字典值
contacts: [
{
name: '',
phone: ''
}
]
});
// 字典存储
const dictStore = useDictStore();
const userStore = useUserStore();
// 选择器数据
const jobTypes = ['技术类', '销售类', '管理类', '服务类', '其他'];
const salaryUnits = ['元/月', '元/年', '元/小时'];
const educationLevels = ['不限', '高中', '大专', '本科', '硕士', '博士'];
const experienceLevels = ['不限', '应届毕业生', '1-3年', '3-5年', '5-10年', '10年以上'];
const educationLevels = ref([]);
const experienceLevels = ref([]);
const workDistricts = ref([]);
const workLocations = ref([]);
const jobCategories = ref([]); // 新增:岗位分类选项
// 选中的值
const selectedJobType = ref('');
const selectedSalaryUnit = ref('');
const selectedEducation = ref('');
const selectedExperience = ref('');
const selectedWorkDistrict = ref('');
const selectedWorkLocation = ref('');
const selectedJobCategory = ref('');
// 滚动视图高度
const scrollViewHeight = ref('calc(100vh - 200rpx)');
// 计算滚动视图高度
const calculateScrollViewHeight = () => {
const systemInfo = uni.getSystemInfoSync();
const windowHeight = systemInfo.windowHeight;
const headerHeight = 100; // 头部高度
const footerHeight = 120; // 底部按钮高度
const scrollHeight = windowHeight - headerHeight - footerHeight;
scrollViewHeight.value = `${scrollHeight}px`;
};
// 页面加载时计算高度和初始化数据
onMounted(async () => {
calculateScrollViewHeight();
await initFormData();
});
// 初始化表单数据
const initFormData = async () => {
try {
// 获取字典数据
await dictStore.getDictData();
// 设置学历选项
educationLevels.value = dictStore.state.education;
// 设置工作经验选项
experienceLevels.value = dictStore.state.experience;
// 设置区县选项(从字典获取)
workDistricts.value = dictStore.state.area;
// 设置岗位分类选项
jobCategories.value = [
{ label: '普通', value: '1' },
{ label: '零工', value: '2' },
{ label: '实习实训', value: '3' }
];
// 设置企业ID从用户信息获取
if (userStore.userInfo && userStore.userInfo.id) {
formData.companyId = userStore.userInfo.id;
}
} catch (error) {
console.error('初始化表单数据失败:', error);
uni.showToast({
title: '数据加载失败',
icon: 'none'
});
}
};
// 选择器事件处理
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 index = e.detail.value;
const selectedItem = educationLevels.value[index];
selectedEducation.value = selectedItem.label;
formData.education = selectedItem.value;
};
const onExperienceChange = (e) => {
selectedExperience.value = experienceLevels[e.detail.value];
const index = e.detail.value;
const selectedItem = experienceLevels.value[index];
selectedExperience.value = selectedItem.label;
formData.experience = selectedItem.value;
};
const onWorkDistrictChange = (e) => {
const index = e.detail.value;
const selectedItem = workDistricts.value[index];
selectedWorkDistrict.value = selectedItem.label;
formData.jobLocationAreaCode = selectedItem.value;
};
const onJobCategoryChange = (e) => {
const index = e.detail.value;
const selectedItem = jobCategories.value[index];
selectedJobCategory.value = selectedItem.label;
formData.jobCategory = selectedItem.value;
};
// 选择位置
const chooseLocation = () => {
uni.chooseLocation({
success: (res) => {
formData.jobLocation = res.address;
formData.latitude = res.latitude.toString();
formData.longitude = res.longitude.toString();
},
fail: (err) => {
console.error('选择位置失败:', err);
uni.showToast({
title: '获取位置失败',
icon: 'none'
});
}
});
};
// 添加联系人
const addContact = () => {
if (formData.contacts.length < 3) {
formData.contacts.push({
name: '',
phone: ''
});
}
};
// 删除联系人
const removeContact = (index) => {
if (formData.contacts.length > 1) {
formData.contacts.splice(index, 1);
}
};
// 返回上一页
@@ -229,49 +374,138 @@ const goBack = () => {
};
// 发布岗位
const publishJob = () => {
// 单验证
if (!formData.jobTitle) {
uni.showToast({
title: '请输入岗位名称',
icon: 'none'
});
return;
}
if (!formData.workLocation) {
uni.showToast({
title: '请输入工作地点',
icon: 'none'
});
const publishJob = async () => {
// 单验证
if (!validateForm()) {
return;
}
// 模拟发布成功
uni.showLoading({
title: '发布中...'
});
setTimeout(() => {
uni.hideLoading();
uni.showToast({
title: '发布成功',
icon: 'success'
try {
uni.showLoading({
title: '发布中...'
});
// 构建请求数据
const requestData = {
jobTitle: formData.jobTitle,
minSalary: formData.minSalary,
maxSalary: formData.maxSalary,
education: formData.education,
experience: formData.experience,
jobLocation: formData.jobLocation,
jobLocationAreaCode: formData.jobLocationAreaCode,
idCardPictureBackUrl: formData.recruitCount, // 招聘人数
latitude: formData.latitude,
longitude: formData.longitude,
description: formData.description,
jobCategory: formData.jobCategory,
companyId: formData.companyId,
companyName: formData.companyName,
jobContactList: formData.contacts.filter(contact => contact.name.trim() && contact.phone.trim())
};
// 调用发布接口
const response = await createRequest('/app/job/publishJob', requestData, 'POST', false);
// 延迟返回
setTimeout(() => {
goBack();
}, 1500);
}, 2000);
uni.hideLoading();
if (response.code === 200) {
uni.showToast({
title: '发布成功',
icon: 'success'
});
// 延迟返回
setTimeout(() => {
goBack();
}, 1500);
} else {
uni.showToast({
title: response.msg || '发布失败',
icon: 'none'
});
}
} catch (error) {
uni.hideLoading();
console.error('发布岗位失败:', error);
uni.showToast({
title: '发布失败,请重试',
icon: 'none'
});
}
};
// 表单验证
const validateForm = () => {
// 必填字段验证
const requiredFields = [
{ field: 'jobTitle', message: '请输入岗位名称' },
{ field: 'companyName', message: '请输入公司名称' },
{ field: 'minSalary', message: '请输入最小薪资' },
{ field: 'maxSalary', message: '请输入最大薪资' },
{ field: 'education', message: '请选择学历要求' },
{ field: 'experience', message: '请选择工作经验' },
{ field: 'jobLocation', message: '请选择工作地点' },
{ field: 'jobLocationAreaCode', message: '请选择工作区县' },
{ field: 'recruitCount', message: '请输入招聘人数' },
{ field: 'description', message: '请输入岗位描述' },
{ field: 'jobCategory', message: '请选择岗位分类' }
];
for (const { field, message } of requiredFields) {
if (!formData[field] || formData[field].toString().trim() === '') {
uni.showToast({
title: message,
icon: 'none'
});
return false;
}
}
// 薪资验证
const minSalary = parseFloat(formData.minSalary);
const maxSalary = parseFloat(formData.maxSalary);
if (minSalary >= maxSalary) {
uni.showToast({
title: '最大薪资必须大于最小薪资',
icon: 'none'
});
return false;
}
// 验证联系人信息
for (let i = 0; i < formData.contacts.length; i++) {
const contact = formData.contacts[i];
if (!contact.name.trim()) {
uni.showToast({
title: `请输入第${i + 1}个联系人姓名`,
icon: 'none'
});
return false;
}
if (!contact.phone.trim()) {
uni.showToast({
title: `请输入第${i + 1}个联系人电话`,
icon: 'none'
});
return false;
}
}
return true;
};
</script>
<style lang="scss" scoped>
.publish-job-page {
min-height: 100vh;
height: 100vh;
background-color: #f5f5f5;
padding-bottom: 120rpx;
display: flex;
flex-direction: column;
position: relative;
overflow: hidden;
}
.header {
@@ -307,16 +541,17 @@ const publishJob = () => {
}
.content {
flex: 1;
padding: 0;
overflow: hidden;
}
.section {
.form-block {
background: #fff;
border-radius: 0;
padding: 30rpx;
margin-bottom: 20rpx;
width: 100%;
position: relative;
padding-bottom: 10rpx;
&:last-child {
margin-bottom: 0;
}
@@ -326,23 +561,27 @@ const publishJob = () => {
font-weight: 600;
color: #333;
margin-bottom: 30rpx;
padding-bottom: 20rpx;
padding: 30rpx 30rpx 20rpx 30rpx;
border-bottom: 2rpx solid #f0f0f0;
}
}
.form-group {
margin-bottom: 30rpx;
margin-bottom: 0;
padding: 0 30rpx;
border-bottom: 2rpx solid #f0f0f0;
&:last-child {
margin-bottom: 0;
border-bottom: none;
padding-bottom: 30rpx;
}
.label {
font-size: 28rpx;
color: #333;
color: #000;
margin-bottom: 15rpx;
font-weight: 500;
font-weight: 400;
padding-top: 30rpx;
}
.input {
@@ -350,15 +589,23 @@ const publishJob = () => {
height: 80rpx;
background: #fff;
border: none;
border-bottom: 2rpx solid #e0e0e0;
border-radius: 0;
padding: 0 0 10rpx 0;
padding: 0 0 20rpx 0;
font-size: 28rpx;
color: #333;
position: relative;
z-index: 1;
box-sizing: border-box;
&::placeholder {
color: #999;
font-size: 24rpx;
}
&:focus {
border-bottom-color: #256BFA;
background: #fff;
transform: translateZ(0);
-webkit-transform: translateZ(0);
}
}
@@ -367,15 +614,23 @@ const publishJob = () => {
min-height: 120rpx;
background: #fff;
border: none;
border-bottom: 2rpx solid #e0e0e0;
border-radius: 0;
padding: 20rpx 0 10rpx 0;
padding: 0 0 20rpx 0;
font-size: 28rpx;
color: #333;
position: relative;
z-index: 1;
box-sizing: border-box;
&::placeholder {
color: #999;
font-size: 24rpx;
}
&:focus {
border-bottom-color: #256BFA;
background: #fff;
transform: translateZ(0);
-webkit-transform: translateZ(0);
}
}
@@ -384,38 +639,132 @@ const publishJob = () => {
height: 80rpx;
background: #fff;
border: none;
border-bottom: 2rpx solid #e0e0e0;
border-radius: 0;
display: flex;
align-items: center;
padding: 0 0 10rpx 0;
justify-content: space-between;
padding: 0 0 20rpx 0;
box-sizing: border-box;
.picker-text {
font-size: 28rpx;
color: #333;
&:empty::before {
content: attr(data-placeholder);
color: #999;
font-size: 24rpx;
}
}
&::after {
content: '';
width: 0;
height: 0;
border-left: 8rpx solid transparent;
border-right: 8rpx solid transparent;
border-top: 8rpx solid #999;
}
}
}
.salary-row {
// 联系人管理样式
.contacts-container {
.contact-item {
margin-bottom: 30rpx;
padding: 0 30rpx;
background: #fff;
border-radius: 12rpx;
&:last-child {
margin-bottom: 0;
}
.contact-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0 10rpx 0;
border-bottom: 1rpx solid #eee;
margin-bottom: 20rpx;
.contact-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
}
.delete-btn {
font-size: 24rpx;
color: #ff4757;
padding: 8rpx 16rpx;
background: #fff;
border: 1rpx solid #ff4757;
border-radius: 6rpx;
&:active {
background: #ff4757;
color: #fff;
}
}
}
.form-group {
background: transparent;
border-bottom: 1rpx solid #eee;
margin-bottom: 0;
&:last-child {
border-bottom: none;
}
.label {
font-size: 26rpx;
color: #666;
margin-bottom: 10rpx;
padding-top: 20rpx;
}
.input {
font-size: 26rpx;
padding-bottom: 15rpx;
}
}
}
}
.add-contact-btn {
display: flex;
align-items: center;
gap: 20rpx;
justify-content: center;
margin: 20rpx 30rpx 30rpx 30rpx;
padding: 20rpx;
background: #fff;
border: 2rpx dashed #ddd;
border-radius: 12rpx;
.form-group {
flex: 1;
margin-bottom: 0;
}
.salary-separator {
.add-icon {
font-size: 32rpx;
color: #666;
margin-top: 40rpx;
color: #256BFA;
margin-right: 10rpx;
font-weight: bold;
}
.salary-input {
text-align: center;
.add-text {
font-size: 28rpx;
color: #256BFA;
font-weight: 500;
}
&:active {
background: #e3f2fd;
border-color: #256BFA;
}
}
.bottom-safe-area {
height: 120rpx;
background: transparent;
}
.footer {
@@ -426,6 +775,7 @@ const publishJob = () => {
background: #fff;
padding: 20rpx 30rpx;
border-top: 1rpx solid #eee;
z-index: 100;
.btn-group {
display: flex;
@@ -451,4 +801,28 @@ const publishJob = () => {
}
}
}
/* 防止键盘弹出时页面偏移 */
/* #ifdef H5 */
.publish-job-page {
-webkit-overflow-scrolling: touch;
-webkit-transform: translateZ(0);
transform: translateZ(0);
}
.publish-job-page * {
-webkit-transform: translateZ(0);
transform: translateZ(0);
}
/* #endif */
/* #ifdef MP-WEIXIN */
.publish-job-page {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
/* #endif */
</style>

View File

@@ -1,5 +1,7 @@
<template>
<AppLayout title="我的" back-gorund-color="#F4F4F4">
<!-- 自定义tabbar -->
<CustomTabBar :currentPage="4" />
<view class="mine-userinfo btn-feel" @click="seeDetail">
<view class="userindo-head">
<image class="userindo-head-img" v-if="userInfo.sex === '0'" src="/static/icon/boy.png"></image>
@@ -108,6 +110,7 @@ import { storeToRefs } from 'pinia';
import { onLoad, onShow } from '@dcloudio/uni-app';
const { $api, navTo } = inject('globalFunction');
import useUserStore from '@/stores/useUserStore';
import { tabbarManager } from '@/utils/tabbarManager';
const popup = ref(null);
const { userInfo, Completion } = storeToRefs(useUserStore());
const counts = ref({});
@@ -116,6 +119,8 @@ function logOut() {
}
onShow(() => {
getUserstatistics();
// 更新自定义tabbar选中状态
tabbarManager.updateSelected(4);
});
function close() {

View File

@@ -28,6 +28,9 @@
</swiper>
</view>
<!-- 自定义tabbar -->
<CustomTabBar :currentPage="3" />
<!-- 统一使用系统tabBar -->
</view>
</view>
@@ -39,6 +42,7 @@ import { onLoad, onShow } from '@dcloudio/uni-app';
import Tabbar from '@/components/tabbar/midell-box.vue';
import ReadComponent from './read.vue';
import UnreadComponent from './unread.vue';
import { tabbarManager } from '@/utils/tabbarManager';
const loadedMap = reactive([false, false]);
const swiperRefs = [ref(null), ref(null)];
const components = [ReadComponent, UnreadComponent];
@@ -49,6 +53,8 @@ const { unreadCount } = storeToRefs(useReadMsg());
onShow(() => {
// 获取消息列表
useReadMsg().fetchMessages();
// 更新自定义tabbar选中状态
tabbarManager.updateSelected(3);
});
const state = reactive({
current: 0,

View File

@@ -0,0 +1,170 @@
<template>
<view class="test-page">
<view class="header">
<text class="title">企业搜索功能测试</text>
</view>
<view class="test-section">
<view class="section-title">功能说明</view>
<view class="description">
<text class="desc-text"> 企业用户isCompanyUser=0招聘公司输入框为普通输入框</text>
<text class="desc-text"> 网格员isCompanyUser=2招聘公司输入框为选择器点击跳转到搜索页面</text>
<text class="desc-text"> 搜索页面支持防抖节流500ms延迟</text>
<text class="desc-text"> 搜索接口/app/company/likeList参数name</text>
</view>
</view>
<view class="test-section">
<view class="section-title">当前用户类型</view>
<view class="user-type-info">
<text class="type-label">用户类型</text>
<text class="type-value">{{ getCurrentTypeLabel() }} ({{ currentUserType }})</text>
</view>
<view class="user-type-info">
<text class="type-label">是否企业用户</text>
<text class="type-value">{{ isCompanyUser ? '是' : '否' }}</text>
</view>
</view>
<view class="test-section">
<view class="section-title">测试操作</view>
<view class="button-group">
<button class="test-btn" @click="switchToCompany">切换到企业用户</button>
<button class="test-btn" @click="switchToGrid">切换到网格员</button>
<button class="test-btn" @click="goToPublishJob">进入发布岗位页面</button>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from 'vue';
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
const userStore = useUserStore();
const { userInfo } = storeToRefs(userStore);
const userTypes = [
{ value: 0, label: '企业用户' },
{ value: 1, label: '求职者' },
{ value: 2, label: '网格员' },
{ value: 3, label: '政府人员' }
];
const currentUserType = computed(() => userInfo.value?.isCompanyUser !== undefined ? userInfo.value.isCompanyUser : 1);
const isCompanyUser = computed(() => {
return currentUserType.value === 0;
});
const getCurrentTypeLabel = () => {
const type = userTypes.find(t => t.value === currentUserType.value);
return type ? type.label : '未知';
};
const switchToCompany = () => {
userInfo.value.isCompanyUser = 0;
uni.setStorageSync('userInfo', userInfo.value);
uni.showToast({
title: '已切换到企业用户',
icon: 'success'
});
};
const switchToGrid = () => {
userInfo.value.isCompanyUser = 2;
uni.setStorageSync('userInfo', userInfo.value);
uni.showToast({
title: '已切换到网格员',
icon: 'success'
});
};
const goToPublishJob = () => {
uni.navigateTo({
url: '/pages/job/publishJob'
});
};
</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;
}
}
.test-section {
background: #fff;
border-radius: 12rpx;
padding: 30rpx;
margin-bottom: 30rpx;
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 20rpx;
}
.description {
.desc-text {
display: block;
font-size: 26rpx;
color: #666;
line-height: 1.6;
margin-bottom: 10rpx;
}
}
.user-type-info {
display: flex;
align-items: center;
margin-bottom: 15rpx;
.type-label {
font-size: 28rpx;
color: #333;
margin-right: 10rpx;
}
.type-value {
font-size: 28rpx;
color: #256BFA;
font-weight: 500;
}
}
.button-group {
display: flex;
flex-direction: column;
gap: 20rpx;
.test-btn {
height: 80rpx;
background: #256BFA;
color: #fff;
border: none;
border-radius: 12rpx;
font-size: 28rpx;
font-weight: 500;
&:active {
background: #1e5ce6;
}
}
}
}
</style>

View File

@@ -0,0 +1,296 @@
<template>
<view class="homepage-test">
<view class="header">
<text class="title">首页内容测试</text>
</view>
<view class="content">
<view class="user-info">
<text class="label">当前用户类型</text>
<text class="value">{{ getCurrentUserTypeLabel() }}</text>
</view>
<view class="login-status">
<text class="label">登录状态</text>
<text class="value" :class="{ 'logged-in': hasLogin, 'not-logged-in': !hasLogin }">
{{ hasLogin ? '已登录' : '未登录' }}
</text>
</view>
<view class="debug-info">
<text class="label">调试信息</text>
<text class="value">userType: {{ userInfo?.userType ?? 'undefined' }}</text>
<text class="value">hasLogin: {{ hasLogin }}</text>
<text class="value">shouldShowJobSeeker: {{ shouldShowJobSeekerContent }}</text>
<text class="value">shouldShowCompany: {{ shouldShowCompanyContent }}</text>
<text class="value">完整userInfo: {{ JSON.stringify(userInfo) }}</text>
</view>
<view class="content-preview">
<text class="section-title">首页内容预览</text>
<view class="content-item" v-if="shouldShowJobSeekerContent">
<text class="content-label"> 求职者内容</text>
<text class="content-desc"> 附近工作卡片</text>
<text class="content-desc"> 服务功能网格</text>
<text class="content-desc"> 职位筛选器</text>
</view>
<view class="content-item" v-if="shouldShowCompanyContent">
<text class="content-label"> 企业用户内容</text>
<text class="content-desc"> 企业服务标题</text>
<text class="content-desc"> 发布岗位按钮</text>
<text class="content-desc"> 企业管理功能</text>
</view>
<view class="content-item" v-if="!shouldShowJobSeekerContent && !shouldShowCompanyContent">
<text class="content-label"> 无内容显示</text>
<text class="content-desc">请检查用户类型设置</text>
</view>
</view>
<view class="test-buttons">
<button @click="testLoginAsJobSeeker" class="test-btn">模拟求职者登录</button>
<button @click="testLoginAsCompany" class="test-btn">模拟企业用户登录</button>
<button @click="testLogout" class="test-btn" v-if="hasLogin">模拟登出</button>
<button @click="forceRefreshUserInfo" class="test-btn">强制刷新用户信息</button>
<button @click="clearUserInfo" class="test-btn">清除用户信息</button>
<button @click="refreshTabBar" class="test-btn">刷新TabBar</button>
</view>
</view>
<!-- 自定义tabbar -->
<CustomTabBar :currentPage="0" />
</view>
</template>
<script setup>
import { ref, computed } from 'vue';
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
import { tabbarManager } from '@/utils/tabbarManager';
const userStore = useUserStore();
const { userInfo, hasLogin } = storeToRefs(userStore);
const userTypes = [
{ value: 0, label: '企业用户' },
{ value: 1, label: '求职者' },
{ value: 2, label: '网格员' },
{ value: 3, label: '政府人员' }
];
const currentUserType = computed(() => userInfo.value?.userType || 1);
const getCurrentUserTypeLabel = () => {
const type = userTypes.find(t => t.value === currentUserType.value);
return type ? type.label : '未知';
};
// 计算是否显示求职者内容
const shouldShowJobSeekerContent = computed(() => {
if (!hasLogin.value) {
return true;
}
const userType = userInfo.value?.isCompanyUser;
return userType !== 0;
});
// 计算是否显示企业用户内容
const shouldShowCompanyContent = computed(() => {
if (!hasLogin.value) {
return false;
}
const userType = userInfo.value?.isCompanyUser;
return userType === 0;
});
const testLoginAsJobSeeker = () => {
const mockUserInfo = {
userType: 1,
name: '求职者用户',
id: 'jobseeker123'
};
userInfo.value = mockUserInfo;
userStore.hasLogin = true;
uni.setStorageSync('userInfo', mockUserInfo);
uni.showToast({
title: '模拟求职者登录成功',
icon: 'success'
});
};
const testLoginAsCompany = () => {
const mockUserInfo = {
userType: 0,
name: '企业用户',
id: 'company123'
};
userInfo.value = mockUserInfo;
userStore.hasLogin = true;
uni.setStorageSync('userInfo', mockUserInfo);
uni.showToast({
title: '模拟企业用户登录成功',
icon: 'success'
});
};
const testLogout = () => {
userStore.logOut(false);
uni.showToast({
title: '模拟登出成功',
icon: 'success'
});
};
const forceRefreshUserInfo = () => {
// 从本地存储重新加载用户信息
const cachedUserInfo = uni.getStorageSync('userInfo');
if (cachedUserInfo) {
userInfo.value = cachedUserInfo;
userStore.hasLogin = true;
uni.showToast({
title: '用户信息已刷新',
icon: 'success'
});
} else {
uni.showToast({
title: '未找到用户信息',
icon: 'none'
});
}
};
const clearUserInfo = () => {
// 清除所有用户信息
uni.removeStorageSync('userInfo');
uni.removeStorageSync('token');
userInfo.value = {};
userStore.hasLogin = false;
uni.showToast({
title: '用户信息已清除',
icon: 'success'
});
};
const refreshTabBar = () => {
// 刷新tabbar
tabbarManager.refreshTabBar();
uni.showToast({
title: 'TabBar已刷新',
icon: 'success'
});
};
</script>
<style lang="scss" scoped>
.homepage-test {
padding: 40rpx;
min-height: 100vh;
background-color: #f5f5f5;
}
.header {
text-align: center;
margin-bottom: 40rpx;
}
.title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.content {
background: white;
border-radius: 20rpx;
padding: 40rpx;
margin-bottom: 40rpx;
}
.user-info, .login-status, .debug-info {
display: flex;
flex-direction: column;
margin-bottom: 20rpx;
padding: 20rpx;
background: #f8f9fa;
border-radius: 10rpx;
}
.debug-info .value {
font-size: 20rpx;
color: #666;
margin: 5rpx 0;
display: block;
word-break: break-all;
}
.label {
font-size: 28rpx;
color: #666;
margin-right: 20rpx;
}
.value {
font-size: 28rpx;
color: #256BFA;
font-weight: bold;
}
.logged-in {
color: #52c41a !important;
}
.not-logged-in {
color: #ff4d4f !important;
}
.content-preview {
margin: 30rpx 0;
}
.section-title {
font-size: 28rpx;
color: #333;
font-weight: bold;
margin-bottom: 20rpx;
display: block;
}
.content-item {
background: #f8f9fa;
padding: 20rpx;
border-radius: 10rpx;
margin-bottom: 15rpx;
}
.content-label {
font-size: 24rpx;
color: #333;
font-weight: bold;
display: block;
margin-bottom: 10rpx;
}
.content-desc {
font-size: 22rpx;
color: #666;
margin: 5rpx 0;
display: block;
}
.test-buttons {
margin-top: 30rpx;
}
.test-btn {
margin: 10rpx 0;
padding: 20rpx 30rpx;
background: #256BFA;
color: white;
border: none;
border-radius: 10rpx;
font-size: 24rpx;
width: 100%;
}
</style>

293
pages/test/tabbar-test.vue Normal file
View File

@@ -0,0 +1,293 @@
<template>
<view class="tabbar-test">
<view class="header">
<text class="title">自定义TabBar测试页面</text>
</view>
<view class="content">
<view class="user-info">
<text class="label">当前用户类型</text>
<text class="value">{{ getCurrentUserTypeLabel() }}</text>
</view>
<view class="login-status">
<text class="label">登录状态</text>
<text class="value" :class="{ 'logged-in': hasLogin, 'not-logged-in': !hasLogin }">
{{ hasLogin ? '已登录' : '未登录' }}
</text>
</view>
<view class="debug-info">
<text class="label">调试信息</text>
<text class="value">userType: {{ userInfo?.userType ?? 'undefined' }}</text>
<text class="value">hasLogin: {{ hasLogin }}</text>
<text class="value">shouldShowJobSeeker: {{ shouldShowJobSeekerContent }}</text>
<text class="value">shouldShowCompany: {{ shouldShowCompanyContent }}</text>
</view>
<view class="switch-section">
<text class="section-title">切换用户类型</text>
<view class="switch-buttons">
<button
v-for="(type, index) in userTypes"
:key="index"
@click="switchUserType(type.value)"
:class="{ active: currentUserType === type.value }"
class="switch-btn"
>
{{ type.label }}
</button>
</view>
</view>
<view class="description">
<text class="desc-title">功能说明</text>
<text class="desc-text"> 未登录状态默认显示求职者tabbar职位 + 招聘会 + AI+ + 消息 + 我的</text>
<text class="desc-text"> 企业用户userType=0显示"发布岗位"导航隐藏"招聘会"</text>
<text class="desc-text"> 求职者用户userType=1,2,3显示"招聘会"导航</text>
<text class="desc-text"> 登录后根据用户角色自动切换对应的tabbar</text>
<text class="desc-text"> 系统默认tabbar已被隐藏使用自定义tabbar</text>
</view>
<view class="test-section">
<text class="section-title">测试功能</text>
<button @click="testHideTabBar" class="test-btn">检查系统TabBar状态</button>
<button @click="testShowTabBar" class="test-btn">临时显示系统TabBar</button>
<button @click="testLogin" class="test-btn" v-if="!hasLogin">模拟登录</button>
<button @click="testLogout" class="test-btn" v-if="hasLogin">模拟登出</button>
</view>
</view>
<!-- 自定义tabbar -->
<CustomTabBar :currentPage="0" />
</view>
</template>
<script setup>
import { ref, computed } from 'vue';
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
const userStore = useUserStore();
const { userInfo, hasLogin } = storeToRefs(userStore);
const userTypes = [
{ value: 0, label: '企业用户' },
{ value: 1, label: '求职者' },
{ value: 2, label: '网格员' },
{ value: 3, label: '政府人员' }
];
const currentUserType = computed(() => userInfo.value?.isCompanyUser !== undefined ? userInfo.value.isCompanyUser : 0);
const switchUserType = (userType) => {
console.log('切换用户类型:', userType);
userInfo.value.isCompanyUser = userType;
// 更新到本地存储
uni.setStorageSync('userInfo', userInfo.value);
};
const getCurrentUserTypeLabel = () => {
const type = userTypes.find(t => t.value === currentUserType.value);
return type ? type.label : '未知';
};
// 计算是否显示求职者内容
const shouldShowJobSeekerContent = computed(() => {
if (!hasLogin.value) {
return true;
}
const userType = userInfo.value?.isCompanyUser;
return userType !== 0;
});
// 计算是否显示企业用户内容
const shouldShowCompanyContent = computed(() => {
if (!hasLogin.value) {
return false;
}
const userType = userInfo.value?.isCompanyUser;
return userType === 0;
});
const testHideTabBar = () => {
uni.hideTabBar();
uni.showToast({
title: '系统TabBar已隐藏',
icon: 'success'
});
};
const testShowTabBar = () => {
uni.showTabBar();
uni.showToast({
title: '系统TabBar已显示测试用',
icon: 'none'
});
// 3秒后自动隐藏
setTimeout(() => {
uni.hideTabBar();
}, 3000);
};
const testLogin = () => {
// 模拟登录,设置用户信息
const mockUserInfo = {
userType: 1, // 默认设置为求职者
name: '测试用户',
id: 'test123'
};
userInfo.value = mockUserInfo;
userStore.hasLogin = true;
uni.setStorageSync('userInfo', mockUserInfo);
uni.showToast({
title: '模拟登录成功',
icon: 'success'
});
};
const testLogout = () => {
// 模拟登出,清除用户信息
userStore.logOut(false);
uni.showToast({
title: '模拟登出成功',
icon: 'success'
});
};
</script>
<style lang="scss" scoped>
.tabbar-test {
padding: 40rpx;
min-height: 100vh;
background-color: #f5f5f5;
}
.header {
text-align: center;
margin-bottom: 40rpx;
}
.title {
font-size: 36rpx;
font-weight: bold;
color: #333;
}
.content {
background: white;
border-radius: 20rpx;
padding: 40rpx;
margin-bottom: 40rpx;
}
.user-info, .login-status, .debug-info {
display: flex;
flex-direction: column;
margin-bottom: 20rpx;
padding: 20rpx;
background: #f8f9fa;
border-radius: 10rpx;
}
.debug-info .value {
font-size: 20rpx;
color: #666;
margin: 5rpx 0;
display: block;
}
.label {
font-size: 28rpx;
color: #666;
margin-right: 20rpx;
}
.value {
font-size: 28rpx;
color: #256BFA;
font-weight: bold;
}
.switch-section {
margin-bottom: 40rpx;
}
.section-title {
font-size: 28rpx;
color: #333;
margin-bottom: 20rpx;
display: block;
}
.switch-buttons {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
}
.switch-btn {
padding: 20rpx 30rpx;
background: #f0f0f0;
border: none;
border-radius: 10rpx;
font-size: 24rpx;
color: #666;
transition: all 0.3s;
}
.switch-btn.active {
background: #256BFA;
color: white;
}
.description {
background: #f8f9fa;
padding: 30rpx;
border-radius: 10rpx;
}
.desc-title {
font-size: 26rpx;
color: #333;
font-weight: bold;
margin-bottom: 20rpx;
display: block;
}
.desc-text {
font-size: 24rpx;
color: #666;
line-height: 1.6;
margin-bottom: 10rpx;
display: block;
}
.test-section {
margin-top: 40rpx;
background: #f8f9fa;
padding: 30rpx;
border-radius: 10rpx;
}
.test-btn {
margin: 10rpx 0;
padding: 20rpx 30rpx;
background: #256BFA;
color: white;
border: none;
border-radius: 10rpx;
font-size: 24rpx;
width: 100%;
}
.logged-in {
color: #52c41a !important;
font-weight: bold;
}
.not-logged-in {
color: #ff4d4f !important;
font-weight: bold;
}
</style>

View File

@@ -0,0 +1,232 @@
<template>
<view class="test-page">
<view class="header">
<text class="title">TabBar用户类型切换测试</text>
</view>
<view class="content">
<view class="current-info">
<text class="label">当前用户类型</text>
<text class="value">{{ getCurrentTypeLabel() }} ({{ currentUserType }})</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="tabbar-preview">
<text class="section-title">TabBar预览</text>
<view class="tabbar-container">
<view
v-for="(item, index) in tabbarConfig"
:key="index"
class="tabbar-item"
>
<text class="tabbar-text">{{ item.text }}</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显示"招聘会"导航</text>
<text class="desc-text"> 网格员userType=2显示"招聘会"导航</text>
<text class="desc-text"> 政府人员userType=3显示"招聘会"导航</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?.isCompanyUser !== undefined ? userInfo.value.isCompanyUser : 1);
const switchUserType = (userType) => {
console.log('切换用户类型:', userType);
userInfo.value.isCompanyUser = 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: bold;
color: #333;
}
}
.content {
.current-info {
background: #fff;
padding: 30rpx;
border-radius: 16rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
.label {
font-size: 28rpx;
color: #666;
}
.value {
font-size: 32rpx;
font-weight: bold;
color: #256BFA;
margin-left: 10rpx;
}
}
.type-switcher {
background: #fff;
padding: 30rpx;
border-radius: 16rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
.section-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
display: block;
}
.buttons {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
.btn {
flex: 1;
min-width: 140rpx;
height: 80rpx;
background: #f8f9fa;
border: 2rpx solid #e9ecef;
border-radius: 12rpx;
font-size: 26rpx;
color: #666;
transition: all 0.3s ease;
&.active {
background: #256BFA;
border-color: #256BFA;
color: #fff;
}
}
}
}
.tabbar-preview {
background: #fff;
padding: 30rpx;
border-radius: 16rpx;
margin-bottom: 30rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
.section-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
display: block;
}
.tabbar-container {
display: flex;
background: #f8f9fa;
border-radius: 12rpx;
padding: 20rpx;
.tabbar-item {
flex: 1;
text-align: center;
padding: 20rpx 10rpx;
.tabbar-text {
font-size: 24rpx;
color: #666;
font-weight: 500;
}
}
}
}
.description {
background: #fff;
padding: 30rpx;
border-radius: 16rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.1);
.desc-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 20rpx;
display: block;
}
.desc-text {
font-size: 24rpx;
color: #666;
line-height: 1.6;
display: block;
margin-bottom: 10rpx;
}
}
}
</style>

View File

@@ -117,9 +117,9 @@ const useUserStore = defineStore("user", () => {
hasLogin.value = true;
// 模拟添加用户类型字段,实际项目中应该从接口获取
if (!userInfo.value.userType) {
userInfo.value.userType = 0; // 默认设置为企业用户
}
// if (!userInfo.value.userType) {
// userInfo.value.userType = 1; // 默认设置为求职者用户
// }
// 持久化存储用户信息到本地缓存
uni.setStorageSync('userInfo', values.data);

45
utils/tabbarManager.js Normal file
View File

@@ -0,0 +1,45 @@
// 自定义tabbar管理器
export const tabbarManager = {
// 显示tabbar
showTabBar() {
if (typeof getTabBar === 'function' && getTabBar()) {
getTabBar().setData({
selected: 0
});
}
},
// 更新tabbar选中状态
updateSelected(index) {
if (typeof getTabBar === 'function' && getTabBar()) {
getTabBar().setData({
selected: index
});
}
},
// 根据用户类型更新tabbar
updateTabBarByUserType() {
if (typeof getTabBar === 'function' && getTabBar()) {
const userInfo = uni.getStorageSync('userInfo') || {};
const userType = userInfo.isCompanyUser !== undefined ? userInfo.isCompanyUser : 1;
getTabBar().generateTabbarList(userType);
}
},
// 初始化tabbar
initTabBar() {
// 延迟初始化自定义tabbar
setTimeout(() => {
this.updateTabBarByUserType();
}, 200);
},
// 强制刷新tabbar登录后调用
refreshTabBar() {
// 延迟刷新,确保用户信息已更新
setTimeout(() => {
this.updateTabBarByUserType();
}, 100);
}
};