29 Commits

Author SHA1 Message Date
lip
438a6f3938 t添加高校毕业生模块 2025-10-30 20:38:17 +08:00
lip
05fd135b3c 添加新图标 2025-10-30 20:32:31 +08:00
9c22cc4444 configjs回滚 2025-10-30 19:34:57 +08:00
4a1aa68245 添加后端请求接口
分包packageB
2025-10-30 17:09:26 +08:00
冯辉
95b9c1cc29 111 2025-10-30 15:34:42 +08:00
冯辉
e048ba6b0b 企业详细信息页面编辑联系人功能开发 2025-10-27 13:35:02 +08:00
冯辉
55cc52c046 111 2025-10-27 12:39:33 +08:00
冯辉
c71ad5f98c bug修复 2025-10-24 18:49:03 +08:00
冯辉
476e44f400 111 2025-10-24 18:10:16 +08:00
冯辉
d80baf6f0b 首页岗位列表接口调用两次的问题修改 2025-10-24 17:53:53 +08:00
冯辉
5a31d56c9a 首页切换导航bug修复 2025-10-24 17:37:38 +08:00
冯辉
12906b65ec 发布岗位回显企业名称 2025-10-24 17:30:19 +08:00
冯辉
2d6370b796 首页卡片开发 2025-10-24 16:54:52 +08:00
冯辉
92ee5c5311 企业我的页面开发 2025-10-24 11:34:11 +08:00
冯辉
0b339ee061 发布职位网格员模糊查询企业开发 2025-10-23 18:30:40 +08:00
冯辉
4450aa21bc Merge branch 'main' of http://124.243.245.42:3000/sdz/ks-app-employment-service 2025-10-23 17:16:20 +08:00
冯辉
b72b7dd7d6 岗位发布开发 2025-10-23 17:16:16 +08:00
3e58a71658 删除功能 2025-10-23 14:45:08 +08:00
d605e940ee Merge branch 'main' of http://124.243.245.42:3000/sdz/ks-app-employment-service 2025-10-22 14:23:56 +08:00
e287209dcf 静态页面 2025-10-22 14:23:38 +08:00
冯辉
20f2038f8c 接口乱码问题修改 2025-10-22 13:22:06 +08:00
冯辉
4f94090b42 企业信息补全接口联调 2025-10-22 13:15:10 +08:00
冯辉
8bb3c424e2 企业信息补全页面开发 2025-10-21 22:58:47 +08:00
4d87af9c3c 新增静态页面 2025-10-21 17:42:35 +08:00
冯辉
968e6b4091 阿里图标库引入 2025-10-20 16:15:29 +08:00
冯辉
d9c1f83693 微信授权登录功能开发联调 2025-10-20 11:43:44 +08:00
冯辉
ae91ded327 微信登录调试 2025-10-20 11:05:11 +08:00
冯辉
959e9ee9e4 登录注册逻辑开发 2025-10-17 13:15:14 +08:00
冯辉
14dafac147 首页布局更改 2025-10-16 16:44:30 +08:00
137 changed files with 2805348 additions and 1042 deletions

43
App.vue
View File

@@ -3,32 +3,37 @@ 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();
// 登录
let token = uni.getStorageSync('token') || ''; // 同步获取 缓存信息
if (token) {
useUserStore()
.loginSetToken(token)
.then(() => {
$api.msg('登录成功');
})
.catch(() => {
uni.redirectTo({
url: '/pages/login/login',
// 先尝试从缓存恢复用户信息
const restored = useUserStore().restoreUserInfo();
// 用户信息恢复后再初始化自定义tabbar
tabbarManager.initTabBar();
if (restored) {
// 如果成功恢复用户信息验证token是否有效
let token = uni.getStorageSync('token') || '';
if (token) {
useUserStore()
.loginSetToken(token)
.then(() => {
console.log('用户登录状态已恢复');
})
.catch(() => {
// token无效清除缓存不跳转登录页
console.log('token已过期需要重新登录');
useUserStore().logOut(false);
});
});
} else {
uni.redirectTo({
url: '/pages/login/login',
});
}
}
// 不再强制跳转到登录页,而是在需要登录时弹出授权弹窗
});
onMounted(() => {
@@ -53,6 +58,8 @@ onHide(() => {
/*每个页面公共css */
@import '@/common/animation.css';
@import '@/common/common.css';
/* 引入阿里图标库 */
@import url("/static/iconfont/iconfont.css");
/* 修改pages tabbar样式 H5有效 */
.uni-tabbar .uni-tabbar__item:nth-child(4) .uni-tabbar__bd .uni-tabbar__icon {
height: 110rpx !important;
@@ -81,6 +88,7 @@ uni-modal,
font-display: swap;
}
/* #ifdef H5 */
@font-face {
font-family: PingFangSC-Regular;
src: url('https://qd.zhaopinzao8dian.com/file/csn/PingFangSC-Regular.woff2') format('woff2');
@@ -98,6 +106,7 @@ uni-modal,
src: url('https://qd.zhaopinzao8dian.com/file/csn/DIN-Medium.woff2') format('woff2');
font-display: swap;
}
/* #endif */
body {
font-family: 'PingFangSC-Regular', 'PingFang SC', 'Microsoft YaHei', 'Helvetica Neue', Helvetica, Arial, sans-serif;

87
clean-build-cache.bat Normal file
View File

@@ -0,0 +1,87 @@
@echo off
REM 清理 uni-app 项目编译缓存脚本
echo ================================================
echo uni-app 项目缓存清理工具
echo ================================================
echo.
echo [提示] 此脚本将清理以下内容:
echo 1. unpackage 编译输出目录
echo 2. node_modules 依赖目录(如果需要)
echo 3. HBuilderX 临时文件
echo.
set /p confirm="确认清理缓存吗?(Y/N): "
if /i not "%confirm%"=="Y" (
echo 已取消操作
pause
exit
)
echo.
echo ================================================
echo 开始清理...
echo ================================================
echo.
REM 1. 清理 unpackage 目录
if exist "unpackage" (
echo [1/3] 正在删除 unpackage 目录...
rd /s /q "unpackage"
echo 已删除 unpackage 目录
) else (
echo [1/3] unpackage 目录不存在,跳过
)
echo.
REM 2. 询问是否清理 node_modules
if exist "node_modules" (
set /p cleanNodeModules="[2/3] 检测到 node_modules 目录,是否删除?(Y/N): "
if /i "!cleanNodeModules!"=="Y" (
echo 正在删除 node_modules 目录...
rd /s /q "node_modules"
echo 已删除 node_modules 目录
echo 提示:如需重新安装,请运行 npm install
) else (
echo 跳过 node_modules 目录
)
) else (
echo [2/3] node_modules 目录不存在,跳过
)
echo.
REM 3. 清理 HBuilderX 临时文件
echo [3/3] 清理 HBuilderX 临时文件...
REM 删除可能的临时文件和缓存
if exist ".hbuilderx" (
rd /s /q ".hbuilderx"
echo 已删除 .hbuilderx 目录
)
if exist ".temp" (
rd /s /q ".temp"
echo 已删除 .temp 目录
)
if exist ".cache" (
rd /s /q ".cache"
echo 已删除 .cache 目录
)
echo 完成
echo.
echo ================================================
echo 清理完成!
echo ================================================
echo.
echo 建议操作:
echo 1. 重启 HBuilderX
echo 2. 重新编译项目
echo 3. 如果清理了 node_modules请先运行 npm install
echo.
echo 按任意键退出...
pause >nul

View File

@@ -111,6 +111,23 @@ class UniStorageHelper {
return storeData.filter(item => item[fieldName] === value);
}
async getRecordCount(storeName) {
const storeData = this._storageGet(this._getStoreKey(storeName)) || [];
return storeData.length;
}
async deleteOldestRecord(storeName) {
const storeKey = this._getStoreKey(storeName);
const storeData = this._storageGet(storeKey) || [];
if (storeData.length > 0) {
// 删除第一条记录(最早的记录)
const newData = storeData.slice(1);
this._storageSet(storeKey, newData);
this._log(`删除最早的记录,剩余${newData.length}条记录`);
}
return Promise.resolve();
}
/*==================
更新/删除方法
==================*/

View File

@@ -7,6 +7,42 @@ page {
overflow: hidden;
}
/* 安全区域适配 - 通用解决方案 */
/* #ifdef MP-WEIXIN */
.safe-area-container {
padding-top: env(safe-area-inset-top);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
padding-bottom: env(safe-area-inset-bottom);
}
.safe-area-top {
padding-top: env(safe-area-inset-top);
}
.safe-area-left {
padding-left: env(safe-area-inset-left);
}
.safe-area-right {
padding-right: env(safe-area-inset-right);
}
.safe-area-bottom {
padding-bottom: env(safe-area-inset-bottom);
}
.safe-area-horizontal {
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
}
.safe-area-vertical {
padding-top: env(safe-area-inset-top);
padding-bottom: env(safe-area-inset-bottom);
}
/* #endif */
/* 禁止页面回弹 */
/* html,
body,

View File

@@ -68,7 +68,7 @@ export const navTo = function(url, {
if (needLogin && !userStore.hasLogin) {
uni.navigateTo({
url: '/pages/login/login'
url: '/pages/complete-info/complete-info'
});
return;
}

View File

@@ -6,6 +6,7 @@
>
<!-- 顶部头部区域 -->
<view
v-if="title"
class="container-header"
:style="border ? { borderBottom: `2rpx solid ${borderColor}` } : { borderBottom: 'none' }"
>
@@ -49,7 +50,7 @@ const emit = defineEmits(['onScrollBottom']);
defineProps({
title: {
type: String,
default: '标题',
default: '',
},
border: {
type: Boolean,

View File

@@ -0,0 +1,337 @@
<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') || {};
// 获取isCompanyUser字段
const storeIsCompanyUser = userInfo.value?.isCompanyUser;
const cachedIsCompanyUser = cachedUserInfo.isCompanyUser;
// 获取用户类型的逻辑:
// 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);
};
// 监听用户类型变化只监听isCompanyUser字段
watch(() => userInfo.value?.isCompanyUser, (newIsCompanyUser, oldIsCompanyUser) => {
if (newIsCompanyUser !== oldIsCompanyUser) {
// 强制触发computed重新计算
forceRefresh();
}
}, { immediate: true });
// 监听用户信息变化(包括登录状态)
watch(() => userInfo.value, (newUserInfo, oldUserInfo) => {
if (newUserInfo !== oldUserInfo) {
// 强制触发computed重新计算
forceRefresh();
}
}, { immediate: true, deep: true });
// 切换tab
const switchTab = (item, index) => {
// 检查是否为"发布岗位"页面,需要判断企业信息是否完整
if (item.path === '/pages/job/publishJob') {
// 检查用户是否已登录
const token = uni.getStorageSync('token') || '';
const hasLogin = userStore.hasLogin;
if (!token || !hasLogin) {
// 未登录,发送事件显示登录弹窗
uni.$emit('showLoginModal');
return; // 不进行页面跳转
}
// 已登录,检查企业信息是否完整
const cachedUserInfo = uni.getStorageSync('userInfo') || {};
const storeUserInfo = userInfo.value || {};
const currentUserInfo = storeUserInfo.id ? storeUserInfo : cachedUserInfo;
// 判断企业信息字段company是否为null或undefined
if (!currentUserInfo.company || currentUserInfo.company === null) {
// 企业信息为空,跳转到企业信息补全页面
uni.navigateTo({
url: '/pages/complete-info/company-info',
});
} else {
// 企业信息完整,跳转到发布岗位页面
uni.navigateTo({
url: '/pages/job/publishJob',
});
}
currentItem.value = item.id;
return;
}
// 检查是否为"我的"页面,需要登录验证和用户类型判断
if (item.path === '/pages/mine/mine') {
// 检查用户是否已登录
const token = uni.getStorageSync('token') || '';
const hasLogin = userStore.hasLogin;
if (!token || !hasLogin) {
// 未登录,发送事件显示登录弹窗
uni.$emit('showLoginModal');
return; // 不进行页面跳转
}
// 已登录,根据用户类型跳转到不同的"我的"页面
const cachedUserInfo = uni.getStorageSync('userInfo') || {};
const storeIsCompanyUser = userInfo.value?.isCompanyUser;
const cachedIsCompanyUser = cachedUserInfo.isCompanyUser;
// 获取用户类型
const userType = Number(storeIsCompanyUser !== undefined ? storeIsCompanyUser : (cachedIsCompanyUser !== undefined ? cachedIsCompanyUser : 1));
let targetPath = '/pages/mine/mine'; // 默认求职者页面
if (userType === 0) {
// 企业用户,跳转到企业我的页面
targetPath = '/pages/mine/company-mine';
} else {
// 求职者或其他用户类型,跳转到普通我的页面
targetPath = '/pages/mine/mine';
}
// 跳转到对应的页面
uni.navigateTo({
url: targetPath,
});
currentItem.value = item.id;
return;
}
// 判断是否为 tabBar 页面
const tabBarPages = [
'/pages/index/index',
'/pages/careerfair/careerfair',
'/pages/chat/chat',
'/pages/msglog/msglog',
'/pages/mine/mine'
];
if (tabBarPages.includes(item.path)) {
// TabBar 页面使用 redirectTo 避免页面栈溢出
uni.redirectTo({
url: item.path,
});
} else {
// 非 TabBar 页面使用 navigateTo
uni.navigateTo({
url: item.path,
});
}
currentItem.value = item.id;
};
onMounted(() => {
currentItem.value = props.currentPage;
// 调试信息显示当前用户状态和tabbar配置
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

@@ -0,0 +1,70 @@
<template>
<text class="iconfont" :class="iconClass" :style="iconStyle" @click="handleClick"></text>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
// 图标名称home、user、search
name: {
type: String,
required: true
},
// 图标大小单位rpx
size: {
type: [String, Number],
default: 32
},
// 图标颜色
color: {
type: String,
default: ''
},
// 是否粗体
bold: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['click'])
// 图标类名
const iconClass = computed(() => {
const prefix = props.name.startsWith('icon-') ? '' : 'icon-'
return `${prefix}${props.name}`
})
// 图标样式
const iconStyle = computed(() => {
const style = {
fontSize: `${props.size}rpx`
}
if (props.color) {
style.color = props.color
}
if (props.bold) {
style.fontWeight = 'bold'
}
return style
})
// 点击事件
const handleClick = (e) => {
emit('click', e)
}
</script>
<style scoped>
.iconfont {
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
display: inline-block;
}
</style>

View File

@@ -13,10 +13,10 @@
</view>
<text class="text-content button-click">{{ content }}</text>
<template v-if="showButton">
<uni-button class="popup-button button-click" v-if="isTip" @click="close">{{ buttonText }}</uni-button>
<button class="popup-button button-click" v-if="isTip" @click="close">{{ buttonText }}</button>
<view v-else class="confirm-btns">
<uni-button class="popup-button button-click" @click="close">{{ cancelText }}</uni-button>
<uni-button class="popup-button button-click" @click="confirm">{{ confirmText }}</uni-button>
<button class="popup-button button-click" @click="close">{{ cancelText }}</button>
<button class="popup-button button-click" @click="confirm">{{ confirmText }}</button>
</view>
</template>
</view>
@@ -137,4 +137,18 @@ export default {
}
}
}
// 重置button样式
button {
padding: 0;
margin: 0;
border: none;
background: none;
font-size: inherit;
line-height: inherit;
}
button::after {
border: none;
}
</style>

View File

@@ -0,0 +1,100 @@
<template>
<view class="user-type-switcher">
<view class="switcher-title">用户类型切换测试用</view>
<view class="switcher-buttons">
<button
v-for="(type, index) in userTypes"
:key="index"
:class="['type-btn', { active: currentUserType === type.value }]"
@click="switchUserType(type.value)"
>
{{ type.label }}
</button>
</view>
<view class="current-type">
当前用户类型{{ getCurrentTypeLabel() }} ({{ currentUserType }})
</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 : 0);
const switchUserType = (userType) => {
console.log('切换用户类型:', userType);
console.log('切换前 userInfo:', userInfo.value);
userInfo.value.isCompanyUser = userType;
console.log('切换后 userInfo:', userInfo.value);
// 保存到本地存储
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 : '未知';
};
</script>
<style lang="scss" scoped>
.user-type-switcher {
padding: 20rpx;
background: #f5f5f5;
border-radius: 10rpx;
margin: 20rpx;
.switcher-title {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 20rpx;
}
.switcher-buttons {
display: flex;
flex-wrap: wrap;
gap: 10rpx;
margin-bottom: 20rpx;
.type-btn {
padding: 10rpx 20rpx;
border: 2rpx solid #ddd;
border-radius: 8rpx;
background: #fff;
font-size: 24rpx;
color: #666;
&.active {
background: #256BFA;
color: #fff;
border-color: #256BFA;
}
}
}
.current-type {
font-size: 24rpx;
color: #666;
}
}
</style>

View File

@@ -0,0 +1,250 @@
# 五级联动地址选择器组件
## 组件简介
`area-cascade-picker` 是一个支持省市区县街道社区的五级联动地址选择组件,适用于需要选择详细地址的场景。
## 功能特点
- ✅ 五级联动选择(省/市/区/街道/社区)
- ✅ 自动级联更新
- ✅ 支持取消和确认操作
- ✅ 底部弹出式交互
- ✅ 支持自定义标题
- ✅ 返回完整的地址信息和各级代码
## 使用方法
### 1. 引入组件
```vue
<template>
<view>
<button @click="openPicker">选择地址</button>
<area-cascade-picker ref="areaPicker"></area-cascade-picker>
</view>
</template>
<script setup>
import { ref } from 'vue'
import AreaCascadePicker from '@/components/area-cascade-picker/area-cascade-picker.vue'
const areaPicker = ref(null)
</script>
```
### 2. 打开选择器
```javascript
const openPicker = () => {
areaPicker.value?.open({
title: '选择地址',
maskClick: true,
success: (addressData) => {
console.log('选择的地址:', addressData)
// 处理选择结果
},
cancel: () => {
console.log('取消选择')
},
change: (addressData) => {
console.log('地址变化:', addressData)
}
})
}
```
## API 说明
### open(config)
打开地址选择器
#### 参数 config
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| title | String | 否 | '选择地址' | 选择器标题 |
| maskClick | Boolean | 否 | false | 是否允许点击遮罩关闭 |
| success | Function | 否 | - | 确认选择的回调函数 |
| cancel | Function | 否 | - | 取消选择的回调函数 |
| change | Function | 否 | - | 选择变化的回调函数 |
| defaultValue | Object | 否 | null | 默认选中的地址(暂未实现) |
#### success 回调参数
```javascript
{
address: "新疆维吾尔自治区/喀什地区/喀什市/学府街道/学府社区居委会",
province: {
code: "650000",
name: "新疆维吾尔自治区"
},
city: {
code: "653100",
name: "喀什地区"
},
district: {
code: "653101",
name: "喀什市"
},
street: {
code: "65310101",
name: "学府街道"
},
community: {
code: "6531010101",
name: "学府社区居委会"
}
}
```
### close()
关闭地址选择器
```javascript
areaPicker.value?.close()
```
## 数据格式
组件使用树形结构的地址数据,格式如下:
```javascript
[
{
code: '650000', // 行政区划代码
name: '新疆维吾尔自治区', // 名称
children: [ // 下级行政区
{
code: '653100',
name: '喀什地区',
children: [
{
code: '653101',
name: '喀什市',
children: [
{
code: '65310101',
name: '学府街道',
children: [
{
code: '6531010101',
name: '学府社区居委会'
}
]
}
]
}
]
}
]
}
]
```
## 完整示例
### 企业注册地址选择
```vue
<template>
<view class="form-item" @click="selectLocation">
<text class="label">企业注册地点</text>
<view class="input-content">
<text :class="{ placeholder: !formData.address }">
{{ formData.address || '请选择注册地点' }}
</text>
<uni-icons type="arrowright" size="16"></uni-icons>
</view>
</view>
<area-cascade-picker ref="areaPicker"></area-cascade-picker>
</template>
<script setup>
import { ref, reactive } from 'vue'
import AreaCascadePicker from '@/components/area-cascade-picker/area-cascade-picker.vue'
const areaPicker = ref(null)
const formData = reactive({
address: '',
provinceCode: '',
provinceName: '',
cityCode: '',
cityName: '',
districtCode: '',
districtName: '',
streetCode: '',
streetName: '',
communityCode: '',
communityName: ''
})
const selectLocation = () => {
areaPicker.value?.open({
title: '选择企业注册地点',
maskClick: true,
success: (addressData) => {
// 保存完整地址
formData.address = addressData.address
// 保存各级信息
formData.provinceCode = addressData.province?.code
formData.provinceName = addressData.province?.name
formData.cityCode = addressData.city?.code
formData.cityName = addressData.city?.name
formData.districtCode = addressData.district?.code
formData.districtName = addressData.district?.name
formData.streetCode = addressData.street?.code
formData.streetName = addressData.street?.name
formData.communityCode = addressData.community?.code
formData.communityName = addressData.community?.name
console.log('已选择地址:', formData)
}
})
}
</script>
```
## 注意事项
1. **数据来源**当前使用本地模拟数据生产环境建议接入后端API
2. **数据更新**如需接入后端API修改 `loadAreaData` 方法即可
3. **性能优化**:地址数据量大时,建议使用懒加载
4. **兼容性**:支持 H5、微信小程序等多端
## 接入后端API
在组件的 `loadAreaData` 方法中取消注释并配置API
```javascript
async loadAreaData() {
try {
const resp = await uni.request({
url: '/app/common/area/cascade',
method: 'GET'
});
if (resp.statusCode === 200 && resp.data && resp.data.data) {
this.areaData = resp.data.data;
return;
}
} catch (error) {
console.error('加载地区数据失败:', error);
}
// 失败时使用模拟数据
this.areaData = this.getMockData();
}
```
## 更新日志
### v1.0.0 (2025-10-21)
- ✨ 初始版本
- ✅ 实现五级联动选择功能
- ✅ 支持省市区县街道社区选择
- ✅ 提供完整的地址信息返回

View File

@@ -0,0 +1,426 @@
<template>
<uni-popup
ref="popup"
type="bottom"
borderRadius="10px 10px 0 0"
background-color="#FFFFFF"
:mask-click="maskClick"
>
<view class="popup-content">
<view class="popup-header">
<view class="btn-cancel" @click="cancel">取消</view>
<view class="title">{{ title }}</view>
<view class="btn-confirm" @click="confirm">确认</view>
</view>
<view class="popup-list">
<picker-view
indicator-style="height: 84rpx;"
:value="selectedIndex"
@change="bindChange"
class="picker-view"
>
<!-- -->
<picker-view-column>
<view
v-for="(item, index) in provinceList"
:key="item.code"
class="item"
:class="{ 'item-active': selectedIndex[0] === index }"
>
<text>{{ item.name }}</text>
</view>
</picker-view-column>
<!-- -->
<picker-view-column>
<view
v-for="(item, index) in cityList"
:key="item.code"
class="item"
:class="{ 'item-active': selectedIndex[1] === index }"
>
<text>{{ item.name }}</text>
</view>
</picker-view-column>
<!-- 区县 -->
<picker-view-column>
<view
v-for="(item, index) in districtList"
:key="item.code"
class="item"
:class="{ 'item-active': selectedIndex[2] === index }"
>
<text>{{ item.name }}</text>
</view>
</picker-view-column>
<!-- 街道 -->
<picker-view-column>
<view
v-for="(item, index) in streetList"
:key="item.code"
class="item"
:class="{ 'item-active': selectedIndex[3] === index }"
>
<text>{{ item.name }}</text>
</view>
</picker-view-column>
<!-- 社区/居委会 -->
<picker-view-column>
<view
v-for="(item, index) in communityList"
:key="item.code"
class="item"
:class="{ 'item-active': selectedIndex[4] === index }"
>
<text>{{ item.name }}</text>
</view>
</picker-view-column>
</picker-view>
</view>
</view>
</uni-popup>
</template>
<script>
import addressJson from '@/static/json/xinjiang.json';
export default {
name: 'AreaCascadePicker',
data() {
return {
maskClick: false,
title: '选择地址',
confirmCallback: null,
cancelCallback: null,
changeCallback: null,
selectedIndex: [0, 0, 0, 0, 0],
// 原始数据
areaData: [],
// 各级列表
provinceList: [],
cityList: [],
districtList: [],
streetList: [],
communityList: [],
// 当前选中的项
selectedProvince: null,
selectedCity: null,
selectedDistrict: null,
selectedStreet: null,
selectedCommunity: null,
};
},
methods: {
async open(newConfig = {}) {
const {
title,
success,
cancel,
change,
maskClick = false,
defaultValue = null,
} = newConfig;
this.reset();
if (title) this.title = title;
if (typeof success === 'function') this.confirmCallback = success;
if (typeof cancel === 'function') this.cancelCallback = cancel;
if (typeof change === 'function') this.changeCallback = change;
this.maskClick = maskClick;
// 加载地区数据
await this.loadAreaData();
// 初始化列表
this.initLists();
this.$nextTick(() => {
this.$refs.popup.open();
});
},
async loadAreaData() {
try {
// 尝试调用后端API获取地区数据
// 如果后端API不存在将使用模拟数据
console.log('正在加载地区数据...');
// const resp = await uni.request({
// url: '/app/common/area/cascade',
// method: 'GET'
// });
// if (resp.statusCode === 200 && resp.data && resp.data.data) {
// this.areaData = resp.data.data;
// }
// 暂时使用模拟数据
this.areaData = this.getMockData();
} catch (error) {
console.error('加载地区数据失败:', error);
// 如果后端API不存在使用模拟数据
this.areaData = this.getMockData();
}
},
initLists() {
// 初始化省列表
this.provinceList = this.areaData;
if (this.provinceList.length > 0) {
this.selectedProvince = this.provinceList[0];
this.updateCityList();
}
},
updateCityList() {
if (!this.selectedProvince || !this.selectedProvince.children) {
this.cityList = [];
this.districtList = [];
this.streetList = [];
this.communityList = [];
return;
}
this.cityList = this.selectedProvince.children;
this.selectedIndex[1] = 0;
if (this.cityList.length > 0) {
this.selectedCity = this.cityList[0];
this.updateDistrictList();
}
},
updateDistrictList() {
if (!this.selectedCity || !this.selectedCity.children) {
this.districtList = [];
this.streetList = [];
this.communityList = [];
return;
}
this.districtList = this.selectedCity.children;
this.selectedIndex[2] = 0;
if (this.districtList.length > 0) {
this.selectedDistrict = this.districtList[0];
this.updateStreetList();
}
},
updateStreetList() {
if (!this.selectedDistrict || !this.selectedDistrict.children) {
this.streetList = [];
this.communityList = [];
return;
}
this.streetList = this.selectedDistrict.children;
this.selectedIndex[3] = 0;
if (this.streetList.length > 0) {
this.selectedStreet = this.streetList[0];
this.updateCommunityList();
}
},
updateCommunityList() {
if (!this.selectedStreet || !this.selectedStreet.children) {
this.communityList = [];
return;
}
this.communityList = this.selectedStreet.children;
this.selectedIndex[4] = 0;
if (this.communityList.length > 0) {
this.selectedCommunity = this.communityList[0];
}
},
bindChange(e) {
const newIndex = e.detail.value;
// 检查哪一列发生了变化
for (let i = 0; i < 5; i++) {
if (newIndex[i] !== this.selectedIndex[i]) {
this.selectedIndex[i] = newIndex[i];
// 根据变化的列更新后续列
if (i === 0) {
// 省变化
this.selectedProvince = this.provinceList[newIndex[0]];
this.updateCityList();
} else if (i === 1) {
// 市变化
this.selectedCity = this.cityList[newIndex[1]];
this.updateDistrictList();
} else if (i === 2) {
// 区县变化
this.selectedDistrict = this.districtList[newIndex[2]];
this.updateStreetList();
} else if (i === 3) {
// 街道变化
this.selectedStreet = this.streetList[newIndex[3]];
this.updateCommunityList();
} else if (i === 4) {
// 社区变化
this.selectedCommunity = this.communityList[newIndex[4]];
}
break;
}
}
if (this.changeCallback) {
this.changeCallback(this.getSelectedAddress());
}
},
getSelectedAddress() {
const parts = [];
if (this.selectedProvince) parts.push(this.selectedProvince.name);
if (this.selectedCity) parts.push(this.selectedCity.name);
if (this.selectedDistrict) parts.push(this.selectedDistrict.name);
if (this.selectedStreet) parts.push(this.selectedStreet.name);
if (this.selectedCommunity) parts.push(this.selectedCommunity.name);
return {
address: parts.join('/'),
province: this.selectedProvince,
city: this.selectedCity,
district: this.selectedDistrict,
street: this.selectedStreet,
community: this.selectedCommunity,
};
},
close() {
this.$refs.popup.close();
},
cancel() {
this.clickCallback(this.cancelCallback);
},
confirm() {
this.clickCallback(this.confirmCallback);
},
async clickCallback(callback) {
if (typeof callback !== 'function') {
this.$refs.popup.close();
return;
}
try {
const result = await callback(this.getSelectedAddress());
if (result !== false) {
this.$refs.popup.close();
}
} catch (error) {
console.error('callback 执行出错:', error);
}
},
reset() {
this.maskClick = false;
this.confirmCallback = null;
this.cancelCallback = null;
this.changeCallback = null;
this.selectedIndex = [0, 0, 0, 0, 0];
this.provinceList = [];
this.cityList = [];
this.districtList = [];
this.streetList = [];
this.communityList = [];
},
// 模拟数据(用于演示)
getMockData() {
return addressJson
}
},
};
</script>
<style lang="scss" scoped>
.popup-content {
color: #000000;
height: 60vh;
}
.popup-list {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
justify-content: space-evenly;
flex: 1;
overflow: hidden;
.picker-view {
width: 100%;
height: calc(60vh - 100rpx);
margin-top: 20rpx;
.uni-picker-view-mask {
background: rgba(0, 0, 0, 0);
}
.item {
line-height: 84rpx;
height: 84rpx;
text-align: center;
font-weight: 400;
font-size: 28rpx;
color: #cccccc;
padding: 0 4rpx;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.item-active {
color: #333333;
font-weight: 500;
}
.uni-picker-view-indicator:after {
border-color: #e3e3e3;
}
.uni-picker-view-indicator:before {
border-color: #e3e3e3;
}
}
}
.popup-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 40rpx 40rpx 10rpx 40rpx;
.title {
font-weight: 500;
font-size: 36rpx;
color: #333333;
text-align: center;
}
.btn-cancel {
font-weight: 400;
font-size: 32rpx;
color: #666d7f;
line-height: 38rpx;
}
.btn-confirm {
font-weight: 400;
font-size: 32rpx;
color: #256bfa;
}
}
</style>

View File

@@ -116,6 +116,7 @@ const handleItemClick = (e) => {
}
/* 表格 */
/* #ifndef MP-WEIXIN */
table {
// display: block; /* 让表格可滚动 */
// width: 100%;
@@ -147,6 +148,7 @@ tr:hover {
background-color: #f1f1f1;
transition: 0.3s;
}
/* #endif */
/* 代码块 */
pre,
@@ -207,11 +209,13 @@ pre code:empty {
cursor: pointer;
border-radius: 6rpx;
}
/* #ifndef MP-WEIXIN */
.copy-btn:hover {
background: rgba(255, 255, 255, 0.2);
color: #259939;
text-decoration: underline;
}
/* #endif */
pre.hljs {
padding: 0 24rpx;
@@ -246,6 +250,7 @@ ol {
text-align: right;
}
/* #ifndef MP-WEIXIN */
#markdown-content ::v-deep div > pre:first-of-type {
margin-top: 20rpx;
}
@@ -254,6 +259,7 @@ ol {
display: flex;
flex-direction: column;
}
/* #endif */
.markdownRich > div {
display: flex;
flex-direction: column;

View File

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

View File

@@ -62,7 +62,7 @@
</template>
<script setup>
import { inject, computed, toRaw, ref, defineExpose } from 'vue';
import { inject, computed, toRaw, ref } from 'vue';
const { insertSortData, navTo, vacanciesTo } = inject('globalFunction');
import { useRecommedIndexedDBStore } from '@/stores/useRecommedIndexedDBStore.js';
const recommedIndexDb = useRecommedIndexedDBStore();
@@ -103,7 +103,7 @@ function nextDetail(job) {
const recordData = recommedIndexDb.JobParameter(job);
recommedIndexDb.addRecord(recordData);
}
navTo(`/packageA/pages/post/post?jobId=${btoa(job.jobId)}`);
navTo(`/packageA/pages/post/post?jobId=${encodeURIComponent(job.jobId)}`);
}
function toggleSelect(jobId) {
@@ -122,7 +122,7 @@ function handleCardClick(job, e) {
const recordData = recommedIndexDb.JobParameter(job);
recommedIndexDb.addRecord(recordData);
}
navTo(`/packageA/pages/post/post?jobId=${btoa(job.jobId)}`);
navTo(`/packageA/pages/post/post?jobId=${encodeURIComponent(job.jobId)}`);
}
// 新增:提供选中状态和切换方法给父组件

View File

@@ -72,6 +72,16 @@ import { ref, reactive, nextTick, onBeforeMount } from 'vue';
import useDictStore from '@/stores/useDictStore';
const { getTransformChildren } = useDictStore();
// 岗位类型数据
const getJobTypeData = () => {
return [
{ label: '常规岗位', value: 0, text: '常规岗位' },
{ label: '就业见习岗位', value: 1, text: '就业见习岗位' },
{ label: '实习实训岗位', value: 2, text: '实习实训岗位' },
{ label: '社区实践岗位', value: 3, text: '社区实践岗位' }
];
};
const area = ref(true);
const maskClick = ref(false);
const maskClickFn = ref(null);
@@ -164,6 +174,12 @@ function getoptions() {
if (area.value) {
arr.push(getTransformChildren('area', '区域'));
}
// 添加岗位类型选项
arr.push({
label: '岗位类型',
key: 'jobType',
options: getJobTypeData()
});
filterOptions.value = arr;
activeTab.value = 'education';
}
@@ -185,13 +201,7 @@ defineExpose({
<style lang="scss" scoped>
.popup-fix {
position: fixed !important;
left: 0;
right: 0;
bottom: 0;
top: 0;
height: 100vh;
z-index: 9999;
z-index: 9999 !important;
}
.popup-content {
color: #000000;

View File

@@ -32,7 +32,7 @@
</template>
<script setup>
import { ref, reactive, computed, inject, nextTick, defineExpose, onMounted } from 'vue';
import { ref, reactive, computed, inject, nextTick, onMounted } from 'vue';
const { $api, navTo, setCheckedNodes, cloneDeep } = inject('globalFunction');
import useUserStore from '@/stores/useUserStore';
import { storeToRefs } from 'pinia';

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,8 +19,11 @@
</template>
<script setup>
import { ref, defineProps, 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';
const props = defineProps({
currentpage: {
type: Number,
@@ -28,65 +31,139 @@ const props = defineProps({
default: 0,
},
});
const readMsg = useReadMsg();
const { userInfo } = storeToRefs(useUserStore());
const currentItem = ref(0);
const tabbarList = computed(() => [
{
id: 0,
text: '首页',
path: '/pages/index/index',
iconPath: '../../static/tabbar/calendar.png',
selectedIconPath: '../../static/tabbar/calendared.png',
centerItem: false,
badge: readMsg.badges[0].count,
},
{
id: 1,
text: '招聘会',
path: '/pages/careerfair/careerfair',
iconPath: '../../static/tabbar/post.png',
selectedIconPath: '../../static/tabbar/posted.png',
centerItem: false,
badge: readMsg.badges[1].count,
},
{
id: 2,
text: '',
path: '/pages/chat/chat',
iconPath: '../../static/tabbar/logo3.png',
selectedIconPath: '../../static/tabbar/logo3.png',
centerItem: true,
badge: readMsg.badges[2].count,
},
{
id: 3,
text: '消息',
path: '/pages/msglog/msglog',
iconPath: '../../static/tabbar/chat4.png',
selectedIconPath: '../../static/tabbar/chat4ed.png',
centerItem: false,
badge: readMsg.badges[3].count,
},
{
id: 4,
text: '我的',
path: '/pages/mine/mine',
iconPath: '../../static/tabbar/mine.png',
selectedIconPath: '../../static/tabbar/mined.png',
centerItem: false,
badge: readMsg.badges[4].count,
},
]);
// 监听用户类型变化重新生成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: '职位2',
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: '消息2',
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,
},
];
// 根据用户类型添加不同的导航项,未登录时默认为求职者
const userType = userInfo.value?.isCompanyUser !== undefined ? userInfo.value.isCompanyUser : 1;
if (userType === 0) {
// 企业用户:显示发布岗位,隐藏招聘会
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();
});
onMounted(() => {
uni.hideTabBar();
// 自定义TabBar不需要调用hideTabBar因为已经在pages.json中设置了custom: true
// uni.hideTabBar(); // 移除这行避免在自定义TabBar模式下调用
currentItem.value = props.currentpage;
});
const changeItem = (item) => {
uni.switchTab({
url: item.path,
});
// 判断是否为 TabBar 页面
const tabBarPages = [
'/pages/index/index',
'/pages/careerfair/careerfair',
'/pages/chat/chat',
'/pages/msglog/msglog',
'/pages/mine/mine'
];
if (tabBarPages.includes(item.path)) {
// TabBar 页面使用 redirectTo 避免页面栈溢出
uni.redirectTo({
url: item.path,
});
} else {
// 非 TabBar 页面使用 navigateTo
uni.navigateTo({
url: item.path,
});
}
};
</script>

View File

@@ -0,0 +1,564 @@
<template>
<uni-popup ref="popup" type="center" :mask-click="false">
<view class="auth-modal">
<view class="modal-content">
<!-- 关闭按钮 -->
<view class="close-btn" @click="close">
<uni-icons type="closeempty" size="24" color="#999"></uni-icons>
</view>
<!-- Logo和标题 -->
<view class="auth-header">
<image class="auth-logo" src="@/static/logo.png" mode="aspectFit"></image>
<view class="auth-title">欢迎使用就业服务</view>
<view class="auth-subtitle">需要您授权手机号登录</view>
</view>
<!-- 角色选择 -->
<view class="role-select">
<view class="role-title">请选择您的角色</view>
<view class="role-options">
<view
class="role-item"
:class="{ active: userType === 1 }"
@click="selectRole(1)"
>
<view class="role-icon">
<uni-icons type="person" size="32" :color="userType === 1 ? '#256BFA' : '#999'"></uni-icons>
</view>
<view class="role-text">我是求职者</view>
</view>
<view
class="role-item"
:class="{ active: userType === 0 }"
@click="selectRole(0)"
>
<view class="role-icon">
<uni-icons type="shop" size="32" :color="userType === 0 ? '#256BFA' : '#999'"></uni-icons>
</view>
<view class="role-text">我是招聘者</view>
</view>
</view>
</view>
<!-- 授权说明 -->
<view class="auth-tips">
<view class="tip-item">
<uni-icons type="checkmarkempty" size="16" color="#256BFA"></uni-icons>
<text>保护您的个人信息安全</text>
</view>
<view class="tip-item">
<uni-icons type="checkmarkempty" size="16" color="#256BFA"></uni-icons>
<text>为您推荐更合适的岗位</text>
</view>
<view class="tip-item">
<uni-icons type="checkmarkempty" size="16" color="#256BFA"></uni-icons>
<text>享受完整的就业服务</text>
</view>
</view>
<!-- 授权按钮 -->
<view class="auth-actions">
<!-- 微信小程序使用 open-type="getPhoneNumber" -->
<!-- #ifdef MP-WEIXIN -->
<button
class="auth-btn primary"
open-type="getPhoneNumber"
@getphonenumber="getPhoneNumber"
>
<uni-icons type="phone" size="20" color="#FFFFFF"></uni-icons>
<text>微信授权登录</text>
</button>
<!-- #endif -->
<!-- H5和App使用普通按钮 -->
<!-- #ifndef MP-WEIXIN -->
<button class="auth-btn primary" @click="wxLogin">
<uni-icons type="phone" size="20" color="#FFFFFF"></uni-icons>
<text>微信授权登录</text>
</button>
<!-- #endif -->
<!-- 测试登录按钮仅开发环境 -->
<!-- #ifdef APP-PLUS || H5 -->
<button class="auth-btn secondary" @click="testLogin">
<text>测试账号登录</text>
</button>
<!-- #endif -->
</view>
<!-- 用户协议 -->
<view class="auth-agreement">
<text>登录即表示同意</text>
<text class="link" @click="openAgreement('user')">用户协议</text>
<text></text>
<text class="link" @click="openAgreement('privacy')">隐私政策</text>
</view>
</view>
</view>
</uni-popup>
</template>
<script setup>
import { ref, inject } from 'vue';
import useUserStore from '@/stores/useUserStore';
import { tabbarManager } from '@/utils/tabbarManager';
const { $api } = inject('globalFunction');
const { loginSetToken } = useUserStore();
const popup = ref(null);
const userType = ref(null); // 用户角色1-求职者0-企业
const emit = defineEmits(['success', 'cancel']);
// 打开弹窗
const open = () => {
popup.value?.open();
userType.value = null; // 重置角色选择
};
// 关闭弹窗
const close = () => {
popup.value?.close();
emit('cancel');
};
// 选择角色
const selectRole = (type) => {
userType.value = type;
};
// 验证角色是否已选择
const validateRole = () => {
if (userType.value === null) {
$api.msg('请先选择您的角色');
return false;
}
return true;
};
const getPhoneNumber = (e) => {
console.log('获取手机号:', e);
// 验证角色是否已选择
if (!validateRole()) {
return;
}
if (e.detail.errMsg === 'getPhoneNumber:ok') {
uni.login({
provider: 'weixin',
success: (loginRes) => {
console.log('微信登录code获取成功', loginRes.code);
const { encryptedData, iv } = e.detail;
const code = loginRes.code; // 使用wx.login返回的code
// 调用后端接口进行登录
uni.showLoading({ title: '登录中...' });
$api.createRequest('/app/appLogin', {
code,
encryptedData,
iv,
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.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 && !resData.idCard) {
// 招聘者跳转到企业信息补全页面
uni.navigateTo({
url: '/pages/complete-info/company-info'
});
}
}
}).catch(() => {
$api.msg('获取用户信息失败');
});
} else {
$api.msg('登录失败,请重试');
}
}).catch((err) => {
uni.hideLoading();
$api.msg(err.msg || '登录失败,请重试');
});
},
fail: (err) => {
console.error('获取微信登录code失败', err);
$api.msg('获取登录信息失败,请重试');
}
});
} else if (e.detail.errMsg === 'getPhoneNumber:fail user deny') {
$api.msg('您取消了授权');
} else {
$api.msg('获取手机号失败');
}
};
// H5/App 微信登录
const wxLogin = () => {
// 验证角色是否已选择
if (!validateRole()) {
return;
}
// #ifdef H5
// H5网页微信登录逻辑
uni.showLoading({ title: '登录中...' });
// 获取微信授权code
uni.login({
provider: 'weixin',
success: (loginRes) => {
console.log('微信登录成功:', loginRes);
// 调用后端接口进行登录
$api.createRequest('/app/appLogin', {
code: loginRes.code,
userType: userType.value
}, 'post').then((resData) => {
uni.hideLoading();
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');
if (!resume.data.jobTitleId) {
if (userType.value === 1) {
// 求职者跳转到个人信息补全页面
uni.navigateTo({
url: '/pages/complete-info/complete-info?step=1'
});
} else if (userType.value === 0) {
// 招聘者跳转到企业信息补全页面
uni.navigateTo({
url: '/pages/complete-info/company-info'
});
}
}
});
} else {
$api.msg('登录失败,请重试');
}
}).catch((err) => {
uni.hideLoading();
$api.msg(err.msg || '登录失败,请重试');
});
},
fail: (err) => {
uni.hideLoading();
console.error('微信登录失败:', err);
$api.msg('微信登录失败');
}
});
// #endif
// #ifdef APP-PLUS
// App微信登录逻辑
uni.getProvider({
service: 'oauth',
success: (res) => {
if (~res.provider.indexOf('weixin')) {
uni.login({
provider: 'weixin',
success: (loginRes) => {
console.log('微信登录成功:', loginRes);
// 调用后端接口进行登录
$api.createRequest('/app/appLogin', {
code: loginRes.code,
userType: userType.value
}, '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');
if (!resume.data.jobTitleId) {
if (userType.value === 1) {
// 求职者跳转到个人信息补全页面
uni.navigateTo({
url: '/pages/complete-info/complete-info?step=1'
});
} else if (userType.value === 0) {
// 招聘者跳转到企业信息补全页面
uni.navigateTo({
url: '/pages/complete-info/company-info'
});
}
}
});
}
});
},
fail: (err) => {
console.error('微信登录失败:', err);
$api.msg('微信登录失败');
}
});
}
}
});
// #endif
};
// 测试账号登录(仅开发环境)
const testLogin = () => {
uni.showLoading({ title: '登录中...' });
const params = {
username: 'test',
password: 'test',
};
$api.createRequest('/app/login', params, 'post').then((resData) => {
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');
if (!resume.data.jobTitleId) {
if (userType.value === 1) {
// 求职者跳转到个人信息补全页面
uni.navigateTo({
url: '/pages/complete-info/complete-info?step=1'
});
} else if (userType.value === 0) {
// 招聘者跳转到企业信息补全页面
uni.navigateTo({
url: '/pages/complete-info/company-info'
});
}
}
}).catch(() => {
$api.msg('获取用户信息失败');
});
}).catch((err) => {
uni.hideLoading();
$api.msg(err.msg || '登录失败');
});
};
// 打开用户协议
const openAgreement = (type) => {
const urls = {
user: '/pages/agreement/user',
privacy: '/pages/agreement/privacy'
};
if (urls[type]) {
uni.navigateTo({
url: urls[type]
});
}
};
// 暴露方法供父组件调用
defineExpose({
open,
close
});
</script>
<style lang="stylus" scoped>
.auth-modal
width: 620rpx
background: #FFFFFF
border-radius: 24rpx
overflow: hidden
.modal-content
padding: 60rpx 40rpx 40rpx
position: relative
.close-btn
position: absolute
right: 20rpx
top: 20rpx
width: 60rpx
height: 60rpx
display: flex
align-items: center
justify-content: center
z-index: 10
.auth-header
text-align: center
margin-bottom: 40rpx
.auth-logo
width: 120rpx
height: 120rpx
margin: 0 auto 24rpx
.auth-title
font-size: 36rpx
font-weight: 600
color: #333333
margin-bottom: 12rpx
.auth-subtitle
font-size: 28rpx
color: #666666
.role-select
margin-bottom: 32rpx
.role-title
font-size: 28rpx
font-weight: 500
color: #333333
margin-bottom: 20rpx
text-align: center
.role-options
display: flex
justify-content: space-between
gap: 20rpx
.role-item
flex: 1
background: #F7F8FA
border: 2rpx solid #E5E5E5
border-radius: 16rpx
padding: 32rpx 20rpx
display: flex
flex-direction: column
align-items: center
position: relative
transition: all 0.3s ease
cursor: pointer
&.active
background: #F0F5FF
border-color: #256BFA
box-shadow: 0 4rpx 12rpx rgba(37, 107, 250, 0.15)
.role-icon
margin-bottom: 16rpx
.role-text
font-size: 28rpx
color: #333333
font-weight: 500
.auth-tips
background: #F7F8FA
border-radius: 16rpx
padding: 24rpx
margin-bottom: 40rpx
.tip-item
display: flex
align-items: center
margin-bottom: 16rpx
font-size: 26rpx
color: #666666
&:last-child
margin-bottom: 0
text
margin-left: 12rpx
.auth-actions
margin-bottom: 32rpx
.auth-btn
width: 100%
height: 88rpx
border-radius: 44rpx
display: flex
align-items: center
justify-content: center
font-size: 32rpx
font-weight: 500
border: none
margin-bottom: 20rpx
&:last-child
margin-bottom: 0
&.primary
background: linear-gradient(135deg, #256BFA 0%, #1E5BFF 100%)
color: #FFFFFF
box-shadow: 0 8rpx 20rpx rgba(37, 107, 250, 0.3)
&.secondary
background: #F7F8FA
color: #666666
text
margin-left: 12rpx
.auth-agreement
text-align: center
font-size: 24rpx
color: #999999
line-height: 1.6
.link
color: #256BFA
text-decoration: underline
// 按钮重置样式
button::after
border: none
</style>

102
config/icons.js Normal file
View File

@@ -0,0 +1,102 @@
/**
* 图标配置文件
* 统一管理项目中使用的所有图标名称
* 使用方式import { ICONS } from '@/config/icons'
*/
export const ICONS = {
// 导航类
HOME: 'home',
SEARCH: 'search',
USER: 'user',
SETTING: 'setting',
BACK: 'back',
CLOSE: 'close',
MENU: 'menu',
MORE: 'more',
// 操作类
ADD: 'add',
EDIT: 'edit',
DELETE: 'delete',
SAVE: 'save',
REFRESH: 'refresh',
UPLOAD: 'upload',
DOWNLOAD: 'download',
SHARE: 'share',
FILTER: 'filter',
SORT: 'sort',
// 通讯类
PHONE: 'phone',
MESSAGE: 'message',
EMAIL: 'email',
CHAT: 'chat',
NOTIFICATION: 'notification',
// 位置类
LOCATION: 'location',
MAP: 'map',
NAVIGATE: 'navigate',
// 状态类
SUCCESS: 'success',
ERROR: 'error',
WARNING: 'warning',
INFO: 'info',
LOADING: 'loading',
// 媒体类
IMAGE: 'image',
VIDEO: 'video',
AUDIO: 'audio',
FILE: 'file',
CAMERA: 'camera',
// 交互类
LIKE: 'like',
STAR: 'star',
COLLECT: 'collect',
COMMENT: 'comment',
EYE: 'eye',
HEART: 'heart',
// 箭头类
ARROW_UP: 'arrow-up',
ARROW_DOWN: 'arrow-down',
ARROW_LEFT: 'arrow-left',
ARROW_RIGHT: 'arrow-right',
// 其他
TIME: 'time',
DATE: 'date',
CALENDAR: 'calendar',
LOCK: 'lock',
UNLOCK: 'unlock',
HELP: 'help',
QUESTION: 'question',
}
// 图标尺寸预设
export const ICON_SIZES = {
MINI: 24,
SMALL: 28,
NORMAL: 32,
LARGE: 40,
XLARGE: 48,
}
// 图标颜色预设
export const ICON_COLORS = {
PRIMARY: '#13C57C',
SECONDARY: '#256BFA',
SUCCESS: '#13C57C',
WARNING: '#FF9800',
DANGER: '#F44336',
INFO: '#2196F3',
TEXT: '#333333',
TEXT_SECONDARY: '#666666',
TEXT_PLACEHOLDER: '#999999',
WHITE: '#FFFFFF',
}

View File

@@ -0,0 +1,318 @@
# H5端CSS引入问题解决方案
## ❌ 错误提示
```
iconfont.css:1 Failed to load module script: Expected a JavaScript module script but the server responded with a MIME type of "text/css". Strict MIME type checking is enforced for module scripts per HTML spec.
```
## 🔍 问题原因
`main.js` 中使用了 **错误的方式** 引入CSS文件
```javascript
// ❌ 错误尝试将CSS作为JavaScript模块导入
import './static/iconfont/iconfont.css'
```
这种 `import` 语法在H5端会导致浏览器尝试将CSS文件作为JavaScript模块加载从而产生MIME类型错误。
## ✅ 解决方案
### 方案一:在 App.vue 中使用 @import推荐
`App.vue``<style>` 标签中使用CSS的 `@import` 语法:
```vue
<style>
/* 引入阿里图标库 */
@import url("/static/iconfont/iconfont.css");
</style>
```
**优点:**
- ✅ 所有平台兼容H5、小程序、App
- ✅ 符合CSS规范
- ✅ 全局生效
### 方案二:条件编译(如果必须在 main.js 中引入)
如果确实需要在 `main.js` 中引入,使用条件编译:
```javascript
// #ifndef H5
import './static/iconfont/iconfont.css'
// #endif
```
然后在 `App.vue` 中单独为H5引入
```vue
<style>
/* #ifdef H5 */
@import url("/static/iconfont/iconfont.css");
/* #endif */
</style>
```
## 📋 CSS引入方式对比
### JavaScript import不推荐用于CSS
```javascript
// ❌ 在 main.js 中
import './static/iconfont/iconfont.css'
```
**问题:**
- H5端会报MIME类型错误
- 将CSS当作JavaScript模块处理
### CSS @import推荐
```vue
<!-- App.vue 或其他 .vue 文件中 -->
<style>
@import url("/static/iconfont/iconfont.css");
</style>
```
**优点:**
- 所有平台兼容
- 符合CSS标准
- 不会产生MIME类型错误
### 使用 <style> 标签的 src 属性(可选)
```vue
<style src="/static/iconfont/iconfont.css"></style>
```
## 🔧 修复步骤
### Step 1: 删除 main.js 中的CSS import
打开 `main.js`,找到并删除或注释掉:
```javascript
// import './static/iconfont/iconfont.css' // 删除这行
```
### Step 2: 确认 App.vue 中的引入
确保 `App.vue` 中有正确的CSS引入
```vue
<style>
@import '@/common/animation.css';
@import '@/common/common.css';
/* 引入阿里图标库 */
@import url("/static/iconfont/iconfont.css");
</style>
```
### Step 3: 清除缓存并重新编译
1. 关闭开发服务器
2. 清除浏览器缓存
3. 重新运行 `npm run dev:h5` 或点击HBuilderX的运行按钮
## 🎯 最佳实践
### 全局CSS引入位置
**推荐顺序:**
```vue
<!-- App.vue -->
<style>
/* 1. 重置样式 / 通用样式 */
@import '@/common/reset.css';
@import '@/common/common.css';
/* 2. 第三方库样式 */
@import url("/static/iconfont/iconfont.css");
/* 3. 动画效果 */
@import '@/common/animation.css';
/* 4. 项目全局样式 */
/* 自定义全局样式 */
</style>
```
### 路径写法
**绝对路径(推荐):**
```css
@import url("/static/iconfont/iconfont.css");
```
**相对路径:**
```css
@import url("./static/iconfont/iconfont.css");
@import url("@/static/iconfont/iconfont.css");
```
**注意:** 在不同平台上路径解析可能有差异,推荐使用绝对路径。
## 🚫 常见错误
### 错误1在 main.js 中 import CSS
```javascript
// ❌ 错误
import './styles/global.css'
import '@/static/iconfont/iconfont.css'
```
**解决:** 改用 App.vue 的 `@import`
### 错误2路径不正确
```css
/* ❌ 错误:路径错误 */
@import url("static/iconfont/iconfont.css");
/* ✅ 正确:使用正确的路径 */
@import url("/static/iconfont/iconfont.css");
```
### 错误3缺少分号
```css
/* ❌ 错误:缺少分号 */
@import url("/static/iconfont/iconfont.css")
/* ✅ 正确:添加分号 */
@import url("/static/iconfont/iconfont.css");
```
### 错误4在 scoped 样式中引入
```vue
<!-- 不推荐 scoped 样式中引入全局CSS -->
<style scoped>
@import url("/static/iconfont/iconfont.css");
</style>
<!-- 推荐全局样式不要加 scoped -->
<style>
@import url("/static/iconfont/iconfont.css");
</style>
```
## 📊 平台兼容性
| 引入方式 | H5 | 小程序 | App | 推荐 |
|---------|----|----|-----|-----|
| main.js import | ❌ | ✅ | ✅ | ❌ |
| App.vue @import | ✅ | ✅ | ✅ | ✅ |
| style src | ✅ | ✅ | ✅ | ✅ |
| 条件编译 | ✅ | ✅ | ✅ | ⚠️ |
## 🔍 调试方法
### 1. 检查CSS是否加载
在浏览器开发者工具中:
```
F12 → Network → Filter: CSS → 查找 iconfont.css
```
**成功标志:**
- 状态码200
- Type: stylesheet
- Size: 文件大小正常
### 2. 检查字体文件
```
F12 → Network → Filter: Font → 查找 iconfont.ttf/woff
```
### 3. 检查控制台错误
```
F12 → Console → 查看是否有错误信息
```
### 4. 验证样式生效
```javascript
// 在控制台执行
document.querySelector('.iconfont')
// 应该能找到使用了 iconfont 类的元素
```
## ✅ 验证成功
修复后,应该看到:
1. ✅ H5端控制台无CSS加载错误
2. ✅ 图标正常显示
3. ✅ Network 中 iconfont.css 状态码为 200
4. ✅ 字体文件正常加载
## 📝 注意事项
### uni-app 项目特点
1. **多平台编译**
- H5端使用浏览器标准
- 小程序有自己的规范
- App使用原生渲染
2. **路径处理**
- `@/` 代表项目根目录
- `/static/` 代表静态资源目录
- 不同平台路径解析略有差异
3. **样式隔离**
- `scoped` 样式只在当前组件生效
- 全局样式在 App.vue 中引入
- 不要在 scoped 中引入全局CSS
### Vite 项目特点
如果使用 Vite 构建HBuilderX 3.2+
```javascript
// main.js 中可以使用(但不推荐)
import './static/iconfont/iconfont.css'
```
但为了兼容性,仍然推荐在 App.vue 中使用 `@import`
## 🎉 总结
### 问题
`main.js` 中使用 `import` 引入CSS导致H5端报错。
### 解决
1. ✅ 删除 `main.js` 中的 CSS import
2. ✅ 在 `App.vue``<style>` 中使用 `@import`
3. ✅ 重启开发服务器
### 最佳实践
- 所有全局CSS在 `App.vue` 中通过 `@import` 引入
- 使用绝对路径:`/static/...`
- 不要在 `scoped` 样式中引入全局CSS
- 保持引入顺序:重置 → 第三方 → 动画 → 自定义
## 📚 相关文档
- [uni-app 样式导入](https://uniapp.dcloud.net.cn/tutorial/syntax-css.html#%E6%A0%B7%E5%BC%8F%E5%AF%BC%E5%85%A5)
- [CSS @import](https://developer.mozilla.org/zh-CN/docs/Web/CSS/@import)
- [Vite 静态资源处理](https://cn.vitejs.dev/guide/assets.html)
---
**该问题已解决!** 🎉
现在H5端应该可以正常加载CSS文件了。如果还有问题请检查
1. 文件路径是否正确
2. 是否清除了浏览器缓存
3. 是否重启了开发服务器

View File

@@ -0,0 +1,207 @@
# 企业地址选择功能说明
## 功能概述
在企业信息补全页面company-info.vue点击"企业注册地点"字段的箭头时,会弹出一个五级联动选择器,用户可以选择省市区县街道社区的完整地址。
## 实现的功能
### 1. 五级联动选择
- **省级选择**:选择省/直辖市/自治区
- **市级选择**:根据选择的省,显示对应的市/地区
- **区县选择**:根据选择的市,显示对应的区/县
- **街道选择**:根据选择的区县,显示对应的街道/乡镇
- **社区选择**:根据选择的街道,显示对应的社区/居委会
### 2. 联动逻辑
- 当选择省级时,自动更新并重置市级及以下选项
- 当选择市级时,自动更新并重置区县及以下选项
- 当选择区县时,自动更新并重置街道及以下选项
- 当选择街道时,自动更新社区选项
- 所有联动过程均实时响应,无需额外操作
### 3. 地址格式
- 选择完成后,地址格式为:`省/市/区/街道/社区`
- 示例:`新疆维吾尔自治区/喀什地区/喀什市/学府街道/学府社区居委会`
- 地址各级信息都会保存,包括代码和名称
### 4. 数据存储
- 保存完整的地址字符串(`registeredAddress`
- 保存各级行政区划代码和名称:
- 省:`provinceCode``provinceName`
- 市:`cityCode``cityName`
- 区县:`districtCode``districtName`
- 街道:`streetCode``streetName`
- 社区:`communityCode``communityName`
## 技术实现
### 组件架构
- **组件名称**`area-cascade-picker`
- **组件位置**`components/area-cascade-picker/area-cascade-picker.vue`
- **依赖组件**`uni-popup`uni-app官方弹窗组件
### 核心技术
- **picker-view**uni-app的多列选择器组件
- **五级联动**:通过监听选择变化,动态更新下级选项
- **树形数据结构**:地区数据采用嵌套的树形结构,每级都有`children`属性
### 数据格式
```javascript
{
code: '650000', // 行政区划代码
name: '新疆维吾尔自治区', // 名称
children: [ // 下级行政区
{
code: '653100',
name: '喀什地区',
children: [...]
}
]
}
```
### 数据流程
1. **打开地址选择器**
```javascript
// company-info.vue
const selectLocation = () => {
areaPicker.value?.open({
title: '选择企业注册地点',
maskClick: true,
success: (addressData) => {
// 处理选择结果
formData.registeredAddress = addressData.address
// 保存各级信息
formData.provinceCode = addressData.province?.code
formData.provinceName = addressData.province?.name
// ... 其他字段
}
})
}
```
2. **选择地址过程**
```javascript
// area-cascade-picker.vue
bindChange(e) {
// 检测哪一级发生变化
// 更新对应级别及其下级的选项列表
// 触发change回调可选
}
```
3. **确认选择**
```javascript
// area-cascade-picker.vue
confirm() {
const addressData = {
address: '新疆维吾尔自治区/喀什地区/喀什市/学府街道/学府社区居委会',
province: { code: '650000', name: '新疆维吾尔自治区' },
city: { code: '653100', name: '喀什地区' },
district: { code: '653101', name: '喀什市' },
street: { code: '65310101', name: '学府街道' },
community: { code: '6531010101', name: '学府社区居委会' }
}
// 调用success回调
callback(addressData)
}
```
## 文件结构
```
components/
└── area-cascade-picker/
└── area-cascade-picker.vue # 五级联动地址选择组件
pages/complete-info/
└── company-info.vue # 企业信息补全页面
```
## 数据字段
在 `company-info.vue` 的 `formData` 中的地址相关字段:
- `registeredAddress`: string - 完整地址(格式:省/市/区/街道/社区)
- `registeredAddressName`: string - 地址名称
- `provinceCode`: string - 省级行政区划代码
- `provinceName`: string - 省级名称
- `cityCode`: string - 市级行政区划代码
- `cityName`: string - 市级名称
- `districtCode`: string - 区县行政区划代码
- `districtName`: string - 区县名称
- `streetCode`: string - 街道行政区划代码
- `streetName`: string - 街道名称
- `communityCode`: string - 社区行政区划代码
- `communityName`: string - 社区名称
## 使用方法
### 用户操作流程
1. 在企业信息页面,点击"企业注册地点"右侧的箭头图标
2. 弹出五级联动地址选择器
3. 依次选择:
- 第一列:选择省/直辖市/自治区
- 第二列:选择市/地区(根据第一列自动更新)
- 第三列:选择区/县(根据第二列自动更新)
- 第四列:选择街道/乡镇(根据第三列自动更新)
- 第五列:选择社区/居委会(根据第四列自动更新)
4. 确认选择无误后,点击右上角"确认"按钮
5. 选择器关闭,地址自动填充到表单中
### 界面说明
- **顶部标题栏**
- 左侧"取消"按钮:关闭选择器,不保存
- 中间标题:显示"选择企业注册地点"
- 右侧"确认"按钮:确认选择并保存
- **五列选择器**
- 每列显示当前级别的所有选项
- 可上下滑动选择
- 选中项高亮显示
- 各列之间自动联动
## 注意事项
1. **数据来源**
- 当前使用本地模拟数据,包含主要城市的五级地址
- 生产环境建议接入后端API提供完整的全国地址数据
- 后端API接口`/app/common/area/cascade`(需要实现)
2. **数据格式要求**
- 必须是树形结构,每级通过`children`属性嵌套
- 每个节点必须包含`code`(行政区划代码)和`name`(名称)
- 最深五级:省 → 市 → 区县 → 街道 → 社区
3. **性能考虑**
- 地址数据量大时,建议使用懒加载方式
- 可以先加载省市区,街道和社区按需加载
- 考虑使用缓存机制,避免重复加载
4. **兼容性**
- 支持H5、微信小程序等多端
- 使用uni-app原生组件兼容性好
## 后续优化建议
1. **数据优化**
- 接入后端API提供完整的全国地址数据
- 实现地址数据的懒加载
- 添加地址数据缓存机制
2. **功能增强**
- 支持默认值回显(编辑时显示已选地址)
- 添加搜索功能,快速定位地址
- 支持手动输入详细地址(如门牌号)
- 增加常用地址保存功能
3. **用户体验**
- 优化滑动选择的流畅度
- 添加选择预览功能
- 支持快速选择最近使用的地址
4. **数据扩展**
- 支持国际地址选择
- 添加邮政编码自动填充
- 提供经纬度坐标(结合地理编码服务)

View File

@@ -0,0 +1,117 @@
# 企业我的页面功能说明
## 功能概述
本功能为企业用户提供了专门的"我的"页面和企业信息展示页面,实现了根据用户类型显示不同内容的功能。
## 页面结构
### 1. 企业我的页面 (`pages/mine/company-mine.vue`)
- **功能**: 企业用户的个人中心页面
- **特点**:
- 显示企业头像、名称和信息完整度
- 包含服务专区(实名认证、通知与提醒)
- 提供退出登录功能
- 点击头像区域可跳转到企业信息页面
### 2. 企业信息展示页面 (`pages/mine/company-info.vue`)
- **功能**: 显示详细的企业信息
- **特点**:
- 显示企业头像编辑功能
- 展示完整的企业信息(名称、统一社会代码、注册地点等)
- 支持编辑各项企业信息
- 包含企业联系人和法人信息
### 3. 修改后的我的页面 (`pages/mine/mine.vue`)
- **功能**: 根据用户类型显示不同的内容
- **特点**:
- 企业用户显示企业信息卡片
- 求职者用户显示个人简历信息
- 自动根据 `userInfo.isCompanyUser` 字段判断用户类型
## 用户类型判断
系统通过 `userInfo.isCompanyUser` 字段来判断用户类型:
- `0` = 企业用户
- `1` = 求职者
- `2` = 网格员
- `3` = 政府人员
## 页面跳转逻辑
### 从我的页面跳转
- **企业用户**: 点击头像区域 → 跳转到企业信息页面 (`/pages/mine/company-info`)
- **求职者用户**: 点击头像区域 → 跳转到简历页面 (`/packageA/pages/myResume/myResume`)
### 企业信息页面功能
- 点击头像 → 编辑头像(调用相册选择图片)
- 点击各项信息 → 跳转到对应的编辑页面(需要后续开发)
## 路由配置
新增的路由配置:
```json
{
"path": "pages/mine/company-mine",
"style": {
"navigationBarTitleText": "我的",
"navigationStyle": "custom"
}
},
{
"path": "pages/mine/company-info",
"style": {
"navigationBarTitleText": "企业信息",
"navigationStyle": "custom"
}
}
```
## 测试页面
创建了测试页面 `pages/test/company-mine-test.vue` 用于测试功能:
- 用户类型切换测试
- 页面跳转测试
- 用户信息显示
## 样式特点
### 企业信息卡片
- 白色背景,圆角设计
- 阴影效果现代化UI
- 头像圆形显示
- 信息完整度显示
### 企业信息页面
- 清晰的信息层级
- 可点击的编辑区域
- 统一的视觉风格
## 数据流
1. 用户登录时设置 `userInfo.isCompanyUser` 字段
2. 我的页面根据此字段判断显示内容
3. 企业用户点击头像跳转到企业信息页面
4. 企业信息页面展示详细的企业数据
## 后续开发建议
1. **编辑功能**: 为每个信息项创建对应的编辑页面
2. **数据接口**: 连接真实的企业信息API
3. **头像上传**: 完善头像上传功能
4. **表单验证**: 添加企业信息编辑的表单验证
5. **权限控制**: 根据用户权限控制可编辑的字段
## 使用方法
1. 在测试页面切换用户类型为企业用户
2. 访问我的页面,查看企业信息卡片
3. 点击头像区域跳转到企业信息页面
4. 在企业信息页面查看详细的企业信息
## 注意事项
- 确保用户类型字段正确设置
- 企业信息数据需要从后端API获取
- 头像上传功能需要配置服务器接口
- 编辑页面需要根据实际需求进行开发

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,284 @@
# 微信小程序组件依赖问题解决方案
## 问题描述
```
components/IconfontIcon/IconfontIcon.js 已被代码依赖分析忽略,无法被其他模块引用。
你可根据控制台中的【代码依赖分析】告警信息修改代码,或关闭【过滤无依赖文件】功能。
```
## 问题原因
1. **组件未被正确引用** - 组件文件存在但没有被任何页面或组件引用
2. **缺少 easycom 配置** - uni-app 项目需要在 `pages.json` 中配置组件自动引入
3. **文件路径问题** - 组件路径不正确或文件名不匹配
## ✅ 解决方案
### 方案一:配置 easycom 自动引入(推荐)✨
`pages.json` 中添加 `easycom` 配置:
```json
{
"easycom": {
"autoscan": true,
"custom": {
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue",
"^IconfontIcon$": "@/components/IconfontIcon/IconfontIcon.vue",
"^WxAuthLogin$": "@/components/WxAuthLogin/WxAuthLogin.vue"
}
}
}
```
**配置说明:**
- `autoscan: true` - 自动扫描 `components` 目录
- `custom` - 自定义组件路径映射
- `^IconfontIcon$` - 组件名称(大小写敏感)
- 配置后无需 import直接在模板中使用
**使用方式:**
```vue
<template>
<!-- 无需 import直接使用 -->
<IconfontIcon name="home" :size="48" />
<WxAuthLogin ref="loginRef" />
</template>
```
### 方案二:手动引入组件
如果不想使用 easycom可以在需要的页面手动引入
```vue
<script setup>
import IconfontIcon from '@/components/IconfontIcon/IconfontIcon.vue'
</script>
<template>
<IconfontIcon name="home" />
</template>
```
### 方案三:关闭"过滤无依赖文件"功能
如果组件确实暂时不需要使用,可以在微信开发者工具中关闭此功能:
1. 打开微信开发者工具
2. 点击右上角"详情"
3. 找到"本地设置"标签
4. 取消勾选"过滤无依赖文件"
**注意:** 不推荐此方法,因为会增加打包体积。
## 🔧 完整操作步骤
### Step 1: 确认文件结构
确保组件文件存在且路径正确:
```
components/
├── IconfontIcon/
│ └── IconfontIcon.vue ✅ 文件存在
└── WxAuthLogin/
└── WxAuthLogin.vue ✅ 文件存在
```
### Step 2: 修改 pages.json
已为你自动添加了 easycom 配置,位置在 `globalStyle` 后面。
### Step 3: 重启微信开发者工具
1. 关闭微信开发者工具
2. 重新打开项目
3. 等待编译完成
### Step 4: 清除缓存
如果问题仍然存在:
1. 点击顶部菜单"工具" → "清除缓存"
2. 选择"清除文件缓存"
3. 重新编译项目
### Step 5: 验证组件可用
在任意页面中测试:
```vue
<template>
<view>
<IconfontIcon name="home" :size="48" color="#13C57C" />
</view>
</template>
<script setup>
// 使用 easycom 后无需 import
</script>
```
## 📋 配置详解
### easycom 规则说明
```json
{
"easycom": {
// 是否自动扫描 components 目录
"autoscan": true,
// 自定义规则
"custom": {
// 格式: "匹配规则": "组件路径"
"^IconfontIcon$": "@/components/IconfontIcon/IconfontIcon.vue"
}
}
}
```
**匹配规则说明:**
- `^` - 字符串开始
- `$` - 字符串结束
- `^IconfontIcon$` - 精确匹配 `IconfontIcon`
- `^uni-(.*)` - 匹配所有 `uni-` 开头的组件
### 组件命名规范
**推荐命名:**
-`IconfontIcon` - 大驼峰命名
-`WxAuthLogin` - 大驼峰命名
-`MyCustomComponent` - 大驼峰命名
**不推荐:**
-`iconfontIcon` - 小驼峰
-`iconfont-icon` - 短横线
-`Iconfont_Icon` - 下划线
## 🎯 常见问题
### Q1: 配置后仍然报错?
**解决方法:**
1. 检查 `pages.json` 语法是否正确JSON格式
2. 确认组件路径是否正确
3. 重启微信开发者工具
4. 清除缓存后重新编译
### Q2: 组件找不到?
**检查清单:**
- [ ] 文件路径是否正确:`@/components/IconfontIcon/IconfontIcon.vue`
- [ ] 文件名大小写是否一致
- [ ] 组件名称是否与配置匹配
- [ ] 是否重启了开发者工具
### Q3: 在页面中使用组件报错?
**常见原因:**
```vue
<!-- 错误使用了短横线命名 -->
<iconfont-icon name="home" />
<!-- 正确使用大驼峰命名 -->
<IconfontIcon name="home" />
```
### Q4: 多个组件如何配置?
```json
{
"easycom": {
"autoscan": true,
"custom": {
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue",
"^IconfontIcon$": "@/components/IconfontIcon/IconfontIcon.vue",
"^WxAuthLogin$": "@/components/WxAuthLogin/WxAuthLogin.vue",
"^CustomButton$": "@/components/CustomButton/CustomButton.vue"
}
}
}
```
### Q5: autoscan 和 custom 的区别?
**autoscan自动扫描**
```
components/
├── CustomButton/
│ └── CustomButton.vue → 自动识别为 <CustomButton>
├── MyCard/
│ └── MyCard.vue → 自动识别为 <MyCard>
```
**custom自定义规则**
```json
{
"custom": {
"^Button$": "@/components/CustomButton/CustomButton.vue"
}
}
```
使用 `<Button>` 会映射到 `CustomButton.vue`
## 🔍 调试方法
### 1. 查看编译日志
在微信开发者工具控制台查看编译信息:
```
点击顶部"编译" → 查看控制台输出
```
### 2. 检查组件是否被打包
1. 打开"详情" → "本地设置"
2. 查看"代码依赖分析"信息
3. 确认组件是否在依赖树中
### 3. 手动引入测试
```vue
<script setup>
// 临时测试:手动引入
import IconfontIcon from '@/components/IconfontIcon/IconfontIcon.vue'
console.log('组件加载成功:', IconfontIcon)
</script>
```
## ✅ 验证成功标志
配置成功后,应该看到:
1. ✅ 微信开发者工具控制台无警告
2. ✅ 组件可以正常显示
3. ✅ 无需 import 即可使用
4. ✅ 组件出现在代码依赖分析中
## 📚 相关文档
- [uni-app easycom 文档](https://uniapp.dcloud.net.cn/collocation/pages.html#easycom)
- [微信小程序代码依赖分析](https://developers.weixin.qq.com/miniprogram/dev/devtools/codecompile.html)
- [组件化开发文档](https://uniapp.dcloud.net.cn/tutorial/vue3-components.html)
## 🎉 总结
该问题已通过以下方式解决:
1. ✅ 在 `pages.json` 中添加了 `easycom` 配置
2. ✅ 配置了 `IconfontIcon``WxAuthLogin` 组件的自动引入
3. ✅ 组件现在可以在任何页面中直接使用,无需 import
**下一步:**
- 重启微信开发者工具
- 清除缓存
- 开始使用组件
如果问题仍然存在,请检查:
1. 文件路径是否正确
2. 文件名大小写是否一致
3. pages.json 语法是否正确
4. 是否已重启开发者工具

View File

@@ -0,0 +1,321 @@
# 微信授权登录功能说明
## 功能概述
本次开发实现了微信授权登录功能,当用户点击首页的特定功能时,会检查用户是否已登录,如果未登录则弹出授权弹窗,而不是直接跳转到登录页面。
## 主要功能点
### 1. 需要登录验证的功能入口
以下功能在点击时会进行登录验证:
- **附近工作** - 点击后跳转到附近工作列表页
- **九宫格服务功能** - 包含9个服务项
- 服务指导
- 事业单位招录
- 简历制作
- 劳动政策指引
- 技能培训信息
- 技能评价指引
- 题库和考试
- 素质测评
- AI智能面试
- **职位列表** - 点击任意职位卡片查看详情
### 2. 登录弹窗功能
#### 弹窗特性
- 使用 `uni-popup` 组件实现弹窗效果
- 弹窗居中显示,支持关闭按钮
- 不可点击遮罩关闭,确保用户必须做出选择
#### 弹窗内容
- **Logo和标题** - 显示应用logo和欢迎信息
- **授权说明** - 列出三个要点:
- 保护您的个人信息安全
- 为您推荐更合适的岗位
- 享受完整的就业服务
- **授权按钮**
- 微信小程序:使用 `open-type="getPhoneNumber"` 获取手机号
- H5/App使用微信登录接口
- 测试登录按钮仅H5/App环境显示
- **用户协议** - 显示用户协议和隐私政策链接
### 3. 登录流程
#### 微信小程序登录流程
1. 用户点击"微信授权登录"按钮
2. 触发微信小程序的手机号授权
3. 获取到 `code``encryptedData``iv`
4. 调用后端 `/app/wxLogin` 接口
5. 后端返回 `token`
6. 存储 `token` 并获取用户信息
7. 如果用户信息不完整,跳转到完善信息页面
8. 关闭弹窗,继续用户之前的操作
#### H5/App登录流程
1. 用户点击"微信授权登录"按钮
2. 调用 `uni.login` 获取微信授权 `code`
3. 调用后端 `/app/wxLogin` 接口
4. 后续流程同上
#### 测试登录流程(仅开发环境)
1. 用户点击"测试账号登录"按钮
2. 使用测试账号密码登录
3. 后续流程同上
### 4. 登录状态管理
#### 状态恢复
- 应用启动时自动从本地缓存恢复用户信息
- 验证 `token` 是否有效
- 如果 `token` 失效,清除缓存但不跳转登录页
#### 状态检查
- 使用 `checkLogin()` 函数统一检查登录状态
- 检查 `token` 是否存在
- 检查 `hasLogin` 状态
- 如果未登录,自动打开授权弹窗
## 文件结构
```
ks-app-employment-service/
├── components/
│ └── WxAuthLogin/
│ └── WxAuthLogin.vue # 微信授权登录弹窗组件
├── pages/
│ └── index/
│ └── components/
│ └── index-one.vue # 首页组件(已修改)
├── stores/
│ └── useUserStore.js # 用户状态管理(已修改)
├── App.vue # 应用入口(已修改)
└── docs/
└── 微信授权登录功能说明.md # 本文档
```
## 核心代码说明
### 1. WxAuthLogin.vue 组件
这是一个可复用的微信授权登录弹窗组件,提供以下接口:
**Props**
-
**Events**
- `success` - 登录成功时触发
- `cancel` - 取消登录时触发
**Methods**
- `open()` - 打开弹窗
- `close()` - 关闭弹窗
**使用示例**
```vue
<template>
<WxAuthLogin ref="wxAuthLoginRef" @success="handleLoginSuccess" />
</template>
<script setup>
import WxAuthLogin from '@/components/WxAuthLogin/WxAuthLogin.vue';
const wxAuthLoginRef = ref(null);
const handleLoginSuccess = () => {
console.log('登录成功');
// 执行登录后的操作
};
// 打开登录弹窗
const showLogin = () => {
wxAuthLoginRef.value?.open();
};
</script>
```
### 2. 登录检查函数
`index-one.vue` 中添加了统一的登录检查函数:
```javascript
// 登录检查函数
const checkLogin = () => {
const tokenValue = uni.getStorageSync('token') || '';
if (!tokenValue || !hasLogin.value) {
// 未登录,打开授权弹窗
wxAuthLoginRef.value?.open();
return false;
}
return true;
};
```
### 3. 点击事件处理
所有需要登录的功能都使用统一的检查逻辑:
```javascript
// 处理附近工作点击
const handleNearbyClick = () => {
if (checkLogin()) {
navTo('/pages/nearby/nearby');
}
};
// 处理服务功能点击
const handleServiceClick = (serviceType) => {
if (checkLogin()) {
navToService(serviceType);
}
};
// 处理职位详情点击
function nextDetail(job) {
if (checkLogin()) {
// 记录岗位类型,用作数据分析
if (job.jobCategory) {
const recordData = recommedIndexDb.JobParameter(job);
recommedIndexDb.addRecord(recordData);
}
navTo(`/packageA/pages/post/post?jobId=${btoa(job.jobId)}`);
}
}
```
### 4. 状态管理优化
`useUserStore.js` 中优化了 `logOut` 函数:
```javascript
const logOut = (redirect = true) => {
hasLogin.value = false;
token.value = ''
resume.value = {}
userInfo.value = {}
role.value = {}
uni.removeStorageSync('userInfo')
uni.removeStorageSync('token')
// 只有在明确需要跳转时才跳转到补全信息页
if (redirect) {
uni.redirectTo({
url: '/pages/complete-info/complete-info',
});
}
}
```
## 后端接口要求
### 1. 微信登录接口
**接口地址**: `/app/appLogin`
**请求方法**: `POST`
**请求参数**:
#### 微信小程序
```json
{
"code": "string", // 微信登录凭证
"encryptedData": "string", // 加密数据
"iv": "string" // 加密算法初始向量
}
```
#### H5/App
```json
{
"code": "string" // 微信登录凭证
}
```
**返回数据**:
```json
{
"token": "string", // 用户token
"msg": "string", // 返回消息
"code": 200 // 状态码
}
```
### 2. 获取用户信息接口
**接口地址**: `/app/user/resume`
**请求方法**: `GET`
**请求头**: `Authorization: Bearer {token}`
**返回数据**:
```json
{
"code": 200,
"data": {
"name": "string",
"phone": "string",
"jobTitle": ["string"],
"jobTitleId": "string",
// ... 其他用户信息
}
}
```
## 注意事项
1. **小程序配置**
- 需要在微信小程序后台配置服务器域名
- 需要申请手机号授权权限
2. **H5配置**
- 需要配置微信公众号的授权回调域名
- 需要引入微信JSSDK
3. **安全性**
- Token存储在本地缓存中注意加密
- 敏感操作前需要重新验证token有效性
4. **用户体验**
- 登录弹窗不可通过点击遮罩关闭,确保用户必须做出选择
- 提供测试登录按钮方便开发调试
- 登录成功后自动刷新数据
5. **兼容性**
- 使用条件编译确保在不同平台上正常运行
- 小程序、H5、App使用不同的登录逻辑
## 测试建议
### 功能测试
1. 未登录状态点击"附近工作",应弹出登录弹窗
2. 未登录状态点击九宫格任意服务,应弹出登录弹窗
3. 未登录状态点击职位列表,应弹出登录弹窗
4. 登录成功后,能够正常访问所有功能
5. 关闭登录弹窗后,不会自动跳转到登录页
### 登录流程测试
1. 微信小程序:测试手机号授权流程
2. H5测试微信网页授权流程
3. 测试账号登录功能(开发环境)
4. 测试登录失败的错误提示
5. 测试用户取消授权的处理
### 状态管理测试
1. 测试应用重启后登录状态的恢复
2. 测试token失效后的处理
3. 测试退出登录功能
4. 测试多次登录的状态切换
## 更新日志
### v1.0.0 (2024-10-20)
- 创建微信授权登录弹窗组件
- 添加登录状态检查逻辑
- 优化用户状态管理
- 更新首页各功能的登录验证
- 完善登录流程和错误处理
## 开发者
- 开发时间: 2024-10-20
- 涉及模块: 登录模块、首页模块、用户状态管理

View File

@@ -0,0 +1,138 @@
# 编译器内存溢出解决方案
## 问题描述
编译时出现 `FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memory` 错误,表示 Node.js 内存不足。
## 解决方案
### 方案一:增加 Node.js 内存限制(推荐)
#### 在 HBuilderX 中设置
1. **修改 HBuilderX 配置文件**
- 关闭 HBuilderX
- 找到 HBuilderX 安装目录
- 打开 `HBuilderX\plugins\node\node_modules\@dcloudio\vite-plugin-uni\dist` 目录
- 或者在项目根目录创建 `vue.config.js` 文件
2. **创建或修改项目根目录下的 `vue.config.js`**
```javascript
module.exports = {
transpileDependencies: [],
// 增加 Node.js 内存限制
configureWebpack: {
devServer: {
disableHostCheck: true
}
}
}
```
3. **修改 HBuilderX 启动配置**
- 找到 HBuilderX 安装目录
- 编辑 `HBuilderX.exe` 的启动参数
- 创建一个批处理文件 `start-hbuilderx.bat`
```bat
@echo off
set NODE_OPTIONS=--max-old-space-size=8192
start "" "HBuilderX安装路径\HBuilderX.exe"
```
- 将内存设置为 8GB8192MB可根据实际情况调整为 4096、8192 或更大
#### 在命令行中运行(如果使用 CLI
如果您使用命令行方式编译,可以设置环境变量:
**Windows PowerShell**
```powershell
$env:NODE_OPTIONS="--max-old-space-size=8192"
```
**Windows CMD**
```cmd
set NODE_OPTIONS=--max-old-space-size=8192
```
**永久设置Windows 系统环境变量):**
1. 右键"此电脑" → "属性" → "高级系统设置" → "环境变量"
2. 在"用户变量"或"系统变量"中新建变量:
- 变量名:`NODE_OPTIONS`
- 变量值:`--max-old-space-size=8192`
3. 重启 HBuilderX
### 方案二:清理缓存
1. **清理 HBuilderX 缓存**
- 在 HBuilderX 中:运行 → 清理项目缓存
- 或者手动删除 `unpackage` 目录
2. **删除 node_modules 并重新安装**
```powershell
Remove-Item -Recurse -Force node_modules
# 如果有 package.json重新安装依赖
npm install
```
### 方案三:优化项目
1. **检查大文件**
- 检查 `static` 目录下是否有过大的图片或资源文件
- 当前项目中有 85 个 icon 图片,建议:
- 压缩图片文件
- 使用雪碧图或字体图标代替多个小图标
- 将不常用的资源移到云存储
2. **检查第三方库**
- 检查 `lib` 目录中的第三方库是否必需
- 当前已引入的库:
- dompurify@3.2.4es.js
- markdown-it.min.js
- highlight-uni.min.js
- lunar-javascript@1.7.2.js
- string-similarity.min.js
- 考虑按需引入或延迟加载
3. **优化编译配置**
在 `manifest.json` 中的 `h5.optimization` 已启用 `treeShaking`,这很好。
4. **分包加载**
- 已使用 `packageA` 分包,继续保持
- 考虑将更多页面移到分包中
### 方案四:升级 HBuilderX
确保使用最新版本的 HBuilderX新版本通常有更好的内存管理。
## 推荐操作步骤
1. **立即执行:** 设置 `NODE_OPTIONS` 环境变量为 `--max-old-space-size=8192`
2. **清理缓存:** 在 HBuilderX 中清理项目缓存
3. **重启 HBuilderX** 使用新的环境变量启动
4. **长期优化:** 压缩静态资源,优化第三方库引入
## 验证
设置完成后,重新编译项目,查看是否还会出现内存溢出错误。
## 参考资料
- [uni-app 官方文档 - 内存溢出问题](https://uniapp.dcloud.net.cn/tutorial/run/OOM.html)
- Node.js 内存限制说明:
- 默认限制:约 1.4GB32位或 1.7GB64位
- 建议设置4096MB4GB或 8192MB8GB
- 最大可设置:取决于系统可用内存
## 常见问题
**Q: 设置后仍然内存溢出?**
A: 尝试增大内存限制值,如 `--max-old-space-size=16384`16GB
**Q: 如何检查当前 Node.js 内存限制?**
A: 在命令行运行:`node -e "console.log(require('v8').getHeapStatistics().heap_size_limit/(1024*1024))"`
**Q: 编译特别慢?**
A: 内存充足但编译慢,可能是 CPU 性能问题,考虑:
- 关闭不必要的后台程序
- 使用 SSD 硬盘
- 升级硬件配置

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

@@ -0,0 +1,429 @@
# 阿里图标库iconfont引入指南
## 📦 方式一:使用字体文件(推荐)
### 第一步:下载图标资源
1. 访问 [阿里图标库](https://www.iconfont.cn/)
2. 注册/登录账号
3. 搜索需要的图标,点击"添加入库"
4. 点击右上角购物车图标
5. 点击"添加至项目"(如果没有项目,先创建一个)
6. 进入"我的项目"
7. 点击"下载至本地"按钮
### 第二步:解压并复制文件
下载的压缩包中包含以下文件:
```
iconfont.css
iconfont.ttf
iconfont.woff
iconfont.woff2
iconfont.json
demo_index.html
demo.css
```
**需要的文件:**
- `iconfont.css` - 样式文件
- `iconfont.ttf` - 字体文件
- `iconfont.woff` - 字体文件
- `iconfont.woff2` - 字体文件
### 第三步:创建项目目录
在项目中创建 `static/iconfont/` 目录(如果不存在):
```
ks-app-employment-service/
├── static/
│ ├── iconfont/ ← 新建此目录
│ │ ├── iconfont.css
│ │ ├── iconfont.ttf
│ │ ├── iconfont.woff
│ │ └── iconfont.woff2
│ └── ...
```
### 第四步:修改 CSS 文件
打开 `static/iconfont/iconfont.css`,修改字体文件路径:
**原始路径:**
```css
@font-face {
font-family: "iconfont";
src: url('iconfont.woff2?t=1234567890') format('woff2'),
url('iconfont.woff?t=1234567890') format('woff'),
url('iconfont.ttf?t=1234567890') format('truetype');
}
```
**修改为(相对路径):**
```css
@font-face {
font-family: "iconfont";
src: url('./iconfont.woff2?t=1234567890') format('woff2'),
url('./iconfont.woff?t=1234567890') format('woff'),
url('./iconfont.ttf?t=1234567890') format('truetype');
}
```
**或修改为(绝对路径,推荐):**
```css
@font-face {
font-family: "iconfont";
src: url('/static/iconfont/iconfont.woff2?t=1234567890') format('woff2'),
url('/static/iconfont/iconfont.woff?t=1234567890') format('woff'),
url('/static/iconfont/iconfont.ttf?t=1234567890') format('truetype');
}
```
### 第五步:在项目中引入
#### 方法 A全局引入App.vue
`App.vue` 中引入:
```vue
<style>
/* 引入阿里图标库 */
@import url("/static/iconfont/iconfont.css");
/* 其他全局样式 */
@import '@/common/animation.css';
@import '@/common/common.css';
</style>
```
#### 方法 B在 main.js 中引入
```javascript
// main.js
import './static/iconfont/iconfont.css'
```
### 第六步:使用图标
#### 使用方式 1Unicode 方式
```vue
<template>
<view class="icon">&#xe600;</view>
</template>
<style>
.icon {
font-family: "iconfont" !important;
font-size: 32rpx;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
</style>
```
#### 使用方式 2Font Class 方式(推荐)
```vue
<template>
<view class="iconfont icon-home"></view>
<view class="iconfont icon-user"></view>
<view class="iconfont icon-search"></view>
</template>
<style scoped>
.iconfont {
font-size: 32rpx;
color: #333;
}
</style>
```
#### 使用方式 3封装为组件
创建 `components/IconfontIcon/IconfontIcon.vue`
```vue
<template>
<text class="iconfont" :class="iconClass" :style="iconStyle"></text>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
name: {
type: String,
required: true
},
size: {
type: [String, Number],
default: 32
},
color: {
type: String,
default: '#333'
}
})
const iconClass = computed(() => `icon-${props.name}`)
const iconStyle = computed(() => ({
fontSize: `${props.size}rpx`,
color: props.color
}))
</script>
<style scoped>
.iconfont {
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
</style>
```
**使用组件:**
```vue
<template>
<IconfontIcon name="home" :size="48" color="#13C57C" />
<IconfontIcon name="user" :size="36" color="#256BFA" />
</template>
<script setup>
import IconfontIcon from '@/components/IconfontIcon/IconfontIcon.vue'
</script>
```
---
## 📦 方式二:使用在线链接(不推荐小程序)
### 第一步:获取在线链接
1. 在阿里图标库"我的项目"中
2. 点击"Font class"
3. 点击"查看在线链接"
4. 复制 CSS 链接
### 第二步:引入在线 CSS
`App.vue` 中:
```vue
<style>
/* 注意:小程序不支持在线字体 */
@import url("//at.alicdn.com/t/c/font_xxxxx.css");
</style>
```
**⚠️ 注意:** 微信小程序不支持外部字体文件,必须使用方式一!
---
## 📦 方式三:使用 Symbol 方式SVG
### 第一步:获取 Symbol 代码
1. 在"我的项目"中
2. 点击"Symbol"
3. 点击"生成代码"
4. 复制生成的 JS 链接
### 第二步:下载 JS 文件
将 JS 文件下载到 `static/iconfont/iconfont.js`
### 第三步:引入并使用
`App.vue` 或需要的页面中:
```vue
<template>
<svg class="icon" aria-hidden="true">
<use xlink:href="#icon-home"></use>
</svg>
</template>
<script>
// 引入 Symbol 脚本
// 注意:需要在 main.js 中引入 iconfont.js
</script>
<style>
.icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
</style>
```
**⚠️ 注意:** 小程序对 SVG 支持有限,推荐使用方式一!
---
## 🎯 最佳实践建议
### 1. 使用 Font Class 方式(方式一)
**优点:**
- ✅ 兼容性好,支持所有平台
- ✅ 可以自定义颜色和大小
- ✅ 语义化强,易于维护
- ✅ 体积小,加载快
**缺点:**
- ❌ 只支持单色图标
### 2. 创建图标组件库
```
components/
├── IconfontIcon/
│ └── IconfontIcon.vue # 通用图标组件
```
### 3. 统一管理图标名称
创建 `config/icons.js`
```javascript
// 图标配置
export const ICONS = {
HOME: 'home',
USER: 'user',
SEARCH: 'search',
LOCATION: 'location',
PHONE: 'phone',
// ... 更多图标
}
```
使用:
```vue
<script setup>
import { ICONS } from '@/config/icons'
</script>
<template>
<IconfontIcon :name="ICONS.HOME" />
</template>
```
---
## 🔧 常见问题
### Q1: 小程序中图标不显示?
**解决方案:**
- 确保使用本地字体文件,不要使用在线链接
- 检查 CSS 中的字体路径是否正确
- 确保字体文件已正确复制到 `static/iconfont/` 目录
### Q2: 图标显示为方框?
**解决方案:**
- 检查字体文件是否完整
- 检查 `@font-face``font-family` 名称是否一致
- 清除缓存重新编译
### Q3: 如何更新图标库?
1. 在阿里图标库添加新图标到项目
2. 重新下载至本地
3. 替换 `static/iconfont/` 下的所有文件
4. 清除缓存,重新编译
### Q4: H5 和小程序路径不一致?
**解决方案:**
使用条件编译:
```css
@font-face {
font-family: "iconfont";
/* #ifdef H5 */
src: url('/static/iconfont/iconfont.woff2') format('woff2');
/* #endif */
/* #ifdef MP-WEIXIN */
src: url('./iconfont.ttf') format('truetype');
/* #endif */
}
```
---
## 📝 示例代码
### 完整示例:登录按钮
```vue
<template>
<button class="login-btn">
<text class="iconfont icon-phone"></text>
<text>手机号登录</text>
</button>
</template>
<style scoped>
.login-btn {
display: flex;
align-items: center;
justify-content: center;
padding: 20rpx 40rpx;
background: #13C57C;
border-radius: 12rpx;
color: #fff;
}
.iconfont {
font-size: 32rpx;
margin-right: 12rpx;
}
</style>
```
---
## 🎨 推荐使用的图标
### 常用图标
- `icon-home` - 首页
- `icon-user` - 用户
- `icon-search` - 搜索
- `icon-location` - 位置
- `icon-phone` - 电话
- `icon-message` - 消息
- `icon-setting` - 设置
- `icon-star` - 收藏
- `icon-share` - 分享
- `icon-close` - 关闭
---
## 📚 相关资源
- [阿里图标库官网](https://www.iconfont.cn/)
- [uni-app 字体图标文档](https://uniapp.dcloud.net.cn/tutorial/syntax-css.html#%E5%AD%97%E4%BD%93%E5%9B%BE%E6%A0%87)
- [CSS @font-face](https://developer.mozilla.org/zh-CN/docs/Web/CSS/@font-face)
---
## ✅ 检查清单
- [ ] 已下载图标文件到 `static/iconfont/` 目录
- [ ] 已修改 CSS 中的字体文件路径
- [ ] 已在 App.vue 中引入 iconfont.css
- [ ] 已测试图标显示正常
- [ ] 已封装图标组件(可选)
- [ ] 已统一管理图标名称(可选)

View File

@@ -0,0 +1,407 @@
# 阿里图标库快速开始 🚀
## 一、5分钟快速上手
### Step 1: 下载图标文件2分钟
1. 访问 https://www.iconfont.cn/
2. 登录后搜索图标,点击"添加入库"
3. 购物车 → 添加至项目(没有项目先创建)
4. 我的项目 → 下载至本地
### Step 2: 放置文件1分钟
解压下载的文件将以下4个文件复制到 `static/iconfont/` 目录:
```
✅ iconfont.css
✅ iconfont.ttf
✅ iconfont.woff
✅ iconfont.woff2
```
### Step 3: 修改CSS路径1分钟
打开 `static/iconfont/iconfont.css`,将字体路径修改为相对路径:
```css
@font-face {
font-family: "iconfont";
src: url('./iconfont.woff2?t=xxx') format('woff2'),
url('./iconfont.woff?t=xxx') format('woff'),
url('./iconfont.ttf?t=xxx') format('truetype');
}
```
### Step 4: 全局引入1分钟
`App.vue``<style>` 标签中添加:
```vue
<style>
/* 引入阿里图标库 */
@import url("/static/iconfont/iconfont.css");
</style>
```
### Step 5: 开始使用 ✨
```vue
<template>
<!-- 直接使用 -->
<text class="iconfont icon-home"></text>
<!-- 或使用组件 -->
<IconfontIcon name="home" :size="48" color="#13C57C" />
</template>
```
---
## 二、推荐使用方式
### 方式 A使用封装的组件最推荐👍
```vue
<template>
<IconfontIcon name="phone" :size="40" color="#13C57C" />
</template>
<script setup>
import IconfontIcon from '@/components/IconfontIcon/IconfontIcon.vue'
</script>
```
**优点:**
- ✅ 统一管理,易于维护
- ✅ 支持动态修改大小和颜色
- ✅ 语义清晰
- ✅ 支持点击事件
### 方式 B使用配置常量推荐
```vue
<template>
<IconfontIcon
:name="ICONS.HOME"
:size="ICON_SIZES.LARGE"
:color="ICON_COLORS.PRIMARY"
/>
</template>
<script setup>
import IconfontIcon from '@/components/IconfontIcon/IconfontIcon.vue'
import { ICONS, ICON_SIZES, ICON_COLORS } from '@/config/icons'
</script>
```
**优点:**
- ✅ 统一图标名称
- ✅ 避免拼写错误
- ✅ IDE 自动补全
- ✅ 便于重构
### 方式 C直接使用类名
```vue
<template>
<text class="iconfont icon-home" style="font-size: 32rpx; color: #333;"></text>
</template>
```
---
## 三、常用场景示例
### 场景1导航栏图标
```vue
<template>
<view class="navbar">
<IconfontIcon name="arrow-left" :size="40" @click="goBack" />
<text class="title">页面标题</text>
<IconfontIcon name="share" :size="36" @click="share" />
</view>
</template>
<script setup>
const goBack = () => {
uni.navigateBack()
}
const share = () => {
// 分享逻辑
}
</script>
```
### 场景2按钮图标
```vue
<template>
<button class="primary-btn">
<IconfontIcon name="phone" :size="32" color="#FFFFFF" />
<text>手机号登录</text>
</button>
</template>
<style>
.primary-btn {
display: flex;
align-items: center;
gap: 12rpx;
}
</style>
```
### 场景3列表项图标
```vue
<template>
<view class="list-item">
<IconfontIcon name="location" :size="36" color="#13C57C" />
<text class="text">工作地点</text>
<IconfontIcon name="arrow-right" :size="28" color="#999" />
</view>
</template>
```
### 场景4状态图标
```vue
<template>
<view class="status-box">
<IconfontIcon
:name="status.icon"
:size="64"
:color="status.color"
/>
<text>{{ status.text }}</text>
</view>
</template>
<script setup>
import { computed } from 'vue'
const orderStatus = ref('success')
const status = computed(() => {
const map = {
success: { icon: 'success', color: '#13C57C', text: '提交成功' },
error: { icon: 'error', color: '#F44336', text: '提交失败' },
loading: { icon: 'loading', color: '#256BFA', text: '处理中...' }
}
return map[orderStatus.value]
})
</script>
```
---
## 四、组件API说明
### IconfontIcon 组件
**Props:**
| 参数 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| name | String | - | 图标名称(必填),如:'home' 或 'icon-home' |
| size | String/Number | 32 | 图标大小单位rpx |
| color | String | - | 图标颜色支持十六进制、rgb等 |
| bold | Boolean | false | 是否加粗 |
**Events:**
| 事件名 | 说明 | 回调参数 |
|--------|------|----------|
| click | 点击图标时触发 | event |
**使用示例:**
```vue
<IconfontIcon
name="home"
:size="48"
color="#13C57C"
:bold="true"
@click="handleClick"
/>
```
---
## 五、配置说明
### 图标名称配置config/icons.js
```javascript
export const ICONS = {
HOME: 'home',
USER: 'user',
SEARCH: 'search',
// ... 更多图标
}
```
### 尺寸预设
```javascript
export const ICON_SIZES = {
MINI: 24, // 24rpx
SMALL: 28, // 28rpx
NORMAL: 32, // 32rpx默认
LARGE: 40, // 40rpx
XLARGE: 48, // 48rpx
}
```
### 颜色预设
```javascript
export const ICON_COLORS = {
PRIMARY: '#13C57C', // 主色调
SECONDARY: '#256BFA', // 次要色
SUCCESS: '#13C57C', // 成功
WARNING: '#FF9800', // 警告
DANGER: '#F44336', // 危险
TEXT: '#333333', // 文本色
}
```
---
## 六、常见问题
### Q1: 图标不显示?
**检查清单:**
- [ ] 文件是否已复制到 `static/iconfont/` 目录
- [ ] CSS路径是否正确修改
- [ ] 是否已在 App.vue 中引入
- [ ] 图标类名是否正确(如:`icon-home`
- [ ] 清除缓存并重新编译
### Q2: 如何查看可用的图标?
1. 打开下载包中的 `demo_index.html`
2. 或查看 `iconfont.css` 中的类名
3. 类名格式通常为 `.icon-xxx:before`
### Q3: 如何更新图标?
1. 在阿里图标库添加新图标到项目
2. 重新下载至本地
3. 替换 `static/iconfont/` 下的所有文件
4. 清除缓存,重新编译
### Q4: 小程序能用在线链接吗?
❌ 不能!微信小程序必须使用本地字体文件。
---
## 七、最佳实践
### ✅ 推荐做法
1. **统一管理图标名称**
```javascript
// 使用配置文件
import { ICONS } from '@/config/icons'
```
2. **使用封装的组件**
```vue
<IconfontIcon name="home" />
```
3. **预设常用尺寸和颜色**
```javascript
import { ICON_SIZES, ICON_COLORS } from '@/config/icons'
```
4. **语义化命名**
```javascript
const ICONS = {
HOME: 'home', // ✅ 语义清晰
USER_CENTER: 'user', // ✅ 明确用途
}
```
### ❌ 不推荐做法
1. **硬编码图标名称**
```vue
<text class="iconfont icon-home"></text> <!-- ❌ 不推荐 -->
```
2. **使用在线链接(小程序)**
```css
@import url("//at.alicdn.com/xxx.css"); /* ❌ 小程序不支持 */
```
3. **直接使用 Unicode**
```vue
<text class="iconfont">&#xe600;</text> <!-- ❌ 不直观 -->
```
---
## 八、测试页面
已为你创建了测试页面,可以查看各种使用方式:
**路径:** `pages/demo/iconfont-demo.vue`
在 `pages.json` 中添加页面配置即可访问:
```json
{
"path": "pages/demo/iconfont-demo",
"style": {
"navigationBarTitleText": "图标示例"
}
}
```
---
## 九、相关文件
```
项目结构:
├── components/
│ └── IconfontIcon/
│ └── IconfontIcon.vue # 图标组件
├── config/
│ └── icons.js # 图标配置
├── static/
│ └── iconfont/
│ ├── iconfont.css # 样式文件
│ ├── iconfont.ttf # 字体文件
│ ├── iconfont.woff # 字体文件
│ ├── iconfont.woff2 # 字体文件
│ └── README.md # 说明文档
├── pages/
│ └── demo/
│ └── iconfont-demo.vue # 测试页面
└── docs/
├── 阿里图标库引入指南.md # 详细文档
└── 阿里图标库快速开始.md # 本文档
```
---
## 十、总结
✅ **记住这三步:**
1. **下载** - 从阿里图标库下载文件
2. **放置** - 复制到 `static/iconfont/` 目录
3. **引入** - 在 `App.vue` 中引入 CSS
🎉 **就是这么简单!**
如有问题,请参考详细文档:`docs/阿里图标库引入指南.md`

View File

@@ -20,7 +20,16 @@ export function useColumnCount(onChange = () => {}) {
// }
// }
const calcColumn = () => {
const width = uni.getSystemInfoSync().windowWidth
// 使用新的API替代已废弃的getSystemInfoSync
let width
// #ifdef MP-WEIXIN
const mpSystemInfo = uni.getWindowInfo()
width = mpSystemInfo.windowWidth
// #endif
// #ifndef MP-WEIXIN
const otherSystemInfo = uni.getSystemInfoSync()
width = otherSystemInfo.windowWidth
// #endif
let count = 2
if (width >= 1000) {
@@ -46,15 +55,20 @@ export function useColumnCount(onChange = () => {}) {
onMounted(() => {
columnCount.value = 2
calcColumn()
// if (process.client) {
window.addEventListener('resize', calcColumn)
// }
// 只在H5环境下添加resize监听器
// #ifdef H5
if (typeof window !== 'undefined') {
window.addEventListener('resize', calcColumn)
}
// #endif
})
onUnmounted(() => {
// if (process.client) {
window.removeEventListener('resize', calcColumn)
// }
// #ifdef H5
if (typeof window !== 'undefined') {
window.removeEventListener('resize', calcColumn)
}
// #endif
})
// 列数变化时执行回调

View File

@@ -149,7 +149,19 @@ export function useAudioRecorder() {
const startRecording = async () => {
if (isRecording.value) return
// #ifdef MP-WEIXIN
$api.msg('小程序暂不支持语音识别功能');
return;
// #endif
// #ifdef H5
try {
if (typeof navigator === 'undefined' || !navigator.mediaDevices) {
$api.msg('当前环境不支持录音功能');
return;
}
recognizedText.value = ''
lastFinalText.value = ''
await connectWebSocket()
@@ -191,6 +203,7 @@ export function useAudioRecorder() {
console.error('启动失败:', err)
cleanup()
}
// #endif
}
const stopRecording = () => {

View File

@@ -4,39 +4,69 @@ import {
export function useScrollDirection(options = {}) {
const {
threshold = 200, // 滚动偏移阈值
throttleTime = 100, // 节流时间(毫秒)
onChange = null // 滚动方向变化的回调
threshold = 50, // 滚动偏移阈值,降低以更敏感
throttleTime = 16, // 节流时间(毫秒)约60fps
onChange = null, // 滚动方向变化的回调
hideThreshold = 100, // 隐藏区域的滚动阈值
enablePerformanceMode = true // 启用性能优化模式
} = options
const lastScrollTop = ref(0)
const accumulatedScroll = ref(0)
const isScrollingDown = ref(false)
const shouldHideTop = ref(false) // 控制顶部区域隐藏
const shouldStickyFilter = ref(false) // 控制筛选区域吸顶
let lastInvoke = 0
function handleScroll(e) {
const now = Date.now()
if (now - lastInvoke < throttleTime) return
if (enablePerformanceMode && now - lastInvoke < throttleTime) return
lastInvoke = now
const scrollTop = e.detail.scrollTop
const delta = scrollTop - lastScrollTop.value
accumulatedScroll.value += delta
if (accumulatedScroll.value > threshold) {
if (!isScrollingDown.value) {
isScrollingDown.value = true
onChange?.(true) // 通知变更为向下
// 控制顶部区域隐藏
if (scrollTop > hideThreshold) {
if (!shouldHideTop.value) {
shouldHideTop.value = true
}
} else {
if (shouldHideTop.value) {
shouldHideTop.value = false
}
accumulatedScroll.value = 0
}
if (accumulatedScroll.value < -threshold) {
if (isScrollingDown.value) {
isScrollingDown.value = false
onChange?.(false) // 通知变更为向上
// 控制筛选区域吸顶(当顶部区域隐藏时)
if (scrollTop > hideThreshold + 50) { // 稍微延迟吸顶
if (!shouldStickyFilter.value) {
shouldStickyFilter.value = true
}
} else {
if (shouldStickyFilter.value) {
shouldStickyFilter.value = false
}
}
// 滚动方向检测(仅在性能模式下使用阈值)
if (!enablePerformanceMode || Math.abs(accumulatedScroll.value) > threshold) {
if (accumulatedScroll.value > 0) {
// 向下滚动
if (!isScrollingDown.value) {
isScrollingDown.value = true
onChange?.(true) // 通知变更为向下
}
} else {
// 向上滚动
if (isScrollingDown.value) {
isScrollingDown.value = false
onChange?.(false) // 通知变更为向上
}
}
if (enablePerformanceMode) {
accumulatedScroll.value = 0
}
accumulatedScroll.value = 0
}
lastScrollTop.value = scrollTop
@@ -44,6 +74,8 @@ export function useScrollDirection(options = {}) {
return {
isScrollingDown,
shouldHideTop,
shouldStickyFilter,
handleScroll
}
}

View File

@@ -15,8 +15,17 @@ export function useTTSPlayer(wsUrl) {
const isPaused = ref(false)
const isComplete = ref(false)
const audioContext = new(window.AudioContext || window.webkitAudioContext)()
let playTime = audioContext.currentTime
// #ifdef H5
const audioContext = typeof window !== 'undefined' && (window.AudioContext || window.webkitAudioContext)
? new(window.AudioContext || window.webkitAudioContext)()
: null
// #endif
// #ifdef MP-WEIXIN
const audioContext = null // 微信小程序不支持 AudioContext
// #endif
let playTime = audioContext ? audioContext.currentTime : 0
let sourceNodes = []
let socket = null
let sampleRate = 16000
@@ -28,6 +37,11 @@ export function useTTSPlayer(wsUrl) {
let activePlayId = 0
const speak = (text) => {
if (!audioContext) {
console.warn('⚠️ TTS not supported in current environment');
return;
}
console.log('🎤 TTS speak function called');
console.log('📝 Text to synthesize:', text ? text.substring(0, 100) + '...' : 'No text');
console.log('🔗 WebSocket URL:', wsUrl);
@@ -44,6 +58,11 @@ export function useTTSPlayer(wsUrl) {
}
const pause = () => {
if (!audioContext) {
console.warn('⚠️ TTS not supported in current environment');
return;
}
console.log('⏸️ TTS pause called');
console.log('🔊 AudioContext state:', audioContext.state);
console.log('🔊 Is speaking before pause:', isSpeaking.value);
@@ -63,6 +82,11 @@ export function useTTSPlayer(wsUrl) {
}
const resume = () => {
if (!audioContext) {
console.warn('⚠️ TTS not supported in current environment');
return;
}
console.log('▶️ TTS resume called');
console.log('🔊 AudioContext state:', audioContext.state);
console.log('🔊 Is speaking before resume:', isSpeaking.value);
@@ -89,7 +113,7 @@ export function useTTSPlayer(wsUrl) {
isSpeaking.value = false
isPaused.value = false
isComplete.value = false
playTime = audioContext.currentTime
playTime = audioContext ? audioContext.currentTime : 0
sourceNodes.forEach(node => {
try {
@@ -113,11 +137,16 @@ export function useTTSPlayer(wsUrl) {
isSpeaking.value = false
isPaused.value = false
isComplete.value = false
playTime = audioContext.currentTime
playTime = audioContext ? audioContext.currentTime : 0
initWebSocket()
}
const initWebSocket = () => {
if (!audioContext) {
console.warn('⚠️ WebSocket TTS not supported in current environment');
return;
}
const thisPlayId = currentPlayId
console.log('🔌 Initializing WebSocket connection');
console.log('🔗 WebSocket URL:', wsUrl);
@@ -167,7 +196,7 @@ export function useTTSPlayer(wsUrl) {
console.log('✅ TTS synthesis completed');
isComplete.value = true
// 计算剩余播放时间,确保播放完整
const remainingTime = Math.max(0, (playTime - audioContext.currentTime) * 1000);
const remainingTime = audioContext ? Math.max(0, (playTime - audioContext.currentTime) * 1000) : 0;
console.log('⏱️ Remaining play time:', remainingTime + 'ms');
setTimeout(() => {
if (thisPlayId === activePlayId) {
@@ -205,6 +234,8 @@ export function useTTSPlayer(wsUrl) {
}
const pcmToAudioBuffer = (pcm, sampleRate, numChannels) => {
if (!audioContext) return null;
const length = pcm.length / numChannels
const audioBuffer = audioContext.createBuffer(numChannels, length, sampleRate)
for (let ch = 0; ch < numChannels; ch++) {
@@ -218,6 +249,8 @@ export function useTTSPlayer(wsUrl) {
}
const playBuffer = (audioBuffer) => {
if (!audioContext || !audioBuffer) return;
console.log('🎵 playBuffer called, duration:', audioBuffer.duration + 's');
if (!isSpeaking.value) {
playTime = audioContext.currentTime
@@ -259,7 +292,10 @@ export function useTTSPlayer(wsUrl) {
onHide(cancelAudio)
onUnload(cancelAudio)
initWebSocket()
// 只在支持 AudioContext 的环境中初始化 WebSocket
if (audioContext) {
initWebSocket()
}
return {
speak,

View File

@@ -13,6 +13,7 @@ import SelectPopup from '@/components/selectPopup/selectPopup.vue'
import SelectPopupPlugin from '@/components/selectPopup/selectPopupPlugin';
import RenderJobs from '@/components/renderJobs/renderJobs.vue';
import RenderCompanys from '@/components/renderCompanys/renderCompanys.vue';
// iconfont.css 已在 App.vue 中通过 @import 引入,无需在此处重复引入
// import Tabbar from '@/components/tabbar/midell-box.vue'
// 自动导入 directives 目录下所有指令
const directives = import.meta.glob('./directives/*.js', {
@@ -23,8 +24,8 @@ import {
createSSRApp,
} from 'vue'
const foldFeature = window.visualViewport && 'segments' in window.visualViewport
console.log('是否支持多段屏幕:', foldFeature)
// const foldFeature = window.visualViewport && 'segments' in window.visualViewport
// console.log('是否支持多段屏幕:', foldFeature)
// 全局组件
export function createApp() {

View File

@@ -1,102 +1,103 @@
{
"name": "qingdao-employment-service",
"appid": "__UNI__C939371",
"description": "招聘",
"versionName": "1.0.0",
"versionCode": "100",
"transformPx": false,
/* 5+App */
"app-plus": {
"usingComponents": true,
"nvueStyleCompiler": "uni-app",
"compilerVersion": 3,
"splashscreen": {
"alwaysShowBeforeRender": true,
"waiting": true,
"autoclose": true,
"delay": 0
},
/* */
"modules": {},
/* */
"distribute": {
/* android */
"android": {
"permissions": [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
/* ios */
"ios": {},
/* SDK */
"sdkConfigs": {}
}
},
/* */
"quickapp": {},
/* */
"mp-weixin": {
"appid": "",
"setting": {
"urlCheck": false,
"es6": true,
"postcss": true,
"minified": true
},
"usingComponents": true,
"permission": {
"scope.userLocation": {
"desc": "用于用户选择地图查看位置"
}
}
},
"mp-alipay": {
"usingComponents": true
},
"mp-baidu": {
"usingComponents": true
},
"mp-toutiao": {
"usingComponents": true
},
"uniStatistics": {
"enable": false
},
"vueVersion": "3",
"locale": "zh-Hans",
"h5": {
"router": {
"base": "/ks_app/",
"mode": "hash"
},
"title": "青岛智慧就业服务",
"optimization": {
"treeShaking": {
"enable": true
}
},
"sdkConfigs": {
"maps": {
"amap": {
"key": "9cfc9370bd8a941951da1cea0308e9e3",
"securityJsCode": "7b16386c7f744c3ca05595965f2b037f",
"serviceHost": ""
}
}
}
}
}
{
"name" : "qingdao-employment-service",
"appid" : "__UNI__F0ABFDF",
"description" : "招聘",
"versionName" : "1.0.0",
"versionCode" : "100",
"transformPx" : false,
/* 5+App */
"app-plus" : {
"usingComponents" : true,
"nvueStyleCompiler" : "uni-app",
"compilerVersion" : 3,
"splashscreen" : {
"alwaysShowBeforeRender" : true,
"waiting" : true,
"autoclose" : true,
"delay" : 0
},
/* */
"modules" : {},
/* */
"distribute" : {
/* android */
"android" : {
"permissions" : [
"<uses-permission android:name=\"android.permission.CHANGE_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.MOUNT_UNMOUNT_FILESYSTEMS\"/>",
"<uses-permission android:name=\"android.permission.VIBRATE\"/>",
"<uses-permission android:name=\"android.permission.READ_LOGS\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_WIFI_STATE\"/>",
"<uses-feature android:name=\"android.hardware.camera.autofocus\"/>",
"<uses-permission android:name=\"android.permission.ACCESS_NETWORK_STATE\"/>",
"<uses-permission android:name=\"android.permission.CAMERA\"/>",
"<uses-permission android:name=\"android.permission.GET_ACCOUNTS\"/>",
"<uses-permission android:name=\"android.permission.READ_PHONE_STATE\"/>",
"<uses-permission android:name=\"android.permission.CHANGE_WIFI_STATE\"/>",
"<uses-permission android:name=\"android.permission.WAKE_LOCK\"/>",
"<uses-permission android:name=\"android.permission.FLASHLIGHT\"/>",
"<uses-feature android:name=\"android.hardware.camera\"/>",
"<uses-permission android:name=\"android.permission.WRITE_SETTINGS\"/>"
]
},
/* ios */
"ios" : {},
/* SDK */
"sdkConfigs" : {}
}
},
/* */
"quickapp" : {},
/* */
"mp-weixin" : {
"appid" : "wx9d1cbc11c8c40ba7",
"setting" : {
"urlCheck" : false,
"es6" : true,
"postcss" : true,
"minified" : true
},
"usingComponents" : true,
"permission" : {
"scope.userLocation" : {
"desc" : "用于用户选择地图查看位置"
}
},
"libVersion" : "3.5.7"
},
"mp-alipay" : {
"usingComponents" : true
},
"mp-baidu" : {
"usingComponents" : true
},
"mp-toutiao" : {
"usingComponents" : true
},
"uniStatistics" : {
"enable" : false
},
"vueVersion" : "3",
"locale" : "zh-Hans",
"h5" : {
"router" : {
"base" : "/ks_app/",
"mode" : "hash"
},
"title" : "青岛智慧就业服务",
"optimization" : {
"treeShaking" : {
"enable" : true
}
},
"sdkConfigs" : {
"maps" : {
"amap" : {
"key" : "9cfc9370bd8a941951da1cea0308e9e3",
"securityJsCode" : "7b16386c7f744c3ca05595965f2b037f",
"serviceHost" : ""
}
}
}
}
}

View File

@@ -32,7 +32,7 @@ const pageState = reactive({
onLoad(() => {
console.log('onLoad');
// $api.sleep(2000).then(() => {
// navTo('/pages/login/login');
// navTo('/pages/complete-info/complete-info');
// });
getJobList();
});
@@ -42,7 +42,7 @@ onReachBottom(() => {
});
function navToPost(jobId) {
navTo(`/packageA/pages/post/post?jobId=${btoa(jobId)}`);
navTo(`/packageA/pages/post/post?jobId=${encodeURIComponent(jobId)}`);
}
function getJobList(type = 'add') {

View File

@@ -1,16 +1,5 @@
<template>
<AppLayout
title="添加工作经历"
border
back-gorund-color="#ffffff"
:show-bg-image="false"
>
<template #headerleft>
<view class="btn mar_le20 button-click" @click="navBack">取消</view>
</template>
<template #headerright>
<view class="btn mar_ri20 button-click" @click="handleConfirm">确认</view>
</template>
<view class="page-container">
<view class="content">
<view class="content-input">
<view class="input-titile">公司名称</view>
@@ -44,8 +33,12 @@
<textarea class="textarea-con" v-model="formData.description" placeholder-style="font-size: 16px" maxlength="500" placeholder="请输入工作描述"/>
</view>
</view>
</AppLayout>
<!-- 底部确认按钮 -->
<view class="bottom-confirm-btn">
<view class="confirm-btn" @click="handleConfirm">确认</view>
</view>
</view>
</template>
<script setup>
@@ -206,7 +199,6 @@
console.log('页面类型:', pageType.value);
let resData;
alert(editData.value.id)
// 根据页面类型调用不同的接口
if (pageType.value === 'edit' && editData.value?.id) {
// 编辑模式:调用更新接口
@@ -234,12 +226,18 @@
</script>
<style lang="stylus" scoped>
.page-container {
min-height: 100vh;
background-color: #ffffff;
position: relative;
}
.content{
padding: 28rpx;
display: flex;
flex-direction: column;
justify-content: flex-start
height: calc(100% - 120rpx)
justify-content: flex-start;
padding-bottom: 120rpx;
}
.content-input
margin-bottom: 52rpx
@@ -328,6 +326,30 @@
line-height: 20rpx
display: flex
align-items: center
// 底部确认按钮样式
.bottom-confirm-btn
position: fixed
bottom: 0
left: 0
right: 0
background-color: #ffffff
padding: 20rpx 28rpx
border-top: 2rpx solid #EBEBEB
z-index: 999
.confirm-btn
width: 100%
height: 90rpx
background: #256BFA
border-radius: 12rpx
font-weight: 500
font-size: 32rpx
color: #FFFFFF
text-align: center
line-height: 90rpx
button-click: true
// .content-sex
// height: 110rpx;
// display: flex

View File

@@ -89,7 +89,7 @@ function toSelectDate() {
}
function navToPost(jobId) {
navTo(`/packageA/pages/post/post?jobId=${btoa(jobId)}`);
navTo(`/packageA/pages/post/post?jobId=${encodeURIComponent(jobId)}`);
}
function searchCollection(e) {

View File

@@ -31,7 +31,7 @@
<input
class="input-con triangle"
disabled
v-if="!state.jobsText.length"
v-if="!state.jobsText || !state.jobsText.length"
placeholder="请选择您的求职岗位"
/>
<view class="input-nx" @click="changeJobs" v-else>
@@ -40,6 +40,7 @@
</view>
</view>
<SelectJobs ref="selectJobsModel"></SelectJobs>
<SelectPopup ref="selectPopupRef"></SelectPopup>
</AppLayout>
</template>
@@ -47,16 +48,24 @@
import { reactive, inject, watch, ref, onMounted, computed } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import SelectJobs from '@/components/selectJobs/selectJobs.vue';
import SelectPopup from '@/components/selectPopup/selectPopup.vue';
const { $api, navTo, navBack, config } = inject('globalFunction');
const openSelectPopup = inject('openSelectPopup');
// 创建本地的 openSelectPopup 函数
const openSelectPopup = (config) => {
if (selectPopupRef.value) {
selectPopupRef.value.open(config);
}
};
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
import useDictStore from '@/stores/useDictStore';
const { userInfo } = storeToRefs(useUserStore());
const { getUserResume } = useUserStore();
const { dictLabel, oneDictData } = useDictStore();
const { dictLabel, oneDictData, getDictData } = useDictStore();
const selectJobsModel = ref();
const selectPopupRef = ref();
const percent = ref('0%');
const salay = [2, 5, 10, 15, 20, 25, 30, 50, 80, 100];
const state = reactive({
@@ -72,7 +81,9 @@ const fromValue = reactive({
area: '',
jobTitleId: [],
});
onLoad(() => {
onLoad(async () => {
// 初始化字典数据
await getDictData();
initLoad();
});
const confirm = () => {
@@ -99,8 +110,12 @@ function initLoad() {
fromValue.jobTitleId = userInfo.value.jobTitleId;
// 回显
state.areaText = dictLabel('area', Number(userInfo.value.area));
state.salayText = `${userInfo.value.salaryMin}-${userInfo.value.salaryMax}`;
state.jobsText = userInfo.value.jobTitle;
if (userInfo.value.salaryMin && userInfo.value.salaryMax) {
state.salayText = `${userInfo.value.salaryMin}-${userInfo.value.salaryMax}`;
} else {
state.salayText = '';
}
state.jobsText = userInfo.value.jobTitle || [];
const result = getFormCompletionPercent(fromValue);
percent.value = result;
}
@@ -123,7 +138,8 @@ const changeSalary = () => {
const copyri = JSON.parse(JSON.stringify(salay));
const [lf, ri] = e.detail.value;
const risalay = copyri.slice(lf, copyri.length);
this.setColunm(1, risalay);
// 更新右侧选项
state.risalay = risalay;
leftIndex = salayData[0];
}
},

View File

@@ -122,28 +122,23 @@
</view>
<!-- 4. 新增简历上传区域固定在页面底部 -->
<view class="resume-upload-section">
<!-- 上传按钮 -->
<!-- <view class="resume-upload-section">
<button class="upload-btn" @click="handleResumeUpload" :loading="isUploading" :disabled="isUploading">
<uni-icons type="cloud-upload" size="20"></uni-icons>
<!-- <image class="upload-icon" src="/static/icons/upload-file.png" mode="widthFix"></image> -->
<text class="upload-text">
{{ uploadedResumeName || '上传简历' }}
</text>
<!-- 已上传时显示重新上传文字 -->
<text class="reupload-text" v-if="uploadedResumeName">重新上传</text>
</button>
<!-- 上传说明 -->
<text class="upload-tip">支持 PDFWord 格式文件大小不超过 20MB</text>
<!-- 已上传文件信息可选 -->
<view class="uploaded-file-info" v-if="uploadedResumeName">
<image class="file-icon" src="/static/icons/file-icon.png" mode="widthFix"></image>
<text class="file-name">{{ uploadedResumeName }}</text>
<button class="delete-file-btn" size="mini" @click.stop="handleDeleteResume">删除</button>
</view>
</view>
</view> -->
</view>
</template>

View File

@@ -16,27 +16,29 @@
</template>
<view class="content" v-show="!isEmptyObject(jobInfo)">
<view class="content-top btn-feel">
<view class="top-salary">
<Salary-Expectation
:max-salary="jobInfo.maxSalary"
:min-salary="jobInfo.minSalary"
:is-month="true"
></Salary-Expectation>
</view>
<view class="top-name">{{ jobInfo.jobTitle }}</view>
<view class="top-info">
<view class="info-img"><image src="/static/icon/post12.png"></image></view>
<view class="info-text">
<dict-Label dictType="experience" :value="jobInfo.experience"></dict-Label>
<view style="background: #ffffff;padding: 24rpx;box-shadow: 0rpx 0rpx 8rpx 0rpx rgba(0,0,0,0.04);border-radius: 20rpx 20rpx 20rpx 20rpx;position: relative;overflow: hidden;">
<view class="top-salary">
<Salary-Expectation
:max-salary="jobInfo.maxSalary"
:min-salary="jobInfo.minSalary"
:is-month="true"
></Salary-Expectation>
</view>
<view class="info-img mar_le20"><image src="/static/icon/post13.png"></image></view>
<view class="info-text">
<dict-Label dictType="education" :value="jobInfo.education"></dict-Label>
<view class="top-name">{{ jobInfo.jobTitle }}</view>
<view class="top-info">
<view class="info-img"><image src="/static/icon/post12.png"></image></view>
<view class="info-text">
<dict-Label dictType="experience" :value="jobInfo.experience"></dict-Label>
</view>
<view class="info-img mar_le20"><image src="/static/icon/post13.png"></image></view>
<view class="info-text">
<dict-Label dictType="education" :value="jobInfo.education"></dict-Label>
</view>
</view>
<view class="position-source">
<text>来源&nbsp;</text>
{{ jobInfo.dataSource }}
</view>
</view>
<view class="position-source">
<text>来源&nbsp;</text>
{{ jobInfo.dataSource }}
</view>
</view>
<view class="ai-explain" v-if="jobInfo.isExplain">
@@ -98,7 +100,7 @@
></map>
</view>
</view>
<view class="content-card" v-if="!userInfo.isCompanyUser">
<view class="content-card" v-if="currentUserType !== 0">
<view class="card-title">
<text class="title">竞争力分析</text>
</view>
@@ -191,6 +193,12 @@ import RadarMap from './component/radarMap.vue';
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
const { userInfo } = storeToRefs(useUserStore());
// 与首页一致的用户类型获取优先store兜底缓存
const currentUserType = computed(() => {
const storeIsCompanyUser = userInfo.value?.isCompanyUser;
const cachedIsCompanyUser = (uni.getStorageSync('userInfo') || {}).isCompanyUser;
return Number(storeIsCompanyUser !== undefined ? storeIsCompanyUser : cachedIsCompanyUser);
});
const { $api, navTo, getLenPx, parseQueryParams, navBack, isEmptyObject } = inject('globalFunction');
import config from '@/config.js';
const matchingDegree = ref(['一般', '良好', '优秀', '极好']);
@@ -245,7 +253,7 @@ onShow(() => {
});
function initLoad(option) {
const jobId = atob(option.jobId);
const jobId = decodeURIComponent(option.jobId);
if (jobId !== jobIdRef.value) {
jobIdRef.value = jobId;
getDetail(jobId);
@@ -263,11 +271,11 @@ function seeExplain() {
function getDetail(jobId) {
return new Promise((reslove, reject) => {
$api.createRequest(`/app/job/${jobId}`).then((resData) => {
const { latitude, longitude, companyName, companyId, isCompanyUser } = resData.data;
const { latitude, longitude, companyName, companyId } = resData.data;
jobInfo.value = resData.data;
reslove(resData.data);
getCompanyIsAJobs(companyId);
if (isCompanyUser) {
if (currentUserType.value !== 0) {
getCompetivetuveness(jobId);
}
// getCompetivetuveness(jobId);
@@ -503,11 +511,11 @@ for i in 0..100
.content{
padding: 0 28rpx
height: 100%
padding-top: 28rpx
.content-top{
background: #FFFFFF;
box-shadow: 0rpx 0rpx 8rpx 0rpx rgba(0,0,0,0.04);
border-radius: 20rpx 20rpx 20rpx 20rpx;
padding: 52rpx 32rpx 34rpx 32rpx
padding: 24rpx
position: relative
overflow: hidden
.top-salary{

View File

@@ -53,7 +53,7 @@ function nextDetail() {
recommedIndexDb.addRecord(recordData);
}
console.log(job.jobId);
navTo(`/packageA/pages/post/post?jobId=${btoa(job.jobId)}`);
navTo(`/packageA/pages/post/post?jobId=${encodeURIComponent(job.jobId)}`);
}
function getNextVideoSrc(num) {

View File

@@ -0,0 +1,11 @@
<template>
<view>
招聘会详情
</view>
</template>
<script>
</script>
<style>
</style>

View File

@@ -3,7 +3,8 @@
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "青岛智慧就业平台",
"navigationBarTitleText": "喀什智慧就业平台",
"navigationBarTitleTextSize": "30rpx",
// #ifdef H5
"navigationStyle": "custom"
// #endif
@@ -13,6 +14,7 @@
"path": "pages/mine/mine",
"style": {
"navigationBarTitleText": "我的",
"navigationBarTitleTextSize": "30rpx",
"navigationStyle": "custom"
}
},
@@ -20,21 +22,39 @@
"path": "pages/msglog/msglog",
"style": {
"navigationBarTitleText": "消息",
"navigationStyle": "custom",
"enablePullDownRefresh": false
"navigationBarTitleTextSize": "30rpx"
// "navigationStyle": "custom",
// "enablePullDownRefresh": false
}
},
{
"path": "pages/careerfair/careerfair",
"style": {
"navigationBarTitleText": "招聘会",
"navigationStyle": "custom"
"navigationBarTitleTextSize": "30rpx"
// "navigationStyle": "custom"
}
},
{
"path": "pages/login/login",
"path": "pages/complete-info/complete-info",
"style": {
"navigationBarTitleText": "AI+就业服务程序",
"navigationBarTitleText": "补全信息",
"navigationBarTitleTextSize": "30rpx"
// "navigationStyle": "custom"
}
},
{
"path": "pages/complete-info/company-info",
"style": {
"navigationBarTitleText": "企业信息",
"navigationBarTitleTextSize": "30rpx"
// "navigationStyle": "custom"
}
},
{
"path": "pages/complete-info/components/map-location-picker",
"style": {
"navigationBarTitleText": "选择地址",
"navigationStyle": "custom"
}
},
@@ -47,38 +67,125 @@
"navigationStyle": "custom"
}
},
{
"path": "pages/test/userTypeTest",
"style": {
"navigationBarTitleText": "用户类型测试",
"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/test/company-mine-test",
"style": {
"navigationBarTitleText": "企业我的页面测试",
"navigationStyle": "custom"
}
},
{
"path": "pages/job/publishJob",
"style": {
"navigationBarTitleText": "发布岗位"
// "navigationStyle": "custom",
// "disableScroll": false,
// "enablePullDownRefresh": false,
// "onReachBottomDistance": 50,
// "backgroundColor": "#f5f5f5"
}
},
{
"path": "pages/job/companySearch",
"style": {
"navigationBarTitleText": "选择企业",
"navigationStyle": "custom",
"disableScroll": false,
"enablePullDownRefresh": false,
"backgroundColor": "#f5f5f5"
}
},
{
"path": "pages/chat/chat",
"style": {
"navigationBarTitleText": "AI+",
"navigationBarTitleTextSize": "30rpx",
"navigationBarBackgroundColor": "#4778EC",
"navigationBarTextStyle": "white",
"enablePullDownRefresh": false,
"enablePullDownRefresh": false
// #ifdef H5
"navigationStyle": "custom"
// "navigationStyle": "custom"
//#endif
}
},
{
"path": "pages/search/search",
"style": {
"navigationBarTitleText": "",
"navigationStyle": "custom"
"navigationBarTitleText": ""
// "navigationStyle": "custom"
}
},
{
"path" : "packageA/pages/addWorkExperience/addWorkExperience",
"style" :
{
"navigationBarTitleText" : "添加工作经历",
"navigationStyle": "custom"
}
"path": "pages/mine/company-mine",
"style": {
"navigationBarTitleText": "我的",
"navigationBarTitleTextSize": "30rpx"
// "navigationStyle": "custom"
}
},
{
"path": "pages/mine/company-info",
"style": {
"navigationBarTitleText": "企业信息",
"navigationBarTitleTextSize": "30rpx"
}
},
{
"path": "pages/mine/edit-company-contacts",
"style": {
"navigationBarTitleText": "编辑联系人",
"navigationBarTitleTextSize": "30rpx"
}
}
],
"subpackages": [{
"root": "packageA",
"pages": [{
"pages": [
{
"path" : "pages/addWorkExperience/addWorkExperience",
"style" :
{
"navigationBarTitleText" : "添加工作经历",
"navigationBarTitleTextSize": "30rpx"
// "navigationStyle": "custom"
}
},{
"path": "pages/choiceness/choiceness",
"style": {
"navigationBarTitleText": "精选",
@@ -91,16 +198,15 @@
"style": {
"navigationBarTitleText": "职位详情",
"navigationBarBackgroundColor": "#4778EC",
"navigationBarTextStyle": "white",
"navigationStyle": "custom"
"navigationBarTextStyle": "white"
}
}, {
"path": "pages/UnitDetails/UnitDetails",
"style": {
"navigationBarTitleText": "单位详情",
"navigationBarBackgroundColor": "#4778EC",
"navigationBarTextStyle": "white",
"navigationStyle": "custom"
"navigationBarTitleText": "单位详情"
// "navigationBarBackgroundColor": "#4778EC",
// "navigationBarTextStyle": "white"
// "navigationStyle": "custom"
}
}, {
"path": "pages/exhibitors/exhibitors",
@@ -110,22 +216,25 @@
"navigationBarTextStyle": "white",
"navigationStyle": "custom"
}
}, {
}, {
"path": "pages/myResume/myResume",
"style": {
"navigationBarTitleText": "我的简历",
"navigationBarTitleTextSize": "30rpx",
"navigationBarBackgroundColor": "#FFFFFF"
}
}, {
}, {
"path": "pages/Intendedposition/Intendedposition",
"style": {
"navigationBarTitleText": "投递记录",
"navigationBarTitleTextSize": "30rpx",
"navigationBarBackgroundColor": "#FFFFFF"
}
}, {
}, {
"path": "pages/collection/collection",
"style": {
"navigationBarTitleText": "我的收藏",
"navigationBarTitleTextSize": "30rpx",
"navigationBarBackgroundColor": "#FFFFFF",
"navigationStyle": "custom"
}
@@ -134,6 +243,7 @@
"path": "pages/browseJob/browseJob",
"style": {
"navigationBarTitleText": "我的浏览",
"navigationBarTitleTextSize": "30rpx",
"navigationBarBackgroundColor": "#FFFFFF",
"navigationStyle": "custom"
}
@@ -142,6 +252,7 @@
"path": "pages/addPosition/addPosition",
"style": {
"navigationBarTitleText": "添加岗位",
"navigationBarTitleTextSize": "30rpx",
"navigationStyle": "custom"
}
},
@@ -156,6 +267,7 @@
"path": "pages/personalInfo/personalInfo",
"style": {
"navigationBarTitleText": "个人信息",
"navigationBarTitleTextSize": "30rpx",
"navigationStyle": "custom"
}
},
@@ -163,13 +275,15 @@
"path": "pages/jobExpect/jobExpect",
"style": {
"navigationBarTitleText": "求职期望",
"navigationStyle": "custom"
"navigationBarTitleTextSize": "30rpx"
// "navigationStyle": "custom"
}
},
{
"path": "pages/reservation/reservation",
"style": {
"navigationBarTitleText": "我的预约",
"navigationBarTitleTextSize": "30rpx",
"navigationBarBackgroundColor": "#FFFFFF"
}
},
@@ -177,6 +291,7 @@
"path": "pages/choicenessList/choicenessList",
"style": {
"navigationBarTitleText": "精选企业",
"navigationBarTitleTextSize": "30rpx",
"navigationBarBackgroundColor": "#FFFFFF",
"navigationStyle": "custom"
}
@@ -185,6 +300,7 @@
"path": "pages/newJobPosition/newJobPosition",
"style": {
"navigationBarTitleText": "新职位推荐",
"navigationBarTitleTextSize": "30rpx",
"navigationBarBackgroundColor": "#FFFFFF"
}
},
@@ -192,6 +308,7 @@
"path": "pages/systemNotification/systemNotification",
"style": {
"navigationBarTitleText": "系统通知",
"navigationBarTitleTextSize": "30rpx",
"navigationBarBackgroundColor": "#FFFFFF"
}
},
@@ -207,6 +324,7 @@
"path": "pages/moreJobs/moreJobs",
"style": {
"navigationBarTitleText": "更多岗位",
"navigationBarTitleTextSize": "30rpx",
"navigationBarBackgroundColor": "#FFFFFF"
}
},
@@ -214,6 +332,7 @@
"path": "pages/collection/compare",
"style": {
"navigationBarTitleText": " 岗位对比",
"navigationBarTitleTextSize": "30rpx",
"navigationBarBackgroundColor": "#FFFFFF"
}
},
@@ -221,64 +340,82 @@
"path": "pages/myResume/corporateInformation",
"style": {
"navigationBarTitleText": " 企业详情",
"navigationBarTitleTextSize": "30rpx",
"navigationBarBackgroundColor": "#FFFFFF"
}
}
]
}],
"tabBar": {
"custom": true,
"display": "none",
"color": "#5E5F60",
"selectedColor": "#256BFA",
"borderStyle": "black",
"backgroundColor": "#ffffff",
"midButton": {
"width": "50px",
"height": "50px",
"backgroundImage": "static/tabbar/logo2copy.png"
},
"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"
},
{
"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": "我的"
}
]
},
{
"root": "packageB",
"pages": [
{
"path" : "jobFair/detail",
"style" :
{
"navigationBarTitleText" : "招聘会详情",
"navigationBarTitleTextSize": "30rpx"
// "navigationStyle": "custom"
}
}
]
}],
// "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",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8",
"navigationBarTitleTextSize": "18px"
// "enablePullDownRefresh": false,
// "navigationStyle": "custom",
"rpxCalcBaseDeviceWidth": 375,
"rpxCalcMaxDeviceWidth": 750,
"rpxCalcIncludeWidth": 750
// "navigationStyle": "custom"
},
"easycom": {
"autoscan": true,
"custom": {
"^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",
"^CustomTabBar$": "@/components/CustomTabBar/CustomTabBar.vue"
}
},
"uniIdRouter": {}
}

View File

@@ -83,18 +83,24 @@
<empty v-else pdTop="200"></empty>
</scroll-view>
</view>
<Tabbar :currentpage="1"></Tabbar>
<!-- 自定义tabbar -->
<CustomTabBar :currentPage="1" />
<!-- 微信授权登录弹窗 -->
<WxAuthLogin ref="wxAuthLoginRef" @success="handleLoginSuccess"></WxAuthLogin>
</view>
</view>
</template>
<script setup>
import { reactive, inject, watch, ref, onMounted } from 'vue';
import { reactive, inject, watch, ref, onMounted, onUnmounted } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import Tabbar from '@/components/tabbar/midell-box.vue';
import useLocationStore from '@/stores/useLocationStore';
import { storeToRefs } from 'pinia';
import { tabbarManager } from '@/utils/tabbarManager';
import WxAuthLogin from '@/components/WxAuthLogin/WxAuthLogin.vue';
const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
const wxAuthLoginRef = ref(null);
const { $api, navTo, cloneDeep } = inject('globalFunction');
const weekList = ref([]);
const fairList = ref([]);
@@ -125,6 +131,28 @@ onLoad(() => {
getFair('refresh');
});
onShow(() => {
// 更新自定义tabbar选中状态
tabbarManager.updateSelected(1);
});
onMounted(() => {
// 监听退出登录事件,显示微信登录弹窗
uni.$on('showLoginModal', () => {
wxAuthLoginRef.value?.open();
});
});
onUnmounted(() => {
uni.$off('showLoginModal');
});
// 登录成功回调
const handleLoginSuccess = () => {
console.log('登录成功');
// 可以在这里添加登录成功后的处理逻辑
};
function toSelectDate() {
navTo('/packageA/pages/selectDate/selectDate', {
query: {

View File

@@ -1,5 +1,10 @@
<template>
<view class="container">
<!-- 自定义tabbar -->
<CustomTabBar :currentPage="2" />
<!-- 微信授权登录弹窗 -->
<WxAuthLogin ref="wxAuthLoginRef" @success="handleLoginSuccess"></WxAuthLogin>
<!-- 抽屉遮罩层 -->
<view v-if="isDrawerOpen" class="overlay" @click="toggleDrawer"></view>
@@ -64,21 +69,22 @@
</view>
<!-- 自定义tabbar -->
<view class="chatmain-footer" v-show="!isDrawerOpen">
<Tabbar :currentpage="2"></Tabbar>
<!-- 统一使用系统tabBar -->
</view>
</view>
</view>
</template>
<script setup>
import { ref, inject, nextTick, computed } from 'vue';
import { ref, inject, nextTick, computed, onMounted, onUnmounted } from 'vue';
const { $api, navTo, insertSortData, config } = inject('globalFunction');
import { onLoad, onShow, onHide } from '@dcloudio/uni-app';
import Tabbar from '@/components/tabbar/midell-box.vue';
import useChatGroupDBStore from '@/stores/userChatGroupStore';
import useUserStore from '@/stores/useUserStore';
import { tabbarManager } from '@/utils/tabbarManager';
import aiPaging from './components/ai-paging.vue';
import { storeToRefs } from 'pinia';
import WxAuthLogin from '@/components/WxAuthLogin/WxAuthLogin.vue';
const { isTyping, tabeList, chatSessionID } = storeToRefs(useChatGroupDBStore());
const { userInfo } = storeToRefs(useUserStore());
const isDrawerOpen = ref(false);
@@ -86,6 +92,7 @@ const scrollIntoView = ref(false);
const searchText = ref('');
const paging = ref(null);
const wxAuthLoginRef = ref(null);
// 实时过滤
const filteredList = computed(() => {
@@ -102,10 +109,29 @@ onLoad(() => {
onShow(() => {
nextTick(() => {
paging.value?.colseFile();
paging.value?.closeFile();
});
// 更新自定义tabbar选中状态
tabbarManager.updateSelected(2);
});
onMounted(() => {
// 监听退出登录事件,显示微信登录弹窗
uni.$on('showLoginModal', () => {
wxAuthLoginRef.value?.open();
});
});
onUnmounted(() => {
uni.$off('showLoginModal');
});
// 登录成功回调
const handleLoginSuccess = () => {
console.log('登录成功');
// 可以在这里添加登录成功后的处理逻辑
};
onHide(() => {
paging.value?.handleTouchCancel();
if (isDrawerOpen.value) {

View File

@@ -1,11 +1,7 @@
<template>
<view class="chat-container">
<!-- #ifdef MP-WEIXIN -->
<view class="chat-background">
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<view class="chat-background" v-fade:600="!messages.length">
<!-- #endif -->
<view class="chat-background" v-if="!messages.length">
<image class="backlogo" src="/static/icon/backAI.png"></image>
<view class="back-rowTitle">欢迎使用{{ config.appInfo.areaName }}AI智能求职</view>
<view class="back-rowText">
@@ -24,6 +20,28 @@
<view class="message">{{ recognizedText }} {{ lastFinalText }}</view>
</view>
</view>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<view class="chat-background" v-if="!messages.length">
<image class="backlogo" src="/static/icon/backAI.png"></image>
<view class="back-rowTitle">欢迎使用{{ config.appInfo.areaName }}AI智能求职</view>
<view class="back-rowText">
我可以根据您的简历和求职需求帮你精准匹配{{ config.appInfo.areaName }}互联网招聘信息对比招聘信息的优缺点提供面试指导等请把你的任务交给我吧~
</view>
<view class="back-rowh3">猜你所想</view>
<view
class="back-rowmsg button-click"
v-for="(item, index) in queries"
:key="index"
@click="sendMessageGuess(item)"
>
{{ item }}
</view>
<view class="chat-item self" v-if="isRecording">
<view class="message">{{ recognizedText }} {{ lastFinalText }}</view>
</view>
</view>
<!-- #endif -->
<scroll-view class="chat-list scrollView" :scroll-top="scrollTop" :scroll-y="true" scroll-with-animation>
<!-- #ifdef MP-WEIXIN -->
<view class="chat-list list-content">
@@ -249,14 +267,13 @@ import {
ref,
inject,
nextTick,
defineProps,
defineEmits,
onMounted,
onUnmounted,
toRaw,
reactive,
computed,
watch,
getCurrentInstance,
} from 'vue';
import { storeToRefs } from 'pinia';
// import config from '@/config.js';
@@ -289,6 +306,9 @@ const {
const { speak, pause, resume, isSpeaking, isPaused, cancelAudio } = useTTSPlayer(config.speechSynthesis);
// 获取组件实例(用于小程序 SelectorQuery
const instance = getCurrentInstance();
// state
const queries = ref([]);
const guessList = ref([]);
@@ -338,18 +358,46 @@ onMounted(async () => {
});
const requestMicPermission = async () => {
// #ifdef H5
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
console.log('✅ 麦克风权限已授权');
if (typeof navigator !== 'undefined' && navigator.mediaDevices) {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
console.log('✅ 麦克风权限已授权');
// 立刻停止所有音轨,释放麦克风
stream.getTracks().forEach((track) => track.stop());
// 立刻停止所有音轨,释放麦克风
stream.getTracks().forEach((track) => track.stop());
return true;
return true;
} else {
console.warn('❌ 当前环境不支持麦克风');
return false;
}
} catch (err) {
console.warn('❌ 用户拒绝麦克风权限或不支持:', err);
return false;
}
// #endif
// #ifdef MP-WEIXIN
try {
// 微信小程序使用 uni.authorize 请求权限
const res = await uni.authorize({
scope: 'scope.record'
});
console.log('✅ 麦克风权限已授权');
return true;
} catch (err) {
console.warn('❌ 用户拒绝麦克风权限:', err);
// 用户拒绝授权,但不影响其他功能
return false;
}
// #endif
// #ifndef H5 || MP-WEIXIN
// 其他平台暂不支持
console.warn('❌ 当前平台不支持麦克风权限检测');
return false;
// #endif
};
function showControll(index) {
@@ -475,10 +523,20 @@ const delfile = (file) => {
const scrollToBottom = throttle(function () {
nextTick(() => {
try {
// #ifdef MP-WEIXIN
const query = uni.createSelectorQuery().in(instance);
// #endif
// #ifndef MP-WEIXIN
const query = uni.createSelectorQuery();
// #endif
query.select('.scrollView').boundingClientRect();
query.select('.list-content').boundingClientRect();
query.exec((res) => {
if (!res || !res[0] || !res[1]) {
console.warn('scrollToBottom: 元素未找到或尚未渲染');
return;
}
const scrollViewHeight = res[0].height;
const scrollContentHeight = res[1].height;
if (scrollContentHeight > scrollViewHeight) {
@@ -647,7 +705,7 @@ function changeShowFile() {
showfile.value = !showfile.value;
}
function colseFile() {
function closeFile() {
showfile.value = false;
}
@@ -773,7 +831,7 @@ function getRandomJobQueries(queries, count = 2) {
return shuffled.slice(0, count); // 取前 count 条
}
defineExpose({ scrollToBottom, closeGuess, colseFile, changeQueries, handleTouchCancel });
defineExpose({ scrollToBottom, closeGuess, closeFile, changeQueries, handleTouchCancel });
</script>
<style lang="stylus" scoped>
@@ -906,19 +964,19 @@ image-margin-top = 40rpx
white-space: pre-wrap;
}
.list-content {
padding: 0 44rpx 44rpx 44rpx;
padding: 0 44rpx 10rpx 44rpx;
}
.chat-item {
display: flex;
align-items: flex-start;
margin-bottom: 20rpx;
margin-bottom: 10rpx;
width: 100%;
}
.chat-item.self {
justify-content: flex-end;
}
.message
margin-top: 40rpx
margin-top: 0rpx
// max-width: 80%;
width: 100%;
word-break: break-word;
@@ -963,10 +1021,12 @@ image-margin-top = 40rpx
}
.input-area {
padding: 32rpx 28rpx 24rpx 28rpx;
padding-bottom: calc(24rpx + env(safe-area-inset-bottom) + 40rpx - 40rpx);
position: relative;
background: #FFFFFF;
box-shadow: 0rpx -4rpx 10rpx 0rpx rgba(11,44,112,0.06);
transition: height 2s ease-in-out;
z-index: 1001;
}
.input-area::after
position: absolute

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,855 @@
<template>
<AppLayout title="AI+就业服务程序">
<view class="tab-container">
<view class="uni-margin-wrap">
<swiper
class="swiper"
:current="tabCurrent"
:circular="false"
:indicator-dots="false"
:autoplay="false"
:duration="500"
>
<swiper-item @touchmove.stop="false">
<view class="login-content">
<image class="logo" src="@/static/logo.png"></image>
<view class="logo-title">就业</view>
</view>
<view class="btns">
<button class="wxlogin" @click="loginTest">内测登录</button>
<view class="wxaddress">{{ config.appInfo.areaName }}公共就业和人才服务中心</view>
</view>
</swiper-item>
<swiper-item @touchmove.stop="false">
<view class="content-one">
<view>
<view class="content-title">
<view class="title-lf">
<view class="lf-h1">请您完善求职名片</view>
<view class="lf-text">个人信息仅用于推送优质内容</view>
</view>
<view class="title-ri">
<text style="color: #256bfa">1</text>
<text>/2</text>
</view>
</view>
<view class="content-input" :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" :class="{ 'input-error': sexError }">
<view class="sex-titile">性别</view>
<view class="sext-ri">
<view
class="sext-box"
:class="{ 'sext-boxactive': fromValue.sex === 0 }"
@click="changeSex(0)"
>
</view>
<view
class="sext-box"
:class="{ 'sext-boxactive': fromValue.sex === 1 }"
@click="changeSex(1)"
>
</view>
</view>
</view>
<view 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="本科" />
</view>
<view class="content-input" :class="{ 'input-error': idCardError }">
<view class="input-titile">身份证</view>
<input
class="input-con2"
v-model="fromValue.idCard"
maxlength="18"
placeholder="请输入身份证号码"
@input="validateIdCard"
/>
<view v-if="idCardError" class="error-message">{{ idCardError }}</view>
<view v-if="fromValue.idCard && !idCardError" class="success-message"> 身份证格式正确</view>
</view>
</view>
<view class="next-btn" @tap="nextStep">下一步</view>
</view>
</swiper-item>
<swiper-item @touchmove.stop="false">
<view class="content-one">
<view>
<view class="content-title">
<view class="title-lf">
<view class="lf-h1">请您完善求职名片</view>
<view class="lf-text">个人信息仅用于推送优质内容</view>
</view>
<view class="title-ri">
<text style="color: #256bfa">2</text>
<text>/2</text>
</view>
</view>
<view class="content-input" @click="changeArea">
<view class="input-titile">求职区域</view>
<input
class="input-con"
v-model="state.areaText"
disabled
placeholder="请选择您的求职区域"
/>
</view>
<view class="content-input" @click="changeJobs">
<view class="input-titile">求职岗位</view>
<input
class="input-con"
disabled
v-if="!state.jobsText.length"
placeholder="请选择您的求职岗位"
/>
<view class="input-nx" @click="changeJobs" v-else>
<view class="nx-item" v-for="item in state.jobsText">{{ item }}</view>
</view>
</view>
<view class="content-input" @click="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
class="input-con"
v-model="state.salayText"
disabled
placeholder="请选择您的期望薪资"
/>
</view>
</view>
<view class="next-btn" @tap="complete">开启求职之旅</view>
</view>
</swiper-item>
</swiper>
</view>
</view>
<SelectJobs ref="selectJobsModel"></SelectJobs>
<SelectPopup ref="selectPopupRef"></SelectPopup>
</AppLayout>
</template>
<script setup>
import SelectJobs from '@/components/selectJobs/selectJobs.vue';
import SelectPopup from '@/components/selectPopup/selectPopup.vue';
import { reactive, inject, watch, ref, onMounted } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import useUserStore from '@/stores/useUserStore';
import useDictStore from '@/stores/useDictStore';
const { $api, navTo, config, IdCardValidator } = inject('globalFunction');
const { loginSetToken, getUserResume } = useUserStore();
const { getDictSelectOption, oneDictData } = useDictStore();
// #ifdef H5
const injectedOpenSelectPopup = inject('openSelectPopup', null);
// #endif
// status
const selectJobsModel = ref();
const selectPopupRef = ref();
// 创建本地的 openSelectPopup 函数
const openSelectPopup = (config) => {
// #ifdef MP-WEIXIN
if (selectPopupRef.value) {
selectPopupRef.value.open(config);
}
// #endif
// #ifdef H5
if (injectedOpenSelectPopup) {
injectedOpenSelectPopup(config);
}
// #endif
};
const tabCurrent = ref(1);
const salay = [2, 5, 10, 15, 20, 25, 30, 50, 80, 100];
const state = reactive({
station: [],
stationCateLog: 1,
lfsalay: [2, 5, 10, 15, 20, 25, 30, 50],
risalay: JSON.parse(JSON.stringify(salay)),
areaText: '',
educationText: '',
workExperience: '',
salayText: '',
jobsText: [],
skillLevelText: '',
skillsText: [],
});
const fromValue = reactive({
sex: null,
education: '4',
salaryMin: 2000,
salaryMax: 2000,
area: 0,
jobTitleId: '',
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();
});
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();
// 如果为空,清除错误信息
if (!idCard) {
idCardError.value = '';
return;
}
// 使用身份证校验器进行校验
const result = IdCardValidator.validate(idCard);
if (result.valid) {
idCardError.value = '';
} else {
idCardError.value = result.message;
}
}
function changeExperience() {
openSelectPopup({
title: '工作经验',
maskClick: true,
data: [oneDictData('experience')],
success: (_, [value]) => {
fromValue.workExperience = value.value;
state.workExperience = value.label;
// 选择后清除工作经验错误
experienceError.value = '';
},
change(_, [value]) {
// this.setColunm(1, [123, 123]);
console.log(this);
},
});
}
function changeEducation() {
openSelectPopup({
title: '学历',
maskClick: true,
data: [oneDictData('education')],
success: (_, [value]) => {
fromValue.area = value.value;
state.educationText = value.label;
},
});
}
function changeArea() {
openSelectPopup({
title: '区域',
maskClick: true,
data: [oneDictData('area')],
success: (_, [value]) => {
fromValue.area = value.value;
state.areaText = config.appInfo.areaName + '-' + value.label;
},
});
}
function changeSalay() {
let leftIndex = 0;
openSelectPopup({
title: '薪资',
maskClick: true,
data: [state.lfsalay, state.risalay],
unit: 'k',
success: (_, [min, max]) => {
fromValue.salaryMin = min.value * 1000;
fromValue.salaryMax = max.value * 1000;
state.salayText = `${fromValue.salaryMin}-${fromValue.salaryMax}`;
},
change(e) {
const salayData = e.detail.value;
if (leftIndex !== salayData[0]) {
const copyri = JSON.parse(JSON.stringify(salay));
const [lf, ri] = e.detail.value;
const risalay = copyri.slice(lf, copyri.length);
this.setColunm(1, risalay);
leftIndex = salayData[0];
}
},
});
}
function changeJobs() {
selectJobsModel.value?.open({
title: '添加岗位',
success: (ids, labels) => {
fromValue.jobTitleId = ids;
state.jobsText = labels.split(',');
},
});
}
// 技能等级选择
function 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() {
// 统一必填与格式校验
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;
}
// 检查所有错误状态
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;
}
// 获取职位
function getTreeselect() {
$api.createRequest('/app/common/jobTitle/treeselect', {}, 'GET').then((resData) => {
state.station = resData.data;
});
}
// 登录
function loginTest() {
// uni.share({
// provider: 'weixin',
// scene: 'WXSceneSession',
// type: 2,
// imageUrl: 'https://qiniu-web-assets.dcloud.net.cn/unidoc/zh/uni@2x.png',
// success: function (res) {
// console.log('success:' + JSON.stringify(res));
// },
// fail: function (err) {
// console.log('fail:' + JSON.stringify(err));
// },
// });
const params = {
username: 'test',
password: 'test',
};
$api.createRequest('/app/login', params, 'post').then((resData) => {
$api.msg('模拟帐号密码测试登录成功');
loginSetToken(resData.token).then((resume) => {
if (resume.data.jobTitleId) {
// 设置推荐列表,每次退出登录都需要更新
useUserStore().initSeesionId();
uni.reLaunch({
url: '/pages/index/index',
});
} else {
nextStep();
}
});
});
}
function complete() {
const result = IdCardValidator.validate(fromValue.idCard);
if (result.valid) {
// 构建 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('/registerUser', requestData, 'post').then(async (resData) => {
$api.msg('完成');
// 如果接口返回了token需要重新保存token
if (resData.token) {
try {
await loginSetToken(resData.token);
console.log('Token已更新:', resData.token);
} catch (error) {
console.error('更新Token失败:', error);
}
}
// 保存成功后,重新获取用户信息并更新缓存
try {
await getUserResume();
console.log('用户信息已更新到缓存');
} catch (error) {
console.error('获取用户信息失败:', error);
// 即使获取用户信息失败,也不影响页面跳转
}
// 跳转到首页
uni.reLaunch({
url: '/pages/index/index',
});
});
} else {
$api.msg('身份证校验失败');
console.log('验证失败:', result.message);
}
}
</script>
<style lang="stylus" scoped>
.tab-container
height: 100%
width: 100%
display: flex
align-items: center
justify-content: center
flex-direction: row
.uni-margin-wrap
width: 100%
height: 100%
.swiper
width: 100%
height: 100%
.swiper-item
display: block;
width: 100%
height: 100%
.input-nx
position: relative
border-bottom: 2rpx solid #EBEBEB
padding-bottom: 30rpx
display: flex
flex-wrap: wrap
.nx-item
padding: 16rpx 24rpx
width: fit-content
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%;
height: calc(100vh - var(--window-top) - var(--status-bar-height) - var(--window-bottom));
position: fixed;
background: url('@/static/icon/background2.png') 0 0 no-repeat;
background-size: 100% 728rpx;
display: flex;
flex-direction: column
.container-hader
height: 88rpx;
text-align: center;
line-height: 88rpx;
color: #000000;
font-weight: bold
font-size: 32rpx
.login-content
position: absolute;
left: 50%;
top: 40%;
transform: translate(-50%, -50%);
display: flex;
align-items: flex-end;
flex-wrap: nowrap;
.logo
width: 266rpx;
height: 182rpx;
.logo-title
font-size: 88rpx;
color: #22c984;
width: 180rpx;
.btns
position: absolute;
top: 70%;
left: 50%;
transform: translate(-50%, 0)
.wxlogin
width: 562rpx;
height: 140rpx;
border-radius: 70rpx;
background-color: #13C57C;
color: #FFFFFF;
text-align: center;
line-height: 140rpx;
font-size: 70rpx;
.wxaddress
color: #BBBBBB;
margin-top: 70rpx;
text-align: center;
.content-one
padding: 60rpx 28rpx;
display: flex;
flex-direction: column;
justify-content: space-between
height: calc(100% - 120rpx)
.content-title
display: flex
justify-content: space-between;
align-items: center
margin-bottom: 70rpx
.title-lf
font-size: 44rpx;
color: #000000;
font-weight: 600;
.lf-text
font-weight: 400;
font-size: 28rpx;
color: #6C7282;
.title-ri
font-size: 36rpx;
color: #000000;
font-weight: 600;
.content-input
margin-bottom: 52rpx
.input-titile
font-weight: 400;
font-size: 28rpx;
color: #6A6A6A;
.input-con2
font-weight: 400;
font-size: 32rpx;
color: #333333;
line-height: 80rpx;
height: 80rpx;
border-bottom: 2rpx solid #EBEBEB
.input-con
font-weight: 400;
font-size: 32rpx;
color: #333333;
line-height: 80rpx;
height: 80rpx;
border-bottom: 2rpx solid #EBEBEB
position: relative;
.input-con::before
position: absolute;
right: 20rpx;
top: calc(50% - 2rpx);
content: '';
width: 4rpx;
height: 18rpx;
border-radius: 2rpx
background: #697279;
transform: translate(0, -50%) rotate(-45deg) ;
.input-con::after
position: absolute;
right: 20rpx;
top: 50%;
content: '';
width: 4rpx;
height: 18rpx;
border-radius: 2rpx
background: #697279;
transform: rotate(45deg)
.error-message
color: #ff4757;
font-size: 24rpx;
margin-top: 10rpx;
line-height: 1.4;
.success-message
color: #2ed573;
font-size: 24rpx;
margin-top: 10rpx;
line-height: 1.4;
.input-error
.input-con2
border-bottom-color: #ff4757;
.content-sex
height: 110rpx;
display: flex
justify-content: space-between;
align-items: flex-start;
border-bottom: 2rpx solid #EBEBEB
margin-bottom: 52rpx
.sex-titile
line-height: 80rpx;
.sext-ri
display: flex
align-items: center;
.sext-box
height: 76rpx;
width: 152rpx;
text-align: center;
line-height: 80rpx;
border-radius: 12rpx 12rpx 12rpx 12rpx
border: 2rpx solid #E8EAEE;
margin-left: 28rpx
font-weight: 400;
font-size: 28rpx;
.sext-boxactive
color: #256BFA
background: rgba(37,107,250,0.1);
border: 2rpx solid #256BFA;
.next-btn
width: 100%;
height: 90rpx;
background: #256BFA;
border-radius: 12rpx 12rpx 12rpx 12rpx;
font-weight: 500;
font-size: 32rpx;
color: #FFFFFF;
text-align: center;
line-height: 90rpx
</style>

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -6,23 +6,29 @@
<IndexOne @onShowTabbar="changeShowTabbar" />
</view>
<Tabbar :currentpage="0"></Tabbar>
<!-- 自定义tabbar -->
<CustomTabBar :currentPage="0" />
</view>
</view>
</template>
<script setup>
import { reactive, inject, watch, ref, onMounted } from 'vue';
import Tabbar from '@/components/tabbar/midell-box.vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import IndexOne from './components/index-one.vue';
// import IndexTwo from './components/index-two.vue';
import { storeToRefs } from 'pinia';
import { useReadMsg } from '@/stores/useReadMsg';
import { tabbarManager } from '@/utils/tabbarManager';
const { unreadCount } = storeToRefs(useReadMsg());
onLoad(() => {
useReadMsg().fetchMessages();
// useReadMsg().fetchMessages();
});
onShow(() => {
// 更新自定义tabbar选中状态
tabbarManager.updateSelected(0);
});
</script>

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

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

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

@@ -0,0 +1,989 @@
<template>
<view class="publish-job-page">
<!-- 头部导航 -->
<view class="header">
<view class="header-title">发布岗位</view>
</view>
<!-- 主要内容 -->
<scroll-view class="content" scroll-y="true" :style="{ height: scrollViewHeight }" :scroll-with-animation="true">
<!-- 基本信息区块 -->
<view class="form-block">
<view class="form-group" v-if="userType === 0">
<view class="label">招聘公司</view>
<view-text style="color: #333;font-size: 38rpx;padding: 16rpx 0;display: inline-block;">{{formData.companyName}}</view-text>
</view>
<view class="form-group">
<view class="label">岗位名称</view>
<input
class="input"
placeholder="请输入岗位名称"
v-model="formData.jobTitle"
/>
</view>
<view class="form-group" v-if="userType === 2">
<view class="label">招聘公司</view>
<!-- 企业用户显示缓存中的企业名称不可编辑 -->
<view
v-if="userType === 1"
class="company-display"
>
<view class="company-name">{{ formData.companyName || '未设置企业名称' }}</view>
<view class="readonly-tip">企业用户不可修改</view>
</view>
<!-- 网格员点击跳转搜索 -->
<view
v-else
class="company-selector"
@click="goToCompanySearch"
>
<view class="selector-text" :class="{ 'placeholder': !formData.companyName }">
{{ formData.companyName || '请选择企业' }}
</view>
<view class="selector-arrow">
<view class="arrow-icon"></view>
</view>
</view>
</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>
<input
class="input"
placeholder="请输入最大薪资"
type="number"
v-model="formData.maxSalary"
/>
</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" data-placeholder="请选择学历要求">{{ selectedEducation || '请选择学历要求' }}</view>
</picker>
</view>
<view class="form-group">
<view class="label">工作经验</view>
<picker
mode="selector"
:range="experienceLevels"
range-key="label"
@change="onExperienceChange"
class="picker"
>
<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">
<view class="label">招聘人数</view>
<input
class="input"
placeholder="请输入招聘人数"
type="number"
v-model="formData.vacancies"
/>
</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="form-block">
<view class="section-title">岗位描述</view>
<view class="form-group">
<textarea
class="textarea"
placeholder="请详细描述岗位职责和工作内容"
v-model="formData.description"
></textarea>
</view>
</view>
<!-- 任职要求区块 -->
<view class="form-block">
<view class="section-title">任职要求</view>
<view class="form-group">
<textarea
class="textarea"
placeholder="请描述对候选人的具体要求"
v-model="formData.jobRequirements"
></textarea>
</view>
</view>
<!-- 联系方式区块 -->
<view class="form-block">
<view class="section-title">联系方式</view>
<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="add-contact-btn" v-if="formData.contacts.length < 3" @click="addContact">
<view class="add-icon">+</view>
<view class="add-text">添加联系人</view>
</view>
</view>
<!-- 底部安全区域 -->
<view class="bottom-safe-area"></view>
</scroll-view>
<!-- 底部操作按钮 -->
<view class="footer">
<view class="btn-group">
<button class="btn btn-publish" @click="publishJob">发布岗位</button>
</view>
</view>
<!-- 自定义tabbar -->
<CustomTabBar :currentPage="1" />
</view>
</template>
<script setup>
import { ref, reactive, onMounted, onUnmounted } from 'vue';
import { onShow } from '@dcloudio/uni-app';
import { storeToRefs } from 'pinia';
import { createRequest } from '@/utils/request';
import useDictStore from '@/stores/useDictStore';
import useUserStore from '@/stores/useUserStore';
const userStore = useUserStore();
const cachedUserInfo = uni.getStorageSync('userInfo') || {};
// 表单数据
const formData = reactive({
jobTitle: '',
companyName: '',
minSalary: '',
maxSalary: '',
vacancies: '', // 招聘人数
description: '', // 对应接口字段 description
jobRequirements: '',
jobCategory: '', // 新增:岗位分类
companyId: '', // 新增企业id
latitude: '', // 新增:纬度
longitude: '', // 新增:经度
jobLocation: '', // 新增:工作地点
jobLocationAreaCode: '', // 新增:工作地点区县字典代码
education: '', // 新增:学历要求字典值
experience: '', // 新增:工作经验字典值
contacts: [
{
name: '',
phone: ''
}
]
});
// 字典存储
const dictStore = useDictStore();
// 选择器数据
const educationLevels = ref([]);
const experienceLevels = ref([]);
const workDistricts = ref([]);
const workLocations = ref([]);
const jobCategories = 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 tabbarHeight = 88; // 自定义tabbar高度
const extraPadding = 50; // 额外间距,确保内容不被遮挡
const scrollHeight = windowHeight - headerHeight - footerHeight - tabbarHeight - extraPadding;
scrollViewHeight.value = `${scrollHeight}px`;
};
const userType = ref(Number(userStore.userInfo.isCompanyUser));
console.log('完整userInfo对象:---223--', userType.value);
// 页面加载时计算高度和初始化数据
onMounted(async () => {
console.log('完整userInfo对象:-----', );
console.log('缓存中的userInfo:----', cachedUserInfo);
calculateScrollViewHeight();
await initFormData();
// 获取企业信息
getCompanyInfo();
// 监听企业选择事件
uni.$on('companySelected', handleCompanySelected);
});
// 页面卸载时移除事件监听
onUnmounted(() => {
uni.$off('companySelected', handleCompanySelected);
});
// 页面显示时获取最新的企业信息(参考首页方法)
onShow(() => {
getCompanyInfo();
});
// 获取企业信息(参考首页方法)
const getCompanyInfo = () => {
try {
const cachedUserInfo = uni.getStorageSync('userInfo') || {};
console.log('缓存中的userInfo:', cachedUserInfo);
// 检查是否有company字段
if (cachedUserInfo.company) {
const company = cachedUserInfo.company;
formData.companyName = company.name || '';
formData.companyId = company.companyId || '';
console.log('从缓存获取企业名称:', company.name);
console.log('从缓存获取企业ID:', company.id);
} else {
console.log('缓存中没有company字段');
// 如果company字段不存在尝试从companyName字段获取兼容旧数据
if (cachedUserInfo.companyName) {
formData.companyName = cachedUserInfo.companyName;
console.log('从companyName字段获取企业名称:', cachedUserInfo.companyName);
}
// 尝试从用户信息中获取companyId
if (cachedUserInfo.id) {
formData.companyId = cachedUserInfo.id;
console.log('从用户ID获取企业ID:', cachedUserInfo.id);
}
}
} catch (error) {
console.error('获取企业信息失败:', error);
}
};
// 初始化表单数据
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;
console.log('从store获取企业ID:', userStore.userInfo.id);
}
// 如果store中没有获取到尝试从缓存获取
if (!formData.companyId) {
const cachedUserInfo = uni.getStorageSync('userInfo') || {};
if (cachedUserInfo.company && cachedUserInfo.company.id) {
formData.companyId = cachedUserInfo.company.id;
console.log('从缓存company字段获取企业ID:', cachedUserInfo.company.id);
} else if (cachedUserInfo.id) {
formData.companyId = cachedUserInfo.id;
console.log('从缓存用户ID获取企业ID:', cachedUserInfo.id);
}
}
// 如果是企业用户,获取企业信息
if (userType.value === 1) {
getCompanyInfo();
}
} catch (error) {
console.error('初始化表单数据失败:', error);
uni.showToast({
title: '数据加载失败',
icon: 'none'
});
}
};
// 选择器事件处理
const onEducationChange = (e) => {
const index = e.detail.value;
const selectedItem = educationLevels.value[index];
selectedEducation.value = selectedItem.label;
formData.education = selectedItem.value;
};
const onExperienceChange = (e) => {
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);
}
};
// 跳转到企业搜索页面
const goToCompanySearch = () => {
uni.navigateTo({
url: '/pages/job/companySearch'
});
};
// 处理企业选择
const handleCompanySelected = (company) => {
formData.companyName = company.name;
formData.companyId = company.id;
};
// 发布岗位
const publishJob = async () => {
// 表单验证
if (!validateForm()) {
return;
}
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,
vacancies: formData.vacancies, // 招聘人数
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())
};
// 调试信息打印companyId
console.log('发布岗位时的companyId:', formData.companyId);
console.log('发布岗位时的companyName:', formData.companyName);
console.log('完整的请求数据:', requestData);
// 调用发布接口
const response = await createRequest('/app/job/publishJob', requestData, 'POST', false);
uni.hideLoading();
if (response.code === 200) {
uni.showToast({
title: '发布成功',
icon: 'success'
});
uni.redirectTo({
url: '/pages/index/index'
});
} 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: 'vacancies', 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 {
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-title {
font-size: 36rpx;
font-weight: 600;
color: #333;
}
.header-right {
width: 60rpx;
}
}
.content {
flex: 1;
padding: 0;
overflow: hidden;
}
.form-block {
background: #fff;
margin-bottom: 20rpx;
width: 100%;
position: relative;
padding-bottom: 10rpx;
&:last-child {
margin-bottom: 0;
}
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 30rpx;
padding: 30rpx 30rpx 20rpx 30rpx;
border-bottom: 2rpx solid #f0f0f0;
}
}
.form-group {
margin-bottom: 0;
padding: 0 30rpx;
border-bottom: 2rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
padding-bottom: 30rpx;
}
.label {
font-size: 28rpx;
color: #000;
margin-bottom: 15rpx;
font-weight: 400;
padding-top: 30rpx;
}
.input {
width: 100%;
height: 80rpx;
background: #fff;
border: none;
border-radius: 0;
padding: 0 0 20rpx 0;
font-size: 28rpx;
color: #333;
position: relative;
z-index: 1;
box-sizing: border-box;
line-height: 1.4;
display: flex;
align-items: center;
&::placeholder {
color: #999;
font-size: 28rpx;
line-height: 1.4;
}
&:focus {
background: #fff;
transform: translateZ(0);
-webkit-transform: translateZ(0);
}
}
.textarea {
width: 100%;
min-height: 120rpx;
background: #fff;
border: none;
border-radius: 0;
padding: 0 0 20rpx 0;
font-size: 28rpx;
color: #333;
position: relative;
z-index: 1;
box-sizing: border-box;
line-height: 1.4;
&::placeholder {
color: #999;
font-size: 28rpx;
line-height: 1.4;
}
&:focus {
background: #fff;
transform: translateZ(0);
-webkit-transform: translateZ(0);
}
}
.picker {
width: 100%;
height: 80rpx;
background: #fff;
border: none;
border-radius: 0;
display: flex;
align-items: center;
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;
}
}
}
// 企业选择器样式
.company-selector {
display: flex;
align-items: center;
justify-content: space-between;
height: 80rpx;
background: #fff;
border: none;
border-radius: 0;
padding: 0 0 20rpx 0;
box-sizing: border-box;
.selector-text {
font-size: 28rpx;
color: #333;
&.placeholder {
color: #999;
font-size: 24rpx;
}
}
.selector-arrow {
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
.arrow-icon {
font-size: 24rpx;
color: #999;
}
}
&:active {
background: #f8f8f8;
}
}
// 企业名称显示样式
.company-display {
display: flex;
flex-direction: column;
padding: 0 0 20rpx 0;
box-sizing: border-box;
.company-name {
font-size: 28rpx;
color: #333;
font-weight: 500;
margin-bottom: 8rpx;
}
.readonly-tip {
font-size: 24rpx;
color: #999;
font-style: italic;
}
}
// 联系人管理样式
.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;
line-height: 1.4;
&::placeholder {
font-size: 26rpx;
line-height: 1.4;
}
}
}
}
}
.add-contact-btn {
display: flex;
align-items: center;
justify-content: center;
margin: 20rpx 30rpx 30rpx 30rpx;
padding: 20rpx;
background: #fff;
border: 2rpx dashed #ddd;
border-radius: 12rpx;
.add-icon {
font-size: 32rpx;
color: #256BFA;
margin-right: 10rpx;
font-weight: bold;
}
.add-text {
font-size: 28rpx;
color: #256BFA;
font-weight: 500;
}
&:active {
background: #e3f2fd;
border-color: #256BFA;
}
}
.bottom-safe-area {
height: 250rpx;
background: transparent;
}
.footer {
position: fixed;
bottom: 140rpx;
left: 0;
right: 0;
background: #fff;
padding: 25rpx 30rpx;
border-top: 1rpx solid #eee;
z-index: 100;
.btn-group {
display: flex;
.btn {
flex: 1;
height: 80rpx;
border-radius: 12rpx;
font-size: 32rpx;
font-weight: 500;
border: none;
&.btn-publish {
background: #256BFA;
color: #fff;
}
}
}
}
/* 防止键盘弹出时页面偏移 */
/* #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,75 +0,0 @@
<template>
<view class="tab-container">
<view class="uni-margin-wrap">
<swiper
class="swiper"
:current="current"
:circular="false"
:indicator-dots="false"
:autoplay="false"
:duration="500"
>
<swiper-item @touchmove.stop="false">
<slot name="tab0"></slot>
</swiper-item>
<swiper-item @touchmove.stop="false">
<slot name="tab1"></slot>
</swiper-item>
<swiper-item @touchmove.stop="false">
<slot name="tab2"></slot>
</swiper-item>
<swiper-item @touchmove.stop="false">
<slot name="tab3"></slot>
</swiper-item>
<swiper-item @touchmove.stop="false">
<slot name="tab3"></slot>
</swiper-item>
<swiper-item @touchmove.stop="false">
<slot name="tab4"></slot>
</swiper-item>
<swiper-item @touchmove.stop="false">
<slot name="tab5"></slot>
</swiper-item>
<swiper-item @touchmove.stop="false">
<slot name="tab6"></slot>
</swiper-item>
</swiper>
</view>
</view>
</template>
<script>
export default {
name: 'tab',
data() {
return {};
},
props: {
current: {
type: Number,
default: 0,
},
},
};
</script>
<style lang="stylus" scoped>
.tab-container
// flex: 1
height: 100%
width: 100%
display: flex
align-items: center
justify-content: center
flex-direction: row
.uni-margin-wrap
width: 100%
height: 100%
.swiper
width: 100%
height: 100%
.swiper-item
display: block;
width: 100%
height: 100%
</style>

View File

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

363
pages/mine/company-info.vue Normal file
View File

@@ -0,0 +1,363 @@
<template>
<AppLayout back-gorund-color="#F4F4F4">
<!-- 编辑头像 -->
<view class="avatar-section btn-feel" @click="editAvatar">
<view class="avatar-label">编辑信息</view>
<view class="avatar-container">
<image class="company-avatar" :src="companyInfo.avatar || '/static/imgs/avatar.jpg'"></image>
<uni-icons color="#A2A2A2" type="right" size="16"></uni-icons>
</view>
</view>
<!-- 企业详细信息 -->
<view class="info-section">
<view class="info-item btn-feel" @click="editInfo('name')">
<view class="info-label">企业名称</view>
<view class="info-content">
<text class="info-value">{{ companyInfo.name || '暂无公司名称' }}</text>
<uni-icons color="#A2A2A2" type="right" size="16"></uni-icons>
</view>
</view>
<view class="info-item btn-feel" @click="editInfo('code')">
<view class="info-label">统一社会代码</view>
<view class="info-content">
<text class="info-value">{{ companyInfo.socialCode || '暂无统一社会代码' }}</text>
<uni-icons color="#A2A2A2" type="right" size="16"></uni-icons>
</view>
</view>
<view class="info-item btn-feel" @click="editInfo('location')">
<view class="info-label">企业注册地点</view>
<view class="info-content">
<text class="info-value">{{ companyInfo.location || '暂无注册地点' }}</text>
<uni-icons color="#A2A2A2" type="right" size="16"></uni-icons>
</view>
</view>
<view class="info-item btn-feel" @click="editInfo('description')">
<view class="info-label">企业信息介绍</view>
<view class="info-content">
<text class="info-value">{{ companyInfo.description || '暂无企业介绍' }}</text>
<uni-icons color="#A2A2A2" type="right" size="16"></uni-icons>
</view>
</view>
<view class="info-item btn-feel" @click="editInfo('legalPerson')">
<view class="info-label">企业法人姓名</view>
<view class="info-content">
<text class="info-value">{{ companyInfo.legalPerson || '暂无法人信息' }}</text>
<uni-icons color="#A2A2A2" type="right" size="16"></uni-icons>
</view>
</view>
<!-- 企业联系人管理 -->
<view class="contact-management-section">
<view class="contact-section-header">
<view class="contact-section-title">企业联系人</view>
<view class="edit-contacts-btn btn-feel" @click="editContacts">
<uni-icons type="compose" size="16" color="#256BFA"></uni-icons>
<text class="edit-text">编辑联系人</text>
</view>
</view>
<!-- 联系人列表展示 -->
<view v-for="(contact, index) in companyInfo.companyContactList" :key="contact.id || index" class="contact-display-item">
<view class="contact-display-title">联系人{{ index + 1 }}</view>
<view class="contact-display-content">
<view class="contact-field">
<text class="field-label">姓名</text>
<text class="field-value">{{ contact.contactPerson || '暂无' }}</text>
</view>
<view class="contact-field">
<text class="field-label">电话</text>
<text class="field-value">{{ contact.contactPersonPhone || '暂无' }}</text>
</view>
</view>
</view>
<!-- 如果没有联系人显示提示 -->
<view v-if="!companyInfo.companyContactList || companyInfo.companyContactList.length === 0" class="no-contacts">
<text class="no-contacts-text">暂无联系人信息</text>
</view>
</view>
</view>
</AppLayout>
</template>
<script setup>
import { reactive, inject, onMounted } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import AppLayout from '@/components/AppLayout/AppLayout.vue';
const { $api, navTo } = inject('globalFunction');
// 企业信息数据
const companyInfo = reactive({
name: '',
avatar: '/static/imgs/avatar.jpg',
completeness: '65%',
socialCode: '',
location: '',
description: '',
legalPerson: '',
companyContactList: [], // 企业联系人列表
isVerified: false // 实名状态
});
function editAvatar() {
// 编辑头像逻辑
// uni.chooseImage({
// count: 1,
// success: (res) => {
// // 上传头像
// uploadAvatar(res.tempFilePaths[0]);
// }
// });
}
function uploadAvatar(filePath) {
// 上传头像到服务器
uni.uploadFile({
url: '/api/upload/avatar',
filePath: filePath,
name: 'avatar',
success: (res) => {
const data = JSON.parse(res.data);
if (data.success) {
companyInfo.avatar = data.data.url;
uni.showToast({
title: '头像更新成功',
icon: 'success'
});
}
}
});
}
function editInfo(type) {
// 根据类型跳转到不同的编辑页面
const editPages = {
name: '/pages/mine/edit-company-name',
code: '/pages/mine/edit-company-code',
location: '/pages/mine/edit-company-location',
description: '/pages/mine/edit-company-description',
legalPerson: '/pages/mine/edit-legal-person'
};
if (editPages[type]) {
navTo(editPages[type]);
}
}
function editContacts() {
// 跳转到联系人编辑页面
navTo('/pages/mine/edit-company-contacts');
}
onShow(() => {
// 获取企业信息
getCompanyInfo();
});
// 从缓存获取公司信息
function getCompanyInfo() {
try {
const cachedUserInfo = uni.getStorageSync('userInfo') || {};
console.log('缓存中的userInfo:', cachedUserInfo);
// 检查是否有company字段
if (cachedUserInfo.company) {
const company = cachedUserInfo.company;
// 基本信息
companyInfo.name = company.name || '';
companyInfo.socialCode = company.code || '';
companyInfo.location = company.registeredAddress || '';
companyInfo.description = company.description || '';
companyInfo.legalPerson = company.legalPerson || '';
// 联系人信息 - 直接使用companyContactList数组
companyInfo.companyContactList = company.companyContactList || [];
// 判断是否实名legalIdCard字段有值则表示已实名
companyInfo.isVerified = !!(company.legalIdCard && company.legalIdCard.trim());
console.log('公司名称:', companyInfo.name);
console.log('实名状态:', companyInfo.isVerified);
console.log('legalIdCard值:', company.legalIdCard);
} else {
console.log('缓存中没有company字段');
// 保持默认值
}
} catch (error) {
console.error('获取公司信息失败:', error);
// 保持默认值
}
}
function goBack() {
uni.navigateBack();
}
</script>
<style lang="stylus" scoped>
.avatar-section {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
background: #FFFFFF;
margin: 20rpx;
border-radius: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
.avatar-label {
font-size: 28rpx;
color: #333333;
}
.avatar-container {
display: flex;
align-items: center;
.company-avatar {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
margin-right: 16rpx;
}
}
}
.info-section {
background: #FFFFFF;
margin: 0 20rpx 40rpx;
border-radius: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
.info-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx;
border-bottom: 1rpx solid #F5F5F5;
&:last-child {
border-bottom: none;
}
.info-label {
font-size: 28rpx;
color: #6C7282;
min-width: 200rpx;
}
.info-content {
display: flex;
align-items: center;
flex: 1;
.info-value {
font-size: 28rpx;
color: #333333;
flex: 1;
text-align: right;
margin-right: 16rpx;
word-break: break-all;
}
}
}
}
.btn-feel {
transition: transform 0.2s ease;
&:active {
transform: scale(0.98);
}
}
.contact-management-section {
border-top: 20rpx solid #F4F4F4;
.contact-section-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 20rpx 30rpx;
background: #F8F8F8;
border-bottom: 1rpx solid #F5F5F5;
.contact-section-title {
font-size: 24rpx;
color: #999999;
}
.edit-contacts-btn {
display: flex;
align-items: center;
padding: 8rpx 16rpx;
background: #E6F7FF;
border-radius: 8rpx;
.edit-text {
font-size: 24rpx;
color: #256BFA;
margin-left: 8rpx;
}
}
}
.contact-display-item {
padding: 20rpx 30rpx;
border-bottom: 1rpx solid #F5F5F5;
&:last-child {
border-bottom: none;
}
.contact-display-title {
font-size: 26rpx;
font-weight: 600;
color: #333333;
margin-bottom: 16rpx;
}
.contact-display-content {
.contact-field {
display: flex;
align-items: center;
margin-bottom: 12rpx;
&:last-child {
margin-bottom: 0;
}
.field-label {
font-size: 26rpx;
color: #6C7282;
min-width: 120rpx;
}
.field-value {
font-size: 26rpx;
color: #333333;
flex: 1;
}
}
}
}
.no-contacts {
padding: 40rpx 30rpx;
text-align: center;
.no-contacts-text {
font-size: 26rpx;
color: #999999;
}
}
}
</style>

285
pages/mine/company-mine.vue Normal file
View File

@@ -0,0 +1,285 @@
<template>
<AppLayout back-gorund-color="#F4F4F4">
<!-- 自定义tabbar -->
<CustomTabBar :currentPage="4" />
<!-- 企业信息卡片 -->
<view class="company-info-card btn-feel" @click="goToCompanyInfo">
<view class="company-avatar">
<image class="company-avatar-img" :src="companyInfo.avatar || '/static/imgs/avatar.jpg'"></image>
</view>
<view class="company-details">
<view class="company-name">{{ companyInfo.name || '暂无公司名称' }}</view>
<view class="company-completeness">
信息完整度 {{ companyInfo.completeness || '100%' }}
<text class="verification-status" :class="{ 'verified': companyInfo.isVerified, 'unverified': !companyInfo.isVerified }">
{{ companyInfo.isVerified ? '已实名' : '未实名' }}
</text>
</view>
</view>
<view class="company-arrow">
<uni-icons color="#A2A2A2" type="right" size="16"></uni-icons>
</view>
</view>
<!-- 服务专区 -->
<view class="service-zone-card">
<view class="service-title">服务专区</view>
<view class="service-item btn-feel">
<view class="service-left">
<uni-icons type="contact" size="20" color="#256BFA"></uni-icons>
<text class="service-text">实名认证</text>
</view>
<view class="service-status" :class="{ 'verified': companyInfo.isVerified, 'unverified': !companyInfo.isVerified }">
{{ companyInfo.isVerified ? '已通过' : '未认证' }}
</view>
</view>
<view class="service-item btn-feel">
<view class="service-left">
<uni-icons type="notification" size="20" color="#256BFA"></uni-icons>
<text class="service-text">通知与提醒</text>
</view>
<view class="service-status">已开启</view>
</view>
</view>
<!-- 退出登录按钮 -->
<view class="logout-btn btn-feel" @click="logOut">
退出登录
</view>
<!-- 退出确认弹窗 -->
<uni-popup ref="popup" type="dialog">
<uni-popup-dialog
mode="base"
title="确定退出登录吗?"
type="info"
:duration="2000"
:before-close="true"
@confirm="confirm"
@close="close"
></uni-popup-dialog>
</uni-popup>
</AppLayout>
</template>
<script setup>
import { reactive, inject, ref, onMounted, onUnmounted } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import useUserStore from '@/stores/useUserStore';
const { $api, navTo } = inject('globalFunction');
const popup = ref(null);
// 企业信息数据
const companyInfo = reactive({
name: '',
avatar: '/static/imgs/avatar.jpg',
completeness: '100%',
isVerified: false // 实名状态
});
function goToCompanyInfo() {
navTo('/pages/mine/company-info');
}
function logOut() {
popup.value.open();
}
function close() {
popup.value.close();
}
function confirm() {
// 调用退出登录
useUserStore().logOut();
// 关闭弹窗
popup.value.close();
// 跳转到首页
uni.reLaunch({
url: '/pages/index/index'
});
}
onShow(() => {
// 获取企业信息
getCompanyInfo();
});
// 监听退出登录事件,显示微信登录弹窗
onMounted(() => {
uni.$on('showLoginModal', () => {
// 这里可以显示微信登录弹窗
// 由于这个页面没有 WxAuthLogin 组件,我们跳转到首页让首页处理
uni.reLaunch({
url: '/pages/index/index'
});
});
});
onUnmounted(() => {
uni.$off('showLoginModal');
});
// 从缓存获取公司信息
function getCompanyInfo() {
try {
const cachedUserInfo = uni.getStorageSync('userInfo') || {};
console.log('缓存中的userInfo:', cachedUserInfo);
// 检查是否有company字段
if (cachedUserInfo.company) {
companyInfo.name = cachedUserInfo.company.name || '';
// 判断是否实名legalIdCard字段有值则表示已实名
companyInfo.isVerified = !!(cachedUserInfo.company.legalIdCard && cachedUserInfo.company.legalIdCard.trim());
console.log('公司名称:', companyInfo.name);
console.log('实名状态:', companyInfo.isVerified);
console.log('legalIdCard值:', cachedUserInfo.company.legalIdCard);
} else {
console.log('缓存中没有company字段');
companyInfo.name = '';
companyInfo.isVerified = false;
}
} catch (error) {
console.error('获取公司信息失败:', error);
companyInfo.name = '';
companyInfo.isVerified = false;
}
}
</script>
<style lang="stylus" scoped>
.company-info-card {
display: flex;
align-items: center;
padding: 30rpx;
background: #FFFFFF;
margin: 20rpx;
border-radius: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
.company-avatar {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
overflow: hidden;
margin-right: 24rpx;
.company-avatar-img {
width: 100%;
height: 100%;
}
}
.company-details {
flex: 1;
.company-name {
font-size: 36rpx;
font-weight: 600;
color: #333333;
margin-bottom: 8rpx;
}
.company-completeness {
font-size: 28rpx;
color: #6C7282;
display: flex;
align-items: center;
gap: 16rpx;
.verification-status {
font-size: 24rpx;
padding: 4rpx 12rpx;
border-radius: 12rpx;
&.verified {
background-color: #E8F5E8;
color: #52C41A;
}
&.unverified {
background-color: #FFF2E8;
color: #FA8C16;
}
}
}
}
.company-arrow {
margin-left: 20rpx;
}
}
.service-zone-card {
background: #FFFFFF;
margin: 0 20rpx 20rpx;
border-radius: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
padding: 32rpx;
.service-title {
font-size: 32rpx;
font-weight: 600;
color: #000000;
margin-bottom: 32rpx;
}
.service-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 24rpx 0;
border-bottom: 1rpx solid #F5F5F5;
&:last-child {
border-bottom: none;
}
.service-left {
display: flex;
align-items: center;
.service-text {
font-size: 28rpx;
color: #333333;
margin-left: 16rpx;
}
}
.service-status {
font-size: 28rpx;
color: #6E6E6E;
&.verified {
color: #52C41A;
}
&.unverified {
color: #FA8C16;
}
}
}
}
.logout-btn {
height: 96rpx;
background: #FFFFFF;
margin: 0 20rpx 40rpx;
border-radius: 20rpx;
text-align: center;
line-height: 96rpx;
font-size: 28rpx;
color: #256BFA;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
}
.btn-feel {
transition: transform 0.2s ease;
&:active {
transform: scale(0.98);
}
}
</style>

View File

@@ -0,0 +1,470 @@
<template>
<AppLayout back-gorund-color="#F4F4F4">
<!-- 联系人列表 -->
<view class="contacts-section">
<view v-for="(contact, index) in contactList" :key="contact.tempId || contact.id" class="contact-item">
<view class="contact-header">
<text class="contact-title">联系人{{ index + 1 }}</text>
<view class="contact-actions" v-if="contactList.length > 1">
<view class="action-btn delete-btn" @click="deleteContact(index)">
<uni-icons type="trash" size="16" color="#FF4D4F"></uni-icons>
<text class="action-text">删除</text>
</view>
</view>
</view>
<view class="contact-form">
<view class="form-item">
<view class="form-label">联系人姓名</view>
<input
class="form-input"
v-model="contact.contactPerson"
placeholder="请输入联系人姓名"
@blur="validateContactName(index)"
/>
</view>
<view class="form-item">
<view class="form-label">联系电话</view>
<input
class="form-input"
v-model="contact.contactPersonPhone"
placeholder="请输入联系电话"
type="number"
@blur="validateContactPhone(index)"
/>
</view>
</view>
</view>
</view>
<!-- 添加联系人按钮 -->
<view class="add-contact-section" v-if="contactList.length < 3">
<view class="add-contact-btn btn-feel" @click="addContact">
<uni-icons type="plus" size="20" color="#256BFA"></uni-icons>
<text class="add-text">添加联系人</text>
</view>
</view>
<!-- 保存按钮 -->
<view class="save-section">
<view class="save-btn btn-feel" @click="saveContacts">
<text class="save-text">保存联系人</text>
</view>
</view>
<!-- 提示信息 -->
<view class="tips-section">
<view class="tips-title">温馨提示</view>
<view class="tips-content">
<text class="tips-item"> 至少需要保留一个联系人最多可添加3个联系人</text>
<text class="tips-item"> 联系人姓名和电话为必填项</text>
<text class="tips-item"> 联系电话请填写正确的手机号码</text>
</view>
</view>
</AppLayout>
</template>
<script setup>
import { reactive, inject, ref, onMounted } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import AppLayout from '@/components/AppLayout/AppLayout.vue';
import { createRequest } from '@/utils/request.js';
const { navTo } = inject('globalFunction');
// 联系人列表数据
const contactList = reactive([]);
let tempIdCounter = 0; // 用于生成临时ID
// 页面加载时获取联系人数据
onLoad((options) => {
// 从缓存获取企业联系人信息
loadContactsFromCache();
});
// 从缓存加载联系人数据
function loadContactsFromCache() {
try {
const cachedUserInfo = uni.getStorageSync('userInfo') || {};
if (cachedUserInfo.company && cachedUserInfo.company.companyContactList) {
const contacts = cachedUserInfo.company.companyContactList;
// 如果联系人列表为空,至少添加一个空联系人
if (contacts.length === 0) {
contactList.push(createEmptyContact());
} else {
// 为每个联系人添加临时ID如果没有ID的话
contacts.forEach(contact => {
if (!contact.tempId) {
contact.tempId = `temp_${++tempIdCounter}`;
}
contactList.push({ ...contact });
});
}
} else {
// 如果没有联系人数据,创建一个空联系人
contactList.push(createEmptyContact());
}
} catch (error) {
console.error('加载联系人数据失败:', error);
// 出错时至少创建一个空联系人
contactList.push(createEmptyContact());
}
}
// 创建空联系人
function createEmptyContact() {
return {
id: '',
contactPerson: '',
contactPersonPhone: '',
tempId: `temp_${++tempIdCounter}`
};
}
// 添加联系人
function addContact() {
if (contactList.length >= 3) {
uni.showToast({
title: '最多只能添加3个联系人',
icon: 'none'
});
return;
}
contactList.push(createEmptyContact());
}
// 删除联系人
function deleteContact(index) {
if (contactList.length <= 1) {
uni.showToast({
title: '至少需要保留一个联系人',
icon: 'none'
});
return;
}
uni.showModal({
title: '确认删除',
content: '确定要删除这个联系人吗?',
success: (res) => {
if (res.confirm) {
contactList.splice(index, 1);
}
}
});
}
// 验证联系人姓名
function validateContactName(index) {
const contact = contactList[index];
if (!contact.contactPerson || contact.contactPerson.trim() === '') {
uni.showToast({
title: '请输入联系人姓名',
icon: 'none'
});
return false;
}
return true;
}
// 验证联系人电话
function validateContactPhone(index) {
const contact = contactList[index];
if (!contact.contactPersonPhone || contact.contactPersonPhone.trim() === '') {
uni.showToast({
title: '请输入联系电话',
icon: 'none'
});
return false;
}
// 简单的手机号验证
const phoneRegex = /^1[3-9]\d{9}$/;
if (!phoneRegex.test(contact.contactPersonPhone)) {
uni.showToast({
title: '请输入正确的手机号码',
icon: 'none'
});
return false;
}
return true;
}
// 验证所有联系人
function validateAllContacts() {
for (let i = 0; i < contactList.length; i++) {
if (!validateContactName(i) || !validateContactPhone(i)) {
return false;
}
}
return true;
}
// 保存联系人
async function saveContacts() {
// 验证所有联系人信息
if (!validateAllContacts()) {
return;
}
try {
uni.showLoading({
title: '保存中...',
mask: true
});
// 获取companyId
const companyId = getCompanyIdFromCache();
if (!companyId) {
uni.showToast({
title: '获取企业信息失败,请重新登录',
icon: 'none'
});
return;
}
// 准备API数据移除临时ID添加companyId
const apiData = contactList.map(contact => ({
id: contact.id || '',
contactPerson: contact.contactPerson.trim(),
contactPersonPhone: contact.contactPersonPhone.trim(),
companyId: companyId
}));
// 调用API保存联系人
const response = await createRequest(
'/app/companycontact/batchInsertUpdate',
{
companyContactList: apiData
},
'POST',
false
);
if (response.code === 200) {
// 保存成功,更新本地缓存
updateLocalCache(apiData);
uni.showToast({
title: '保存成功',
icon: 'success'
});
// 延迟返回上一页
setTimeout(() => {
goBack();
}, 1500);
} else {
uni.showToast({
title: response.msg || '保存失败',
icon: 'none'
});
}
} catch (error) {
console.error('保存联系人失败:', error);
uni.showToast({
title: '保存失败,请重试',
icon: 'none'
});
} finally {
uni.hideLoading();
}
}
// 从缓存获取companyId
function getCompanyIdFromCache() {
try {
const cachedUserInfo = uni.getStorageSync('userInfo') || {};
if (cachedUserInfo.company && cachedUserInfo.company.companyId) {
return cachedUserInfo.company.companyId;
}
return null;
} catch (error) {
console.error('获取companyId失败:', error);
return null;
}
}
// 更新本地缓存
function updateLocalCache(contactData) {
try {
const cachedUserInfo = uni.getStorageSync('userInfo') || {};
if (cachedUserInfo.company) {
cachedUserInfo.company.companyContactList = contactData;
uni.setStorageSync('userInfo', cachedUserInfo);
}
} catch (error) {
console.error('更新本地缓存失败:', error);
}
}
// 返回上一页
function goBack() {
uni.navigateBack();
}
</script>
<style lang="stylus" scoped>
.save-section {
margin: 0 20rpx 20rpx;
.save-btn {
display: flex;
align-items: center;
justify-content: center;
height: 88rpx;
background: #256BFA;
border-radius: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(37, 107, 250, 0.3);
.save-text {
font-size: 32rpx;
color: #FFFFFF;
font-weight: 600;
}
}
}
.contacts-section {
margin: 20rpx;
.contact-item {
background: #FFFFFF;
border-radius: 20rpx;
margin-bottom: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
.contact-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 30rpx 30rpx 20rpx;
border-bottom: 1rpx solid #F5F5F5;
.contact-title {
font-size: 28rpx;
font-weight: 600;
color: #333333;
}
.contact-actions {
display: flex;
align-items: center;
.action-btn {
display: flex;
align-items: center;
padding: 8rpx 16rpx;
border-radius: 8rpx;
&.delete-btn {
background: #FFF2F0;
.action-text {
font-size: 24rpx;
color: #FF4D4F;
margin-left: 8rpx;
}
}
}
}
}
.contact-form {
padding: 20rpx 30rpx 30rpx;
.form-item {
margin-bottom: 30rpx;
&:last-child {
margin-bottom: 0;
}
.form-label {
font-size: 26rpx;
color: #6C7282;
margin-bottom: 16rpx;
}
.form-input {
width: 100%;
height: 80rpx;
background: #F8F9FA;
border: 1rpx solid #E9ECEF;
border-radius: 12rpx;
padding: 0 20rpx;
font-size: 28rpx;
color: #333333;
box-sizing: border-box;
&:focus {
border-color: #256BFA;
background: #FFFFFF;
}
}
}
}
}
}
.add-contact-section {
margin: 0 20rpx 20rpx;
.add-contact-btn {
display: flex;
align-items: center;
justify-content: center;
height: 88rpx;
background: #FFFFFF;
border: 2rpx dashed #256BFA;
border-radius: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
.add-text {
font-size: 28rpx;
color: #256BFA;
margin-left: 12rpx;
}
}
}
.tips-section {
margin: 0 20rpx 40rpx;
padding: 30rpx;
background: #F8F9FA;
border-radius: 20rpx;
.tips-title {
font-size: 26rpx;
font-weight: 600;
color: #333333;
margin-bottom: 20rpx;
}
.tips-content {
.tips-item {
display: block;
font-size: 24rpx;
color: #6C7282;
line-height: 1.6;
margin-bottom: 8rpx;
&:last-child {
margin-bottom: 0;
}
}
}
}
.btn-feel {
transition: transform 0.2s ease;
&:active {
transform: scale(0.98);
}
}
</style>

View File

@@ -1,6 +1,23 @@
<template>
<AppLayout title="我的" back-gorund-color="#F4F4F4">
<view class="mine-userinfo btn-feel" @click="seeDetail">
<!-- 自定义tabbar -->
<CustomTabBar :currentPage="4" />
<!-- 企业用户信息卡片 -->
<view v-if="userType === 0" class="company-info-card btn-feel" @click="seeDetail">
<view class="company-avatar">
<image class="company-avatar-img" :src="companyInfo.avatar || '/static/icon/company-default.png'"></image>
</view>
<view class="company-details">
<view class="company-name">{{ companyInfo.name || '科里喀什分公司' }}</view>
<view class="company-completeness">信息完整度 {{ companyInfo.completeness || '100%' }}</view>
</view>
<view class="company-arrow">
<uni-icons color="#A2A2A2" type="right" size="16"></uni-icons>
</view>
</view>
<!-- 求职者用户信息卡片 -->
<view v-else 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>
<image class="userindo-head-img" v-else src="/static/icon/girl.png"></image>
@@ -83,7 +100,8 @@
<view class="row-right">已开启</view>
</view>
</view>
<view class="card-back button-click" @click="logOut">退出登录</view>
<view v-if="userType === 2" class="card-help button-click" @click="goToJobHelper">求职帮</view>
<view class="card-back button-click" @click="logOut">退出登录</view>
<uni-popup ref="popup" type="dialog">
<uni-popup-dialog
mode="base"
@@ -97,26 +115,62 @@
</uni-popup>
</view>
<template #footer>
<Tabbar :currentpage="4"></Tabbar>
<!-- 统一使用系统tabBar -->
</template>
</AppLayout>
</template>
<script setup>
import { reactive, inject, watch, ref, onMounted } from 'vue';
import { reactive, inject, watch, ref, onMounted, onUnmounted, computed } from 'vue';
import { storeToRefs } from 'pinia';
import Tabbar from '@/components/tabbar/midell-box.vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
const { $api, navTo } = inject('globalFunction');
import useUserStore from '@/stores/useUserStore';
import { tabbarManager } from '@/utils/tabbarManager';
const popup = ref(null);
const { userInfo, Completion } = storeToRefs(useUserStore());
const counts = ref({});
// 获取用户类型,参考首页的实现方式
const userType = computed(() => {
// 优先从store获取如果为空则从缓存获取
const storeIsCompanyUser = userInfo.value?.isCompanyUser;
const cachedUserInfo = uni.getStorageSync('userInfo') || {};
const cachedIsCompanyUser = cachedUserInfo.isCompanyUser;
// 获取用户类型优先使用store中的isCompanyUser如果store中没有使用缓存中的isCompanyUser
// 缓存中的值可能是字符串,需要转换为数值类型
return storeIsCompanyUser !== undefined ? Number(storeIsCompanyUser) : Number(cachedIsCompanyUser);
});
// 企业信息数据
const companyInfo = reactive({
name: '科里喀什分公司',
avatar: '/static/icon/company-avatar.png',
completeness: '100%'
});
function logOut() {
popup.value.open();
}
onShow(() => {
getUserstatistics();
// 更新自定义tabbar选中状态
tabbarManager.updateSelected(4);
});
// 监听退出登录事件,显示微信登录弹窗
onMounted(() => {
uni.$on('showLoginModal', () => {
// 这里可以显示微信登录弹窗
// 由于这个页面没有 WxAuthLogin 组件,我们跳转到首页让首页处理
uni.reLaunch({
url: '/pages/index/index'
});
});
});
onUnmounted(() => {
uni.$off('showLoginModal');
});
function close() {
@@ -136,12 +190,19 @@ function getUserstatistics() {
});
}
function seeDetail() {
if (userInfo.isCompanyUser) {
navTo('/packageA/pages/myResume/corporateInformation');
if (userType === 0) {
// 企业用户跳转到企业信息页面
navTo('/pages/mine/company-info');
} else {
// 求职者用户跳转到简历页面
navTo('/packageA/pages/myResume/myResume');
}
}
function goToJobHelper() {
// 跳转到求职者信息补全页面
navTo('/pages/complete-info/complete-info');
}
</script>
<style lang="stylus" scoped>
@@ -241,6 +302,16 @@ function seeDetail() {
margin: 0
}
}
.card-help{
height: 96rpx;
background: #FFFFFF;
border-radius: 20rpx 20rpx 20rpx 20rpx;
text-align: center;
line-height: 96rpx;
font-size: 28rpx;
color: #256BFA;
margin-bottom: 20rpx;
}
.card-back{
height: 96rpx;
background: #FFFFFF;
@@ -322,4 +393,48 @@ function seeDetail() {
border-radius: 2rpx
background: #A2A2A2;
transform: rotate(45deg)
// 企业信息卡片样式
.company-info-card {
display: flex;
align-items: center;
padding: 30rpx;
background: #FFFFFF;
margin: 20rpx;
border-radius: 20rpx;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.08);
.company-avatar {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
overflow: hidden;
margin-right: 24rpx;
.company-avatar-img {
width: 100%;
height: 100%;
}
}
.company-details {
flex: 1;
.company-name {
font-size: 36rpx;
font-weight: 600;
color: #333333;
margin-bottom: 8rpx;
}
.company-completeness {
font-size: 28rpx;
color: #6C7282;
}
}
.company-arrow {
margin-left: 20rpx;
}
}
</style>

View File

@@ -21,34 +21,45 @@
<component :is="components[index]" :ref="(el) => handelComponentsRef(el, index)" />
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<ReadComponent v-show="currentIndex === 0" :ref="(el) => handelComponentsRef(el, index)" />
<UnreadComponent v-show="currentIndex === 1" :ref="(el) => handelComponentsRef(el, index)" />
<ReadComponent v-show="state.current === 0" :ref="(el) => handelComponentsRef(el, index)" />
<UnreadComponent v-show="state.current === 1" :ref="(el) => handelComponentsRef(el, index)" />
<!-- #endif -->
</swiper-item>
</swiper>
</view>
<Tabbar :currentpage="3"></Tabbar>
<!-- 自定义tabbar -->
<CustomTabBar :currentPage="3" />
<!-- 微信授权登录弹窗 -->
<WxAuthLogin ref="wxAuthLoginRef" @success="handleLoginSuccess"></WxAuthLogin>
<!-- 统一使用系统tabBar -->
</view>
</view>
</template>
<script setup>
import { reactive, inject, watch, ref, onMounted } from 'vue';
import { reactive, inject, watch, ref, onMounted, onUnmounted } from 'vue';
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';
import WxAuthLogin from '@/components/WxAuthLogin/WxAuthLogin.vue';
const loadedMap = reactive([false, false]);
const swiperRefs = [ref(null), ref(null)];
const components = [ReadComponent, UnreadComponent];
import { storeToRefs } from 'pinia';
import { useReadMsg } from '@/stores/useReadMsg';
const { unreadCount } = storeToRefs(useReadMsg());
const wxAuthLoginRef = ref(null);
onShow(() => {
// 获取消息列表
useReadMsg().fetchMessages();
// 更新自定义tabbar选中状态
tabbarManager.updateSelected(3);
});
const state = reactive({
current: 0,
@@ -57,8 +68,23 @@ const state = reactive({
onMounted(() => {
handleTabChange(state.current);
// 监听退出登录事件,显示微信登录弹窗
uni.$on('showLoginModal', () => {
wxAuthLoginRef.value?.open();
});
});
onUnmounted(() => {
uni.$off('showLoginModal');
});
// 登录成功回调
const handleLoginSuccess = () => {
console.log('登录成功');
// 可以在这里添加登录成功后的处理逻辑
};
const handelComponentsRef = (el, index) => {
if (el) {
swiperRefs[index].value = el;

View File

@@ -1,6 +1,7 @@
<template>
<scroll-view scroll-y class="main-scroll">
<view class="scrollmain">
<!-- 消息列表 -->
<view
class="list-card press-button"
v-for="(item, index) in msgList"
@@ -35,6 +36,13 @@
<view class="info-text line_2">{{ item.subTitle || '消息' }}</view>
</view>
</view>
<!-- 暂无消息提示 -->
<view class="empty-state" v-if="msgList.length === 0">
<image class="empty-icon" src="/static/icon/empty.png" mode="aspectFit"></image>
<text class="empty-text">暂无消息</text>
<text class="empty-desc">您还没有收到任何消息</text>
</view>
</view>
</scroll-view>
</template>
@@ -146,4 +154,26 @@ defineExpose({ loadData });
font-size: 28rpx;
color: #6C7282;
margin-top: 4rpx;
// 空状态样式
.empty-state
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 40rpx;
.empty-icon
width: 200rpx;
height: 200rpx;
margin-bottom: 40rpx;
opacity: 0.6;
.empty-text
font-size: 32rpx;
color: #999999;
font-weight: 500;
margin-bottom: 16rpx;
.empty-desc
font-size: 28rpx;
color: #CCCCCC;
font-weight: 400;
</style>

View File

@@ -1,6 +1,7 @@
<template>
<scroll-view scroll-y class="main-scroll">
<view class="scrollmain">
<!-- 未读消息列表 -->
<view
class="list-card press-button"
v-for="(item, index) in unreadMsgList"
@@ -33,6 +34,13 @@
<view class="info-text line_2">{{ item.subTitle || '消息' }}</view>
</view>
</view>
<!-- 暂无未读消息提示 -->
<view class="empty-state" v-if="unreadMsgList.length === 0">
<image class="empty-icon" src="/static/icon/empty.png" mode="aspectFit"></image>
<text class="empty-text">暂无未读消息</text>
<text class="empty-desc">您没有未读的消息</text>
</view>
</view>
</scroll-view>
</template>
@@ -132,4 +140,26 @@ defineExpose({ loadData });
font-size: 28rpx;
color: #6C7282;
margin-top: 4rpx;
// 空状态样式
.empty-state
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 120rpx 40rpx;
.empty-icon
width: 200rpx;
height: 200rpx;
margin-bottom: 40rpx;
opacity: 0.6;
.empty-text
font-size: 32rpx;
color: #999999;
font-weight: 500;
margin-bottom: 16rpx;
.empty-desc
font-size: 28rpx;
color: #CCCCCC;
font-weight: 400;
</style>

View File

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

View File

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

View File

@@ -0,0 +1,156 @@
<template>
<AppLayout title="企业我的页面测试" back-gorund-color="#F4F4F4">
<view class="test-container">
<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="switchToJobSeeker">切换到求职者</button>
</view>
<view class="current-type">
当前用户类型{{ getCurrentTypeLabel() }} ({{ currentUserType }})
</view>
</view>
<view class="test-section">
<view class="section-title">页面跳转测试</view>
<view class="button-group">
<button class="test-btn" @click="goToCompanyMine">企业我的页面</button>
<button class="test-btn" @click="goToCompanyInfo">企业信息页面</button>
<button class="test-btn" @click="goToMine">普通我的页面</button>
</view>
</view>
<view class="test-section">
<view class="section-title">用户信息显示</view>
<view class="info-display">
<text>用户类型{{ userInfo.isCompanyUser }}</text>
<text>用户名{{ userInfo.name || '未设置' }}</text>
</view>
</view>
</view>
</AppLayout>
</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 switchToCompany = () => {
userInfo.value.isCompanyUser = 0;
userInfo.value.name = '科里喀什分公司';
uni.setStorageSync('userInfo', userInfo.value);
uni.showToast({
title: '已切换到企业用户',
icon: 'success'
});
};
const switchToJobSeeker = () => {
userInfo.value.isCompanyUser = 1;
userInfo.value.name = '求职者用户';
uni.setStorageSync('userInfo', userInfo.value);
uni.showToast({
title: '已切换到求职者',
icon: 'success'
});
};
const getCurrentTypeLabel = () => {
const type = userTypes.find(t => t.value === currentUserType.value);
return type ? type.label : '未知';
};
const goToCompanyMine = () => {
uni.navigateTo({
url: '/pages/mine/company-mine'
});
};
const goToCompanyInfo = () => {
uni.navigateTo({
url: '/pages/mine/company-info'
});
};
const goToMine = () => {
uni.navigateTo({
url: '/pages/mine/mine'
});
};
</script>
<style lang="scss" scoped>
.test-container {
padding: 40rpx;
background: #f5f5f5;
min-height: 100vh;
}
.test-section {
background: #fff;
border-radius: 12rpx;
padding: 30rpx;
margin-bottom: 30rpx;
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.1);
}
.section-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 20rpx;
}
.button-group {
display: flex;
flex-direction: column;
gap: 20rpx;
margin-bottom: 20rpx;
}
.test-btn {
background: #256BFA;
color: #fff;
border: none;
border-radius: 8rpx;
padding: 20rpx;
font-size: 28rpx;
text-align: center;
}
.current-type {
font-size: 28rpx;
color: #666;
padding: 20rpx;
background: #f8f8f8;
border-radius: 8rpx;
}
.info-display {
display: flex;
flex-direction: column;
gap: 10rpx;
text {
font-size: 28rpx;
color: #333;
padding: 10rpx;
background: #f8f8f8;
border-radius: 4rpx;
}
}
</style>

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>

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

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

83
set-node-memory-limit.ps1 Normal file
View File

@@ -0,0 +1,83 @@
# PowerShell 脚本:设置 Node.js 内存限制环境变量
# 需要以管理员权限运行
Write-Host "================================================" -ForegroundColor Cyan
Write-Host "Node.js 内存限制设置工具" -ForegroundColor Cyan
Write-Host "================================================" -ForegroundColor Cyan
Write-Host ""
# 检查是否以管理员权限运行
$isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
if (-not $isAdmin) {
Write-Host "[警告] 设置系统环境变量需要管理员权限" -ForegroundColor Yellow
Write-Host "将仅为当前用户设置环境变量..." -ForegroundColor Yellow
Write-Host ""
}
# 内存大小选项
Write-Host "请选择内存限制大小:" -ForegroundColor Green
Write-Host "1. 4GB (4096 MB) - 推荐用于小型项目"
Write-Host "2. 8GB (8192 MB) - 推荐用于中型项目 [默认]"
Write-Host "3. 16GB (16384 MB) - 推荐用于大型项目"
Write-Host "4. 自定义大小"
Write-Host ""
$choice = Read-Host "请输入选项 (1-4直接回车使用默认值 8GB)"
$memorySize = 8192
switch ($choice) {
"1" { $memorySize = 4096 }
"2" { $memorySize = 8192 }
"3" { $memorySize = 16384 }
"4" {
$customSize = Read-Host "请输入自定义内存大小 (MB)"
if ($customSize -match '^\d+$') {
$memorySize = [int]$customSize
} else {
Write-Host "[错误] 无效的数字,使用默认值 8192 MB" -ForegroundColor Red
}
}
default { $memorySize = 8192 }
}
$nodeOptions = "--max-old-space-size=$memorySize"
Write-Host ""
Write-Host "将设置 NODE_OPTIONS = $nodeOptions" -ForegroundColor Green
Write-Host "内存限制:$memorySize MB ($($memorySize/1024) GB)" -ForegroundColor Green
Write-Host ""
# 设置环境变量
try {
if ($isAdmin) {
# 设置系统环境变量
[System.Environment]::SetEnvironmentVariable("NODE_OPTIONS", $nodeOptions, [System.EnvironmentVariableTarget]::Machine)
Write-Host "[成功] 已设置系统环境变量(对所有用户生效)" -ForegroundColor Green
} else {
# 设置用户环境变量
[System.Environment]::SetEnvironmentVariable("NODE_OPTIONS", $nodeOptions, [System.EnvironmentVariableTarget]::User)
Write-Host "[成功] 已设置用户环境变量(仅对当前用户生效)" -ForegroundColor Green
}
Write-Host ""
Write-Host "重要提示:" -ForegroundColor Yellow
Write-Host "1. 请重启 HBuilderX 使环境变量生效" -ForegroundColor Yellow
Write-Host "2. 如果问题仍然存在,请尝试增大内存限制" -ForegroundColor Yellow
Write-Host "3. 也可以尝试清理项目缓存(运行 → 清理项目缓存)" -ForegroundColor Yellow
Write-Host ""
} catch {
Write-Host "[错误] 设置环境变量失败:$($_.Exception.Message)" -ForegroundColor Red
Write-Host ""
Write-Host "手动设置方法:" -ForegroundColor Yellow
Write-Host "1. 右键'此电脑' → '属性' → '高级系统设置' → '环境变量'" -ForegroundColor Yellow
Write-Host "2. 在'用户变量'中新建变量:" -ForegroundColor Yellow
Write-Host " 变量名NODE_OPTIONS" -ForegroundColor Yellow
Write-Host " 变量值:$nodeOptions" -ForegroundColor Yellow
Write-Host ""
}
Write-Host "按任意键退出..."
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")

View File

@@ -0,0 +1,37 @@
@echo off
REM 设置 Node.js 内存限制为 8GB
REM 可以根据需要调整为 4096 (4GB)、8192 (8GB)、16384 (16GB) 等
echo ================================================
echo 正在启动 HBuilderX增加内存限制
echo 内存限制8GB
echo ================================================
REM 设置 Node.js 环境变量
set NODE_OPTIONS=--max-old-space-size=8192
REM 请修改下面的路径为您的 HBuilderX 安装路径
REM 例如C:\HBuilderX\HBuilderX.exe
set HBUILDERX_PATH=C:\HBuilderX\HBuilderX.exe
REM 检查路径是否存在
if not exist "%HBUILDERX_PATH%" (
echo.
echo [错误] 未找到 HBuilderX请修改脚本中的 HBUILDERX_PATH 变量
echo 当前路径:%HBUILDERX_PATH%
echo.
pause
exit
)
REM 启动 HBuilderX
echo 正在启动...
start "" "%HBUILDERX_PATH%"
echo.
echo HBuilderX 已启动!
echo 环境变量 NODE_OPTIONS 已设置为:%NODE_OPTIONS%
echo.
echo 提示:此窗口可以关闭
timeout /t 3

539
static/iconfont/demo.css Normal file
View File

@@ -0,0 +1,539 @@
/* Logo 字体 */
@font-face {
font-family: "iconfont logo";
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834');
src: url('https://at.alicdn.com/t/font_985780_km7mi63cihi.eot?t=1545807318834#iefix') format('embedded-opentype'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.woff?t=1545807318834') format('woff'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.ttf?t=1545807318834') format('truetype'),
url('https://at.alicdn.com/t/font_985780_km7mi63cihi.svg?t=1545807318834#iconfont') format('svg');
}
.logo {
font-family: "iconfont logo";
font-size: 160px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
/* tabs */
.nav-tabs {
position: relative;
}
.nav-tabs .nav-more {
position: absolute;
right: 0;
bottom: 0;
height: 42px;
line-height: 42px;
color: #666;
}
#tabs {
border-bottom: 1px solid #eee;
}
#tabs li {
cursor: pointer;
width: 100px;
height: 40px;
line-height: 40px;
text-align: center;
font-size: 16px;
border-bottom: 2px solid transparent;
position: relative;
z-index: 1;
margin-bottom: -1px;
color: #666;
}
#tabs .active {
border-bottom-color: #f00;
color: #222;
}
.tab-container .content {
display: none;
}
/* 页面布局 */
.main {
padding: 30px 100px;
width: 960px;
margin: 0 auto;
}
.main .logo {
color: #333;
text-align: left;
margin-bottom: 30px;
line-height: 1;
height: 110px;
margin-top: -50px;
overflow: hidden;
*zoom: 1;
}
.main .logo a {
font-size: 160px;
color: #333;
}
.helps {
margin-top: 40px;
}
.helps pre {
padding: 20px;
margin: 10px 0;
border: solid 1px #e7e1cd;
background-color: #fffdef;
overflow: auto;
}
.icon_lists {
width: 100% !important;
overflow: hidden;
*zoom: 1;
}
.icon_lists li {
width: 100px;
margin-bottom: 10px;
margin-right: 20px;
text-align: center;
list-style: none !important;
cursor: default;
}
.icon_lists li .code-name {
line-height: 1.2;
}
.icon_lists .icon {
display: block;
height: 100px;
line-height: 100px;
font-size: 42px;
margin: 10px auto;
color: #333;
-webkit-transition: font-size 0.25s linear, width 0.25s linear;
-moz-transition: font-size 0.25s linear, width 0.25s linear;
transition: font-size 0.25s linear, width 0.25s linear;
}
.icon_lists .icon:hover {
font-size: 100px;
}
.icon_lists .svg-icon {
/* 通过设置 font-size 来改变图标大小 */
width: 1em;
/* 图标和文字相邻时,垂直对齐 */
vertical-align: -0.15em;
/* 通过设置 color 来改变 SVG 的颜色/fill */
fill: currentColor;
/* path 和 stroke 溢出 viewBox 部分在 IE 下会显示
normalize.css 中也包含这行 */
overflow: hidden;
}
.icon_lists li .name,
.icon_lists li .code-name {
color: #666;
}
/* markdown 样式 */
.markdown {
color: #666;
font-size: 14px;
line-height: 1.8;
}
.highlight {
line-height: 1.5;
}
.markdown img {
vertical-align: middle;
max-width: 100%;
}
.markdown h1 {
color: #404040;
font-weight: 500;
line-height: 40px;
margin-bottom: 24px;
}
.markdown h2,
.markdown h3,
.markdown h4,
.markdown h5,
.markdown h6 {
color: #404040;
margin: 1.6em 0 0.6em 0;
font-weight: 500;
clear: both;
}
.markdown h1 {
font-size: 28px;
}
.markdown h2 {
font-size: 22px;
}
.markdown h3 {
font-size: 16px;
}
.markdown h4 {
font-size: 14px;
}
.markdown h5 {
font-size: 12px;
}
.markdown h6 {
font-size: 12px;
}
.markdown hr {
height: 1px;
border: 0;
background: #e9e9e9;
margin: 16px 0;
clear: both;
}
.markdown p {
margin: 1em 0;
}
.markdown>p,
.markdown>blockquote,
.markdown>.highlight,
.markdown>ol,
.markdown>ul {
width: 80%;
}
.markdown ul>li {
list-style: circle;
}
.markdown>ul li,
.markdown blockquote ul>li {
margin-left: 20px;
padding-left: 4px;
}
.markdown>ul li p,
.markdown>ol li p {
margin: 0.6em 0;
}
.markdown ol>li {
list-style: decimal;
}
.markdown>ol li,
.markdown blockquote ol>li {
margin-left: 20px;
padding-left: 4px;
}
.markdown code {
margin: 0 3px;
padding: 0 5px;
background: #eee;
border-radius: 3px;
}
.markdown strong,
.markdown b {
font-weight: 600;
}
.markdown>table {
border-collapse: collapse;
border-spacing: 0px;
empty-cells: show;
border: 1px solid #e9e9e9;
width: 95%;
margin-bottom: 24px;
}
.markdown>table th {
white-space: nowrap;
color: #333;
font-weight: 600;
}
.markdown>table th,
.markdown>table td {
border: 1px solid #e9e9e9;
padding: 8px 16px;
text-align: left;
}
.markdown>table th {
background: #F7F7F7;
}
.markdown blockquote {
font-size: 90%;
color: #999;
border-left: 4px solid #e9e9e9;
padding-left: 0.8em;
margin: 1em 0;
}
.markdown blockquote p {
margin: 0;
}
.markdown .anchor {
opacity: 0;
transition: opacity 0.3s ease;
margin-left: 8px;
}
.markdown .waiting {
color: #ccc;
}
.markdown h1:hover .anchor,
.markdown h2:hover .anchor,
.markdown h3:hover .anchor,
.markdown h4:hover .anchor,
.markdown h5:hover .anchor,
.markdown h6:hover .anchor {
opacity: 1;
display: inline-block;
}
.markdown>br,
.markdown>p>br {
clear: both;
}
.hljs {
display: block;
background: white;
padding: 0.5em;
color: #333333;
overflow-x: auto;
}
.hljs-comment,
.hljs-meta {
color: #969896;
}
.hljs-string,
.hljs-variable,
.hljs-template-variable,
.hljs-strong,
.hljs-emphasis,
.hljs-quote {
color: #df5000;
}
.hljs-keyword,
.hljs-selector-tag,
.hljs-type {
color: #a71d5d;
}
.hljs-literal,
.hljs-symbol,
.hljs-bullet,
.hljs-attribute {
color: #0086b3;
}
.hljs-section,
.hljs-name {
color: #63a35c;
}
.hljs-tag {
color: #333333;
}
.hljs-title,
.hljs-attr,
.hljs-selector-id,
.hljs-selector-class,
.hljs-selector-attr,
.hljs-selector-pseudo {
color: #795da3;
}
.hljs-addition {
color: #55a532;
background-color: #eaffea;
}
.hljs-deletion {
color: #bd2c00;
background-color: #ffecec;
}
.hljs-link {
text-decoration: underline;
}
/* 代码高亮 */
/* PrismJS 1.15.0
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript */
/**
* prism.js default theme for JavaScript, CSS and HTML
* Based on dabblet (http://dabblet.com)
* @author Lea Verou
*/
code[class*="language-"],
pre[class*="language-"] {
color: black;
background: none;
text-shadow: 0 1px white;
font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre[class*="language-"]::-moz-selection,
pre[class*="language-"] ::-moz-selection,
code[class*="language-"]::-moz-selection,
code[class*="language-"] ::-moz-selection {
text-shadow: none;
background: #b3d4fc;
}
pre[class*="language-"]::selection,
pre[class*="language-"] ::selection,
code[class*="language-"]::selection,
code[class*="language-"] ::selection {
text-shadow: none;
background: #b3d4fc;
}
@media print {
code[class*="language-"],
pre[class*="language-"] {
text-shadow: none;
}
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: .5em 0;
overflow: auto;
}
:not(pre)>code[class*="language-"],
pre[class*="language-"] {
background: #f5f2f0;
}
/* Inline code */
:not(pre)>code[class*="language-"] {
padding: .1em;
border-radius: .3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: slategray;
}
.token.punctuation {
color: #999;
}
.namespace {
opacity: .7;
}
.token.property,
.token.tag,
.token.boolean,
.token.number,
.token.constant,
.token.symbol,
.token.deleted {
color: #905;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #690;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string {
color: #9a6e3a;
background: hsla(0, 0%, 100%, .5);
}
.token.atrule,
.token.attr-value,
.token.keyword {
color: #07a;
}
.token.function,
.token.class-name {
color: #DD4A68;
}
.token.regex,
.token.important,
.token.variable {
color: #e90;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}

View File

@@ -0,0 +1,418 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>iconfont Demo</title>
<link rel="shortcut icon" href="//img.alicdn.com/imgextra/i4/O1CN01Z5paLz1O0zuCC7osS_!!6000000001644-55-tps-83-82.svg" type="image/x-icon"/>
<link rel="icon" type="image/svg+xml" href="//img.alicdn.com/imgextra/i4/O1CN01Z5paLz1O0zuCC7osS_!!6000000001644-55-tps-83-82.svg"/>
<link rel="stylesheet" href="https://g.alicdn.com/thx/cube/1.3.2/cube.min.css">
<link rel="stylesheet" href="demo.css">
<link rel="stylesheet" href="iconfont.css">
<script src="iconfont.js"></script>
<!-- jQuery -->
<script src="https://a1.alicdn.com/oss/uploads/2018/12/26/7bfddb60-08e8-11e9-9b04-53e73bb6408b.js"></script>
<!-- 代码高亮 -->
<script src="https://a1.alicdn.com/oss/uploads/2018/12/26/a3f714d0-08e6-11e9-8a15-ebf944d7534c.js"></script>
<style>
.main .logo {
margin-top: 0;
height: auto;
}
.main .logo a {
display: flex;
align-items: center;
}
.main .logo .sub-title {
margin-left: 0.5em;
font-size: 22px;
color: #fff;
background: linear-gradient(-45deg, #3967FF, #B500FE);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
</style>
</head>
<body>
<div class="main">
<h1 class="logo"><a href="https://www.iconfont.cn/" title="iconfont 首页" target="_blank">
<img width="200" src="https://img.alicdn.com/imgextra/i3/O1CN01Mn65HV1FfSEzR6DKv_!!6000000000514-55-tps-228-59.svg">
</a></h1>
<div class="nav-tabs">
<ul id="tabs" class="dib-box">
<li class="dib active"><span>Unicode</span></li>
<li class="dib"><span>Font class</span></li>
<li class="dib"><span>Symbol</span></li>
</ul>
<a href="https://www.iconfont.cn/manage/index?manage_type=myprojects&projectId=5044714" target="_blank" class="nav-more">查看项目</a>
</div>
<div class="tab-container">
<div class="content unicode" style="display: block;">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont">&#xe63f;</span>
<div class="name">gxbys</div>
<div class="code-name">&amp;#xe63f;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe60f;</span>
<div class="name">个人中心</div>
<div class="code-name">&amp;#xe60f;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe607;</span>
<div class="name">素质测评</div>
<div class="code-name">&amp;#xe607;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe887;</span>
<div class="name">智能AI</div>
<div class="code-name">&amp;#xe887;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe614;</span>
<div class="name">技能培训</div>
<div class="code-name">&amp;#xe614;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe61d;</span>
<div class="name">政策</div>
<div class="code-name">&amp;#xe61d;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe67f;</span>
<div class="name">题库和考试</div>
<div class="code-name">&amp;#xe67f;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe723;</span>
<div class="name">技能评价</div>
<div class="code-name">&amp;#xe723;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe61c;</span>
<div class="name">简历</div>
<div class="code-name">&amp;#xe61c;</div>
</li>
<li class="dib">
<span class="icon iconfont">&#xe7e9;</span>
<div class="name">政府楼</div>
<div class="code-name">&amp;#xe7e9;</div>
</li>
</ul>
<div class="article markdown">
<h2 id="unicode-">Unicode 引用</h2>
<hr>
<p>Unicode 是字体在网页端最原始的应用方式,特点是:</p>
<ul>
<li>支持按字体的方式去动态调整图标大小,颜色等等。</li>
<li>默认情况下不支持多色,直接添加多色图标会自动去色。</li>
</ul>
<blockquote>
<p>注意:新版 iconfont 支持两种方式引用多色图标SVG symbol 引用方式和彩色字体图标模式。(使用彩色字体图标需要在「编辑项目」中开启「彩色」选项后并重新生成。)</p>
</blockquote>
<p>Unicode 使用步骤如下:</p>
<h3 id="-font-face">第一步:拷贝项目下面生成的 <code>@font-face</code></h3>
<pre><code class="language-css"
>@font-face {
font-family: 'iconfont';
src: url('iconfont.woff2?t=1761826914823') format('woff2'),
url('iconfont.woff?t=1761826914823') format('woff'),
url('iconfont.ttf?t=1761826914823') format('truetype');
}
</code></pre>
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
<pre><code class="language-css"
>.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
</code></pre>
<h3 id="-">第三步:挑选相应图标并获取字体编码,应用于页面</h3>
<pre>
<code class="language-html"
>&lt;span class="iconfont"&gt;&amp;#x33;&lt;/span&gt;
</code></pre>
<blockquote>
<p>"iconfont" 是你项目下的 font-family。可以通过编辑项目查看默认是 "iconfont"。</p>
</blockquote>
</div>
</div>
<div class="content font-class">
<ul class="icon_lists dib-box">
<li class="dib">
<span class="icon iconfont icon-Graduation-simple-"></span>
<div class="name">
gxbys
</div>
<div class="code-name">.icon-Graduation-simple-
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-gerenzhongxin"></span>
<div class="name">
个人中心
</div>
<div class="code-name">.icon-gerenzhongxin
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-suzhicepingtiku"></span>
<div class="name">
素质测评
</div>
<div class="code-name">.icon-suzhicepingtiku
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-ai"></span>
<div class="name">
智能AI
</div>
<div class="code-name">.icon-ai
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-jinengpeixun"></span>
<div class="name">
技能培训
</div>
<div class="code-name">.icon-jinengpeixun
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-zhengce"></span>
<div class="name">
政策
</div>
<div class="code-name">.icon-zhengce
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-chengjifuben"></span>
<div class="name">
题库和考试
</div>
<div class="code-name">.icon-chengjifuben
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-jinengpingjia"></span>
<div class="name">
技能评价
</div>
<div class="code-name">.icon-jinengpingjia
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-jianli"></span>
<div class="name">
简历
</div>
<div class="code-name">.icon-jianli
</div>
</li>
<li class="dib">
<span class="icon iconfont icon-zhengfulou"></span>
<div class="name">
政府楼
</div>
<div class="code-name">.icon-zhengfulou
</div>
</li>
</ul>
<div class="article markdown">
<h2 id="font-class-">font-class 引用</h2>
<hr>
<p>font-class 是 Unicode 使用方式的一种变种,主要是解决 Unicode 书写不直观,语意不明确的问题。</p>
<p>与 Unicode 使用方式相比,具有如下特点:</p>
<ul>
<li>相比于 Unicode 语意明确,书写更直观。可以很容易分辨这个 icon 是什么。</li>
<li>因为使用 class 来定义图标,所以当要替换图标时,只需要修改 class 里面的 Unicode 引用。</li>
</ul>
<p>使用步骤如下:</p>
<h3 id="-fontclass-">第一步:引入项目下面生成的 fontclass 代码:</h3>
<pre><code class="language-html">&lt;link rel="stylesheet" href="./iconfont.css"&gt;
</code></pre>
<h3 id="-">第二步:挑选相应图标并获取类名,应用于页面:</h3>
<pre><code class="language-html">&lt;span class="iconfont icon-xxx"&gt;&lt;/span&gt;
</code></pre>
<blockquote>
<p>"
iconfont" 是你项目下的 font-family。可以通过编辑项目查看默认是 "iconfont"。</p>
</blockquote>
</div>
</div>
<div class="content symbol">
<ul class="icon_lists dib-box">
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-Graduation-simple-"></use>
</svg>
<div class="name">gxbys</div>
<div class="code-name">#icon-Graduation-simple-</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-gerenzhongxin"></use>
</svg>
<div class="name">个人中心</div>
<div class="code-name">#icon-gerenzhongxin</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-suzhicepingtiku"></use>
</svg>
<div class="name">素质测评</div>
<div class="code-name">#icon-suzhicepingtiku</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-ai"></use>
</svg>
<div class="name">智能AI</div>
<div class="code-name">#icon-ai</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-jinengpeixun"></use>
</svg>
<div class="name">技能培训</div>
<div class="code-name">#icon-jinengpeixun</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-zhengce"></use>
</svg>
<div class="name">政策</div>
<div class="code-name">#icon-zhengce</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-chengjifuben"></use>
</svg>
<div class="name">题库和考试</div>
<div class="code-name">#icon-chengjifuben</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-jinengpingjia"></use>
</svg>
<div class="name">技能评价</div>
<div class="code-name">#icon-jinengpingjia</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-jianli"></use>
</svg>
<div class="name">简历</div>
<div class="code-name">#icon-jianli</div>
</li>
<li class="dib">
<svg class="icon svg-icon" aria-hidden="true">
<use xlink:href="#icon-zhengfulou"></use>
</svg>
<div class="name">政府楼</div>
<div class="code-name">#icon-zhengfulou</div>
</li>
</ul>
<div class="article markdown">
<h2 id="symbol-">Symbol 引用</h2>
<hr>
<p>这是一种全新的使用方式,应该说这才是未来的主流,也是平台目前推荐的用法。相关介绍可以参考这篇<a href="">文章</a>
这种用法其实是做了一个 SVG 的集合,与另外两种相比具有如下特点:</p>
<ul>
<li>支持多色图标了,不再受单色限制。</li>
<li>通过一些技巧,支持像字体那样,通过 <code>font-size</code>, <code>color</code> 来调整样式。</li>
<li>兼容性较差,支持 IE9+,及现代浏览器。</li>
<li>浏览器渲染 SVG 的性能一般,还不如 png。</li>
</ul>
<p>使用步骤如下:</p>
<h3 id="-symbol-">第一步:引入项目下面生成的 symbol 代码:</h3>
<pre><code class="language-html">&lt;script src="./iconfont.js"&gt;&lt;/script&gt;
</code></pre>
<h3 id="-css-">第二步:加入通用 CSS 代码(引入一次就行):</h3>
<pre><code class="language-html">&lt;style&gt;
.icon {
width: 1em;
height: 1em;
vertical-align: -0.15em;
fill: currentColor;
overflow: hidden;
}
&lt;/style&gt;
</code></pre>
<h3 id="-">第三步:挑选相应图标并获取类名,应用于页面:</h3>
<pre><code class="language-html">&lt;svg class="icon" aria-hidden="true"&gt;
&lt;use xlink:href="#icon-xxx"&gt;&lt;/use&gt;
&lt;/svg&gt;
</code></pre>
</div>
</div>
</div>
</div>
<script>
$(document).ready(function () {
$('.tab-container .content:first').show()
$('#tabs li').click(function (e) {
var tabContent = $('.tab-container .content')
var index = $(this).index()
if ($(this).hasClass('active')) {
return
} else {
$('#tabs li').removeClass('active')
$(this).addClass('active')
tabContent.hide().eq(index).fadeIn()
}
})
})
</script>
</body>
</html>

View File

@@ -0,0 +1,55 @@
@font-face {
font-family: "iconfont"; /* Project id 5044714 */
src: url('iconfont.woff2?t=1761826914823') format('woff2'),
url('iconfont.woff?t=1761826914823') format('woff'),
url('iconfont.ttf?t=1761826914823') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-Graduation-simple-:before {
content: "\e63f";
}
.icon-gerenzhongxin:before {
content: "\e60f";
}
.icon-suzhicepingtiku:before {
content: "\e607";
}
.icon-ai:before {
content: "\e887";
}
.icon-jinengpeixun:before {
content: "\e614";
}
.icon-zhengce:before {
content: "\e61d";
}
.icon-chengjifuben:before {
content: "\e67f";
}
.icon-jinengpingjia:before {
content: "\e723";
}
.icon-jianli:before {
content: "\e61c";
}
.icon-zhengfulou:before {
content: "\e7e9";
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,79 @@
{
"id": "5044714",
"name": "喀什APP",
"font_family": "iconfont",
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"icon_id": "5122382",
"name": "gxbys",
"font_class": "Graduation-simple-",
"unicode": "e63f",
"unicode_decimal": 58943
},
{
"icon_id": "6311925",
"name": "个人中心",
"font_class": "gerenzhongxin",
"unicode": "e60f",
"unicode_decimal": 58895
},
{
"icon_id": "28808301",
"name": "素质测评",
"font_class": "suzhicepingtiku",
"unicode": "e607",
"unicode_decimal": 58887
},
{
"icon_id": "4638874",
"name": "智能AI",
"font_class": "ai",
"unicode": "e887",
"unicode_decimal": 59527
},
{
"icon_id": "2400112",
"name": "技能培训",
"font_class": "jinengpeixun",
"unicode": "e614",
"unicode_decimal": 58900
},
{
"icon_id": "21800726",
"name": "政策",
"font_class": "zhengce",
"unicode": "e61d",
"unicode_decimal": 58909
},
{
"icon_id": "28948661",
"name": "题库和考试",
"font_class": "chengjifuben",
"unicode": "e67f",
"unicode_decimal": 59007
},
{
"icon_id": "43251924",
"name": "技能评价",
"font_class": "jinengpingjia",
"unicode": "e723",
"unicode_decimal": 59171
},
{
"icon_id": "648773",
"name": "简历",
"font_class": "jianli",
"unicode": "e61c",
"unicode_decimal": 58908
},
{
"icon_id": "43701068",
"name": "政府楼",
"font_class": "zhengfulou",
"unicode": "e7e9",
"unicode_decimal": 59369
}
]
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
static/imgs/avatar.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

2790155
static/json/address.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More