Compare commits

...

13 Commits

Author SHA1 Message Date
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
冯辉
028e3202bd 修改疆外职位展示内容 2025-10-15 16:46:21 +08:00
冯辉
7307335487 删除用户ID参数 2025-10-13 17:29:57 +08:00
125 changed files with 2799785 additions and 437 deletions

26
App.vue
View File

@@ -11,24 +11,26 @@ onLaunch((options) => {
useDictStore().getDictData();
// uni.hideTabBar();
// 登录
let token = uni.getStorageSync('token') || ''; // 同步获取 缓存信息
// 尝试从缓存恢复用户信息
const restored = useUserStore().restoreUserInfo();
if (restored) {
// 如果成功恢复用户信息验证token是否有效
let token = uni.getStorageSync('token') || '';
if (token) {
useUserStore()
.loginSetToken(token)
.then(() => {
$api.msg('登录成功');
console.log('用户登录状态已恢复');
})
.catch(() => {
uni.redirectTo({
url: '/pages/login/login',
});
});
} else {
uni.redirectTo({
url: '/pages/login/login',
// token无效清除缓存不跳转登录页
console.log('token已过期需要重新登录');
useUserStore().logOut(false);
});
}
}
// 不再强制跳转到登录页,而是在需要登录时弹出授权弹窗
});
onMounted(() => {
@@ -53,6 +55,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 +85,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 +103,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

@@ -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?.userType || 0);
const switchUserType = (userType) => {
console.log('切换用户类型:', userType);
console.log('切换前 userInfo:', userInfo.value);
userInfo.value.userType = 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

@@ -19,8 +19,11 @@
</template>
<script setup>
import { ref, defineProps, onMounted, computed } from 'vue';
import { ref, onMounted, computed } from 'vue';
import { useReadMsg } from '@/stores/useReadMsg';
import { storeToRefs } from 'pinia';
import useUserStore from '@/stores/useUserStore';
const props = defineProps({
currentpage: {
type: Number,
@@ -28,9 +31,14 @@ const props = defineProps({
default: 0,
},
});
const readMsg = useReadMsg();
const { userInfo } = storeToRefs(useUserStore());
const currentItem = ref(0);
const tabbarList = computed(() => [
// 根据用户类型生成不同的导航栏配置
const tabbarList = computed(() => {
const baseItems = [
{
id: 0,
text: '首页',
@@ -40,15 +48,6 @@ const tabbarList = computed(() => [
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: '',
@@ -76,17 +75,65 @@ const tabbarList = computed(() => [
centerItem: false,
badge: readMsg.badges[4].count,
},
]);
];
// 根据用户类型添加不同的导航项
const userType = userInfo.value?.userType || 0;
if (userType === 0) {
// 企业用户:显示发布岗位,隐藏招聘会
baseItems.splice(1, 0, {
id: 1,
text: '发布岗位',
path: '/pages/job/publishJob',
iconPath: '../../static/tabbar/publish-job.svg',
selectedIconPath: '../../static/tabbar/publish-job-selected.svg',
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,
});
}
return baseItems;
});
onMounted(() => {
uni.hideTabBar();
// 自定义TabBar不需要调用hideTabBar因为已经在pages.json中设置了custom: true
// uni.hideTabBar(); // 移除这行避免在自定义TabBar模式下调用
currentItem.value = props.currentpage;
});
const changeItem = (item) => {
// 判断是否为 tabBar 页面
const tabBarPages = [
'/pages/index/index',
'/pages/careerfair/careerfair',
'/pages/chat/chat',
'/pages/msglog/msglog',
'/pages/mine/mine'
];
if (tabBarPages.includes(item.path)) {
// tabBar 页面使用 switchTab
uni.switchTab({
url: item.path,
});
} else {
// 非 tabBar 页面使用 navigateTo
uni.navigateTo({
url: item.path,
});
}
};
</script>

View File

@@ -0,0 +1,525 @@
<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';
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();
if (resData.token) {
// 登录成功存储token
loginSetToken(resData.token).then((resume) => {
$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'
});
}
}
}).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) => {
$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) => {
$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'
});
}
}
});
}
});
},
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) => {
$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'
});
}
}
}).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,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,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) {
// 只在H5环境下添加resize监听器
// #ifdef H5
if (typeof window !== 'undefined') {
window.addEventListener('resize', calcColumn)
// }
}
// #endif
})
onUnmounted(() => {
// if (process.client) {
// #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,46 +4,78 @@ 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 (scrollTop > hideThreshold) {
if (!shouldHideTop.value) {
shouldHideTop.value = true
}
} else {
if (shouldHideTop.value) {
shouldHideTop.value = 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) // 通知变更为向下
}
accumulatedScroll.value = 0
}
if (accumulatedScroll.value < -threshold) {
} else {
// 向上滚动
if (isScrollingDown.value) {
isScrollingDown.value = false
onChange?.(false) // 通知变更为向上
}
}
if (enablePerformanceMode) {
accumulatedScroll.value = 0
}
}
lastScrollTop.value = scrollTop
}
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)
// 只在支持 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,6 +1,6 @@
{
"name" : "qingdao-employment-service",
"appid": "__UNI__C939371",
"appid" : "__UNI__F0ABFDF",
"description" : "招聘",
"versionName" : "1.0.0",
"versionCode" : "100",
@@ -50,7 +50,7 @@
"quickapp" : {},
/* */
"mp-weixin" : {
"appid": "",
"appid" : "wx9d1cbc11c8c40ba7",
"setting" : {
"urlCheck" : false,
"es6" : true,
@@ -62,7 +62,8 @@
"scope.userLocation" : {
"desc" : "用于用户选择地图查看位置"
}
}
},
"libVersion" : "3.5.7"
},
"mp-alipay" : {
"usingComponents" : true

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

@@ -184,13 +184,6 @@
return;
}
console.log(userInfo.userId)
// 动态获取用户ID
const currentUserId = userInfo.userId;
if (!currentUserId) {
$api.msg('用户信息获取失败,请重新登录');
return;
}
// 调试:打印表单数据
console.log('表单数据:', formData);
console.log('结束时间:', formData.endDate);
@@ -200,11 +193,10 @@
const endDateValue = formData.endDate === '至今' ? currentDate : formData.endDate;
console.log('处理后的结束时间:', endDateValue);
// 准备请求参数
// 准备请求参数不包含userId让后端通过token获取
const params = {
companyName: formData.companyName.trim(),
position: formData.position.trim(),
userId: currentUserId, // 使用动态获取的用户ID
startDate: formData.startDate,
endDate: endDateValue, // 使用处理后的结束时间
description: formData.description.trim()
@@ -214,11 +206,11 @@
console.log('页面类型:', pageType.value);
let resData;
alert(editData.value.id)
// 根据页面类型调用不同的接口
if (pageType.value === 'edit' && editData.value?.id) {
// 编辑模式:调用更新接口
resData = await $api.createRequest(`/app/userworkexperiences/${editData.value.id}`, params, 'put');
resData = await $api.createRequest(`/app/userworkexperiences/edit`, {...params, id: editData.value.id}, 'put');
console.log('编辑接口响应:', resData);
} else {
// 添加模式:调用新增接口

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

@@ -245,7 +245,7 @@ onShow(() => {
});
function initLoad(option) {
const jobId = atob(option.jobId);
const jobId = decodeURIComponent(option.jobId);
if (jobId !== jobIdRef.value) {
jobIdRef.value = jobId;
getDetail(jobId);

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,395 @@
<!--
* @Date: 2025-10-16 15:15:47
* @LastEditors: shirlwang
* @LastEditTime: 2025-10-22 14:17:46
-->
<template>
<!-- @scroll="handleScroll" @scrolltolower="scrollBottom" -->
<scroll-view :scroll-y="true" class="container" style="background-image: url('../../../packageRc/static/pageBg.png');">
<view style="padding: 40rpx 28rpx;">
<view class="kinggang">
<view>
<view class="num-title" style="color: #1A62CE">重点毕业生数</view>
<view>1120</view>
</view>
<text style="color: #B5C1D1;"></text>
<view>
<view class="num-title" style="color: #16ACB7">累计需求数</view>
<view>1120</view>
</view>
<text style="color: #B5C1D1;"></text>
<view>
<view class="num-title" style="color: #6A57D1">累计服务数</view>
<view>1120</view>
</view>
</view>
<view class="trace-line">
<view class="trace">
<view class="title">
<image src="../../../packageRc/static/trace.png"/>
毕业生追踪
</view>
<view style="display: flex;justify-content: space-between;">
<view>点击查看</view>
<uni-icons color="#639AEB" type="arrow-right" size="16"></uni-icons>
</view>
</view>
<view class="sendManager">
<view class="title">
<image src="../../../packageRc/static/sendManager.png"/>
任务下发管理员
</view>
<view style="display: flex;justify-content: space-between;">
<view>点击查看</view>
<uni-icons color="#DBAA4E" type="arrow-right" size="16"></uni-icons>
</view>
</view>
</view>
<view class="titles">
<view class="title-item active"><view>待办需求预警列表</view></view>
<view> 2 条信息</view>
</view>
<view v-for="(item, index) in jobList" :key="index" class="job-list">
<view class="title">销售顾问</view>
<view class="info">
待办内容文字示例待办内容文字示例待办内容文字示例待办内容文字示例
</view>
<view class="bottom-line">
<view>发起时间2025-09-24 15:02</view>
<view style="color: #EF7325;">青岛xx公司</view>
</view>
</view>
<view class="titles">
<view class="title-item active"><view>待服务毕业生列表</view></view>
<view> 22 条信息</view>
</view>
<view v-for="(item, index) in jobList" :key="index" class="person-list">
<view class="top-info">
<image v-if="index%2==0" src="../../../packageRc/static/personIcon.png"/>
<image v-else src="../../../packageRc/static/personIconFe.png"/>
<view class="top-right">
<view class="name-line">
<view class="name">姓名<view class="tag">硕士</view></view>
<view class="service-status">·未服务</view>
</view>
<view class="info-line" style="display: flex;">
<view style="margin-right: 24rpx;"><text>年龄</text>27</view>
<view><text>服务次数</text>1</view>
</view>
</view>
</view>
<view class="info-line">
<view><text>联系电话</text>152****5488</view>
<view><text>详细地址</text>山东省济南市历城区港沟街道融创文旅城鹊桥华居8号楼1单元801</view>
</view>
<view class="services">
<view>退回</view>
<view>服务</view>
</view>
</view>
</view>
</scroll-view>
</template>
<script setup>
import { reactive, inject, watch, ref, onMounted, watchEffect, nextTick } from 'vue';
let activeTab = ref(1)
let activeTitle = ref(1)
let jobList = ref([{},{},{},{},{}])
function back() {
uni.navigateBack({
delta: 1
})
}
function viewMore() {
// uni.navigateTo({
// url: '/pages/jobList/jobList'
// })
}
</script>
<style lang="less" scoped>
view{box-sizing: border-box;display: block;}
.container{
background-color: #F4F4F4;background-position: top center;background-size: 100% auto;
height: 100vh;
min-width: 100vw;
padding-bottom: 0;
background-repeat: no-repeat;
box-sizing: border-box;
}
.kinggang{
display: flex;
justify-content: space-around;
align-items: center;
padding: 36rpx 32rpx 33rpx 32rpx;
background: #FFFFFF;
border-radius: 16rpx;
border: 3rpx solid #FFFFFF;
margin-bottom: 24rpx;background: linear-gradient(180deg, #EDF4FF 0%, #FFFFFF 52%);
>view{
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
font-weight: bold;
>view{font-size: 50rpx;}
.num-title{
font-size: 28rpx;
margin-top: 16rpx;
font-weight: normal;
}
// text{
// font-size: 28rpx;
// }
}
image{
width: 78rpx;
// margin-bottom: 15rpx;
height: 78rpx;
}
}
.trace-line{
width: 100%;
margin-bottom: 24rpx;
display: flex;
justify-content: space-between;
>view{
padding: 37.5rpx 32rpx;
width: calc(50% - 12rpx);
.title{
font-size: 28rpx;
font-weight: bold;
color: #1D6AD7;
margin-bottom: 16rpx;
display: flex;
image{
width: 46rpx;
height: 46rpx;
margin-right: 11rpx;
}
}
.more{
display: flex;
justify-content: space-between;
}
}
.trace{
color: #2E77DF;
border: 2rpx solid #78ADFF;
background: linear-gradient(0deg, #FFFFFF 1%, #D0E1FF 100%);
box-shadow: inset 0px 4rpx 10rpx 0px rgba(255, 255, 255, 0.302);
.title{
color: #1D6AD7;
}
}
.sendManager{
border: 2rpx solid #FFC34B;
color: #C68412;
box-shadow: inset 0px 4px 10px 0px rgba(255, 255, 255, 0.302);
background: linear-gradient(0deg, #FFFFFF 0%, #FBF4D1 100%);
.title{color: #CE9523;}
}
}
.tabs{
margin-bottom: 29rpx;
border-radius: 16rpx;
display: flex;
background: #fff;
color: #878787;
text-align: center;
width: 100%;
.tab{
width: 50%;
border: 4rpx solid transparent;
border-radius: 16rpx;
line-height: 64rpx;
position: relative;
&.active{
border: 4rpx solid #fff;
color: #fff;
background: linear-gradient(180deg, #79AFFF 1%, #A2B3FE 100%);
box-shadow: 0px 4rpx 10rpx 0px rgba(40, 102, 194, 0.4);
}
}
}
.titles{
display: flex;
margin-bottom: 44rpx;
display: flex;
justify-content: space-between;
.title-item{
font-size: 32rpx;
font-weight: bold;
color: #282828;
margin-right: 32rpx;
position: relative;
>view{
position: relative;
z-index: 2;
padding: 0 16rpx;
}
&.active::after{
content: '';
position: absolute;
z-index: 1;
bottom: -12rpx;
left: 0;
width: 120%;
height: 24rpx;
border-radius: 50px 0px 0px 50px;
background: linear-gradient(90deg, #78AEFF 0%, rgba(120, 174, 255, 0.31) 52%, rgba(24, 116, 255, 0) 100%);
}
}
}
.job-list{
width: 100%;
margin: 0 auto;
color: #333333;
border-radius: 24rpx;
background: #FFFFFF;
padding: 32rpx;
margin-bottom: 24rpx;
position: relative;
.sign{
position: absolute;
font-size: 24rpx;
right: 0;
top: 0;
padding: 4rpx 14rpx;
border: 1rpx solid #EC4827;
background: rgba(227, 79, 49, 0.09);
border-top-right-radius: 24rpx;
border-bottom-left-radius: 24rpx;
color: #EC4827;
}
.top-line{
display: flex;
justify-content: space-between;
font-size: 24rpx;
color: #A2A2A2;
margin-bottom: 16rpx;
.salary{
font-size: 32rpx;
color: #4C6EFB;
font-weight: bold;
}
}
.title{
font-size: 32rpx;
font-weight: bold;
color: #282828;
margin-bottom: 16rpx;
display: flex;
image{
width: 46rpx;
height: 46rpx;
margin-right: 11rpx;
}
}
.info{
font-size: 24rpx;
margin-bottom: 16rpx;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.bottom-line{
display: flex;
justify-content: space-between;
font-size: 24rpx;
color: #A2A2A2;
margin-top: 12rpx;
}
}
.view-more-btn{
padding: 10rpx 56rpx;
background: #FFFFFF;
color: #4C6EFB;
border: 1rpx solid #4C6EFB;
text-align: center;
border-radius: 40rpx;
width: fit-content;
margin: 0 auto;
margin-bottom: 20rpx;
}
.person-list{
padding-top: 32rpx;
margin-bottom: 24rpx;
border-radius: 16rpx;
background: #FFFFFF;
.top-info{
padding: 0 32rpx;
margin-bottom: 24rpx;
display: flex;
image{
width: 86rpx;
height: 86rpx;
margin-right: 19rpx;
display: block;
}
.top-right{
flex-grow: 1;
flex-shrink: 1;
.name-line{
display: flex;
justify-content: space-between;
align-items: center;
.name{
font-size: 32rpx;
display: flex;
margin-right: 15rpx;
margin-bottom: 3rpx;
align-items: center;
.tag{
font-size: 24rpx;
line-height: 32rpx;
padding: 0 12rpx;
margin-left: 15rpx;
border-radius: 4rpx;
background: #4D89E3;
color: #fff;
}
}
.service-status{
color: #E0A61F;
font-weight: bold;
font-size: 26rpx;
}
}
.info-line{
padding: 0;
border: 0;
}
}
}
.info-line{
padding: 0 32rpx 32rpx;
color: #3D3D3D;
border-bottom: 1px solid #E8E8E8;
>view{
text{
color: #8E8E8E;
}
font-size: 26rpx;
}
}
.services{
line-height: 40rpx;
padding: 28rpx 32rpx;
display: flex;
>view{
flex-grow: 1;
font-size: 26rpx;
color: #1A62CE;
text-align: center;
&:first-child{
color: #E04020;
border-right: 1px solid #D8D8D8;
}
}
}
}
</style>

View File

@@ -0,0 +1,255 @@
<!--
* @Date: 2025-10-16 15:15:47
* @LastEditors: shirlwang
* @LastEditTime: 2025-10-22 09:54:53
-->
<template>
<!-- @scroll="handleScroll" @scrolltolower="scrollBottom" -->
<scroll-view :scroll-y="true" class="container" style="background-image: url('../../../packageRc/static/pageBg.png');">
<view style="padding: 40rpx 28rpx;">
<view class="kinggang">
<view>
<image src="../../../packageRc/static/kinggang1.png"/>
<view>信息维护</view>
</view>
<view>
<image src="../../../packageRc/static/kinggang5.png"/>
<view>投递记录</view>
</view>
<view>
<image src="../../../packageRc/static/kinggang2.png"/>
<view>需求上报</view>
</view>
<view>
<image src="../../../packageRc/static/kinggang3.png"/>
<view>虚拟面试</view>
</view>
<view>
<image src="../../../packageRc/static/kinggang4.png"/>
<view>素质测评</view>
</view>
</view>
<view class="tabs">
<view class="tab" :class="{active: activeTab == 1}" @click="activeTab = 1">岗位列表</view>
<view class="tab" :class="{active: activeTab == 2}" @click="activeTab = 2">实习实训</view>
<view class="tab" :class="{active: activeTab == 3}" @click="activeTab = 3">社区实践</view>
</view>
<view class="titles">
<view class="title-item" :class="{active: activeTitle == 1}" @click="activeTitle = 1"><view>推荐岗位</view></view>
<view class="title-item" :class="{active: activeTitle == 2}" @click="activeTitle = 2"><view>热门岗位</view></view>
</view>
<view v-for="(item, index) in jobList" :key="index" class="job-list">
<view class="top-line">
<view class="salary">4000-8000/</view>
<view class="time"><uni-icons color="#A2A2A2" type="info" size="12"></uni-icons>发布日期2025-10-20</view>
</view>
<view class="title">销售顾问</view>
<view class="infos">
<view>大专</view>
<view>1-3</view>
<view>喀什 市南区</view>
</view>
<view class="bottom-line">
<view><uni-icons color="#A2A2A2" type="person" size="12"></uni-icons>6</view>
<view>青岛xx公司</view>
</view>
</view>
<view class="view-more-btn" @click="viewMore">查看更多内容</view>
<view class="titles" style="justify-content: space-between;">
<view class="title-item active"><view>政策专区</view></view>
<view>{{'查看更多 >'}}</view>
</view>
<view v-for="(item, index) in jobList" :key="index" class="job-list">
<view class="sign">推荐</view>
<view class="title">
<image src="../../../packageRc/static/zcLeft.png"/>
销售顾问</view>
<view class="infos">
<view>大专</view>
<view>1-3</view>
<view>喀什 市南区</view>
</view>
<view class="bottom-line">
<view><uni-icons color="#A2A2A2" type="info" size="12"></uni-icons>发布日期2025-10-20</view>
<view>浏览数<text style="color: #6AA7E8">99+</text></view>
</view>
</view>
</view>
</scroll-view>
</template>
<script setup>
import { reactive, inject, watch, ref, onMounted, watchEffect, nextTick } from 'vue';
let activeTab = ref(1)
let activeTitle = ref(1)
let jobList = ref([{},{},{},{},{},{}])
function back() {
uni.navigateBack({
delta: 1
})
}
function viewMore() {
// uni.navigateTo({
// url: '/pages/jobList/jobList'
// })
}
</script>
<style lang="less" scoped>
view{box-sizing: border-box;display: block;}
.container{
background-color: #F4F4F4;background-position: top center;background-size: 100% auto;
height: 100vh;
min-width: 100vw;
padding-bottom: 0;
background-repeat: no-repeat;
box-sizing: border-box;
}
.kinggang{
display: flex;
justify-content: space-around;
align-items: center;
padding: 16rpx 16rpx 32rpx 16rpx;
font-size: 24rpx;
background: #FFFFFF;
border-radius: 16rpx;
margin-bottom: 24rpx;
>view{
display: flex;
flex-direction: column;
align-items: center;
justify-content: space-around;
}
image{
width: 78rpx;
// margin-bottom: 15rpx;
height: 78rpx;
}
}
.tabs{
margin-bottom: 29rpx;
border-radius: 16rpx;
display: flex;
background: #fff;
color: #878787;
text-align: center;
width: 100%;
.tab{
width: 50%;
border: 4rpx solid transparent;
border-radius: 16rpx;
line-height: 64rpx;
position: relative;
&.active{
border: 4rpx solid #fff;
color: #fff;
background: linear-gradient(180deg, #79AFFF 1%, #A2B3FE 100%);
box-shadow: 0px 4rpx 10rpx 0px rgba(40, 102, 194, 0.4);
}
}
}
.titles{
display: flex;
margin-bottom: 44rpx;
.title-item{
font-size: 32rpx;
font-weight: bold;
color: #282828;
margin-right: 32rpx;
position: relative;
>view{
position: relative;
z-index: 2;
padding: 0 16rpx;
}
&.active::after{
content: '';
position: absolute;
z-index: 1;
bottom: -12rpx;
left: 0;
width: 120%;
height: 24rpx;
border-radius: 50px 0px 0px 50px;
background: linear-gradient(90deg, #78AEFF 0%, rgba(120, 174, 255, 0.31) 52%, rgba(24, 116, 255, 0) 100%);
}
}
}
.job-list{
width: 100%;
margin: 0 auto;
color: #333333;
border-radius: 24rpx;
background: #FFFFFF;
padding: 32rpx;
margin-bottom: 24rpx;
position: relative;
.sign{
position: absolute;
font-size: 24rpx;
right: 0;
top: 0;
padding: 4rpx 14rpx;
border: 1rpx solid #EC4827;
background: rgba(227, 79, 49, 0.09);
border-top-right-radius: 24rpx;
border-bottom-left-radius: 24rpx;
color: #EC4827;
}
.top-line{
display: flex;
justify-content: space-between;
font-size: 24rpx;
color: #A2A2A2;
margin-bottom: 16rpx;
.salary{
font-size: 32rpx;
color: #4C6EFB;
font-weight: bold;
}
}
.title{
font-size: 32rpx;
font-weight: bold;
color: #282828;
margin-bottom: 16rpx;
display: flex;
image{
width: 46rpx;
height: 46rpx;
margin-right: 11rpx;
}
}
.infos{
display: flex;
flex-wrap: wrap;
font-size: 24rpx;
margin-bottom: 16rpx;
line-height: 42rpx;
view{
padding: 0 16rpx;
margin-right: 10rpx;
background: #F2F2F2;
}
}
.bottom-line{
display: flex;
justify-content: space-between;
font-size: 24rpx;
color: #A2A2A2;
margin-top: 12rpx;
}
}
.view-more-btn{
padding: 10rpx 56rpx;
background: #FFFFFF;
color: #4C6EFB;
border: 1rpx solid #4C6EFB;
text-align: center;
border-radius: 40rpx;
width: fit-content;
margin: 0 auto;
margin-bottom: 20rpx;
}
</style>

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

BIN
packageRc/static/pageBg.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
packageRc/static/trace.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 989 B

BIN
packageRc/static/zcLeft.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -27,14 +27,28 @@
{
"path": "pages/careerfair/careerfair",
"style": {
"navigationBarTitleText": "招聘会",
"navigationStyle": "custom"
"navigationBarTitleText": "招聘会"
// "navigationStyle": "custom"
}
},
{
"path": "pages/login/login",
"path": "pages/complete-info/complete-info",
"style": {
"navigationBarTitleText": "AI+就业服务程序",
"navigationBarTitleText": "补全信息"
// "navigationStyle": "custom"
}
},
{
"path": "pages/complete-info/company-info",
"style": {
"navigationBarTitleText": "企业信息"
// "navigationStyle": "custom"
}
},
{
"path": "pages/complete-info/components/map-location-picker",
"style": {
"navigationBarTitleText": "选择地址",
"navigationStyle": "custom"
}
},
@@ -47,6 +61,20 @@
"navigationStyle": "custom"
}
},
{
"path": "pages/test/userTypeTest",
"style": {
"navigationBarTitleText": "用户类型测试",
"navigationStyle": "custom"
}
},
{
"path": "pages/job/publishJob",
"style": {
"navigationBarTitleText": "发布岗位",
"navigationStyle": "custom"
}
},
{
"path": "pages/chat/chat",
"style": {
@@ -65,20 +93,20 @@
"navigationBarTitleText": "",
"navigationStyle": "custom"
}
},
{
"path" : "packageA/pages/addWorkExperience/addWorkExperience",
"style" :
{
"navigationBarTitleText" : "添加工作经历",
"navigationStyle": "custom"
}
}
],
"subpackages": [{
"root": "packageA",
"pages": [{
"pages": [
{
"path" : "pages/addWorkExperience/addWorkExperience",
"style" :
{
"navigationBarTitleText" : "添加工作经历",
"navigationStyle": "custom"
}
},{
"path": "pages/choiceness/choiceness",
"style": {
"navigationBarTitleText": "精选",
@@ -227,17 +255,10 @@
]
}],
"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",
@@ -253,7 +274,8 @@
{
"pagePath": "pages/chat/chat",
"iconPath": "static/tabbar/logo3.png",
"selectedIconPath": "static/tabbar/logo3.png"
"selectedIconPath": "static/tabbar/logo3.png",
"text": "AI+"
},
{
"pagePath": "pages/msglog/msglog",
@@ -273,12 +295,18 @@
"navigationBarTextStyle": "black",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
// "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"
}
},
"uniIdRouter": {}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,900 @@
<template>
<AppLayout title="企业信息">
<view class="company-info-container">
<!-- 头部信息 -->
<view class="header-info">
<view class="progress-text">企业信息完善度</view>
<view class="progress-value">{{ completionPercentage }}%</view>
</view>
<!-- 表单内容 -->
<view class="form-content">
<!-- 企业名称 -->
<view class="form-item">
<view class="label">企业名称</view>
<input
class="input-field"
v-model="formData.companyName"
placeholder="请输入企业名称"
@input="updateCompletion"
/>
</view>
<!-- 统一社会信用代码 -->
<view class="form-item">
<view class="label">统一社会信用代码</view>
<input
class="input-field"
v-model="formData.socialCreditCode"
placeholder="请输入统一社会信用代码"
maxlength="18"
@input="updateCompletion"
/>
</view>
<!-- 企业注册地点 -->
<view class="form-item clickable" @click="selectLocation">
<view class="label">企业注册地点</view>
<view class="input-content">
<text class="input-text" :class="{ placeholder: !formData.registeredAddress }">
{{ formData.registeredAddress || '请选择注册地点' }}
</text>
<uni-icons type="arrowright" size="16" color="#999"></uni-icons>
</view>
</view>
<!-- 企业信息介绍 -->
<view class="form-item clickable" @click="editCompanyIntro">
<view class="label">企业信息介绍</view>
<view class="input-content">
<text class="input-text intro-text" :class="{ placeholder: !formData.companyIntro }">
{{ formData.companyIntro || '请输入企业介绍' }}
</text>
<uni-icons type="arrowright" size="16" color="#999"></uni-icons>
</view>
</view>
<!-- 企业法人姓名 -->
<view class="form-item">
<view class="label">企业法人姓名</view>
<input
class="input-field"
v-model="formData.legalPersonName"
placeholder="请输入法人姓名"
@input="updateCompletion"
/>
</view>
<!-- 企业类型 -->
<view class="form-item clickable" @click="selectEnterpriseType">
<view class="label">企业类型</view>
<view class="input-content">
<input class="input-con" v-model="formData.natureText" disabled placeholder="请选择企业类型" />
<uni-icons type="arrowright" size="16" color="#999"></uni-icons>
</view>
</view>
<!-- 是否是就业见习基地 -->
<view class="form-item clickable" @click="selectEmploymentBase">
<view class="label">是否是就业见习基地</view>
<view class="input-content">
<text class="input-text" :class="{ placeholder: formData.enterpriseType === null }">
{{ formData.enterpriseType === null ? '请选择' : (formData.enterpriseType ? '是' : '否') }}
</text>
<uni-icons type="arrowright" size="16" color="#999"></uni-icons>
</view>
</view>
<!-- 法人身份证号 -->
<view class="form-item">
<view class="label">法人身份证号</view>
<input
class="input-field"
v-model="formData.legalIdCard"
placeholder="请输入法人身份证号"
maxlength="18"
@input="updateCompletion"
/>
</view>
<!-- 法人联系方式 -->
<view class="form-item">
<view class="label">法人联系方式</view>
<input
class="input-field"
v-model="formData.legalPhone"
placeholder="请输入法人联系方式"
maxlength="11"
@input="updateCompletion"
/>
</view>
<!-- 本地重点发展产业 -->
<view class="form-item clickable" @click="selectIndustry">
<view class="label">本地重点发展产业</view>
<view class="input-content">
<text class="input-text" :class="{ placeholder: !formData.industryType }">
{{ formData.industryType || '请选择产业类型' }}
</text>
<uni-icons type="arrowright" size="16" color="#999"></uni-icons>
</view>
</view>
<!-- 是否是本地企业 -->
<view class="form-item clickable" @click="selectLocalCompany">
<view class="label">是否是本地重点发展产业</view>
<view class="input-content">
<text class="input-text" :class="{ placeholder: formData.isLocalCompany === null }">
{{ formData.isLocalCompany === null ? '请选择' : (formData.isLocalCompany ? '是' : '否') }}
</text>
<uni-icons type="arrowright" size="16" color="#999"></uni-icons>
</view>
</view>
<!-- 联系人信息列表 -->
<view class="contact-section">
<view class="section-title">联系人信息</view>
<!-- 每个联系人作为一个分组 -->
<view
class="contact-group"
v-for="(contact, index) in formData.companyContactList"
:key="index"
>
<view class="group-header">
<text>联系人{{ index + 1 }}</text>
<view
class="delete-btn"
@click="deleteContact(index)"
v-if="formData.companyContactList.length > 1"
>
<uni-icons type="trash" size="16" color="#ff4757"></uni-icons>
<text class="delete-text">删除</text>
</view>
</view>
<view class="form-item">
<view class="label">联系人姓名</view>
<input
class="input-field"
v-model="contact.contactPerson"
placeholder="请输入联系人姓名"
@input="updateCompletion"
/>
</view>
<view class="form-item">
<view class="label">联系人电话</view>
<input
class="input-field"
v-model="contact.contactPersonPhone"
placeholder="请输入联系人电话"
@input="updateCompletion"
/>
</view>
</view>
</view>
<!-- 添加联系人按钮 -->
<view class="add-contact-btn" @click="addContact" v-if="formData.companyContactList.length < 3">
<uni-icons type="plus" size="20" color="#256BFA"></uni-icons>
<text>添加联系人</text>
</view>
</view>
<!-- 底部按钮 -->
<view class="bottom-actions">
<button class="cancel-btn" @click="cancel">取消</button>
<button class="confirm-btn" @click="confirm">确认</button>
</view>
</view>
<!-- 弹窗组件 -->
<uni-popup ref="popup" type="center">
<view class="popup-content">
<view class="popup-title">{{ popupTitle }}</view>
<input
v-if="popupType === 'text'"
class="popup-input"
v-model="popupValue"
:placeholder="popupPlaceholder"
/>
<textarea
v-if="popupType === 'textarea'"
class="popup-textarea"
v-model="popupValue"
:placeholder="popupPlaceholder"
maxlength="500"
></textarea>
<view class="popup-actions">
<button class="popup-cancel" @click="closePopup">取消</button>
<button class="popup-confirm" @click="confirmPopup">确定</button>
</view>
</view>
</uni-popup>
<!-- 地址选择器 -->
<area-cascade-picker ref="areaPicker"></area-cascade-picker>
<!-- 滚动选择器 -->
<SelectPopup ref="selectPopupRef"></SelectPopup>
</AppLayout>
</template>
<script setup>
import { ref, reactive, computed, inject } from 'vue'
import { onLoad } from '@dcloudio/uni-app'
import AreaCascadePicker from '@/components/area-cascade-picker/area-cascade-picker.vue'
import SelectPopup from '@/components/selectPopup/selectPopup.vue'
import useDictStore from '@/stores/useDictStore'
const { $api } = inject('globalFunction')
const dictStore = useDictStore()
// 表单数据
const formData = reactive({
companyName: '',
socialCreditCode: '',
registeredAddress: '',
registeredAddressName: '',
longitude: null,
latitude: null,
companyIntro: '',
legalPersonName: '',
nature: '', // 企业类型
natureText: '', // 企业类型显示文本
enterpriseType: null, // 是否是就业见习基地 (true/false/null)
legalIdCard: '', // 法人身份证号
legalPhone: '', // 法人联系方式
industryType: '', // 是否是本地重点发展产业
isLocalCompany: null, // 是否是本地企业 (true/false/null)
companyContactList: [
{ contactPerson: '', contactPersonPhone: '' }
]
})
// 弹窗相关
const popup = ref(null)
const popupTitle = ref('')
const popupType = ref('text') // text | textarea
const popupValue = ref('')
const popupPlaceholder = ref('')
const currentEditField = ref('')
// 地址选择器引用
const areaPicker = ref(null)
// 滚动选择器引用
const selectPopupRef = ref(null)
// 创建本地的 openSelectPopup 函数
const openSelectPopup = (config) => {
if (selectPopupRef.value) {
selectPopupRef.value.open(config);
}
}
// 产业类型选项数据
const industryOptions = [
'人工智能',
'生物医药',
'新能源',
'高端装备制造',
'其他'
]
// 备用企业类型选项(当字典数据加载失败时使用)
const fallbackEnterpriseTypes = [
{ label: '有限责任公司', value: '1' },
{ label: '股份有限公司', value: '2' },
{ label: '个人独资企业', value: '3' },
{ label: '合伙企业', value: '4' },
{ label: '外商投资企业', value: '5' },
{ label: '其他', value: '6' }
]
// 企业类型选项数据从字典中获取
const enterpriseTypeOptions = computed(() => {
const natureData = dictStore.state?.nature || []
console.log('企业类型选项数据:', natureData)
return natureData
})
// 完成度计算
const completionPercentage = computed(() => {
const fields = [
formData.companyName,
formData.socialCreditCode,
formData.registeredAddress,
formData.companyIntro,
formData.legalPersonName,
formData.nature,
formData.enterpriseType !== null ? 'filled' : '',
formData.legalIdCard,
formData.legalPhone,
formData.industryType,
formData.isLocalCompany !== null ? 'filled' : ''
]
// 检查联系人信息
const hasContact = formData.companyContactList.some(contact => contact.contactPerson && contact.contactPersonPhone)
const filledFields = fields.filter(field => field && field.trim()).length + (hasContact ? 1 : 0)
const totalFields = fields.length + 1
return Math.round((filledFields / totalFields) * 100)
})
// 更新完成度
const updateCompletion = () => {
// 完成度会自动通过computed更新
}
// 选择注册地点
const selectLocation = () => {
// 打开五级联动地址选择器
areaPicker.value?.open({
title: '选择企业注册地点',
maskClick: true,
success: (addressData) => {
// addressData 包含: address, province, city, district, street, community
formData.registeredAddress = addressData.address
formData.registeredAddressName = addressData.address
// 可以保存详细的地址信息
formData.provinceCode = addressData.province?.code
formData.provinceName = addressData.province?.name
formData.cityCode = addressData.city?.code
formData.cityName = addressData.city?.name
formData.districtCode = addressData.district?.code
formData.districtName = addressData.district?.name
formData.streetCode = addressData.street?.code
formData.streetName = addressData.street?.name
formData.communityCode = addressData.community?.code
formData.communityName = addressData.community?.name
updateCompletion()
$api.msg('地址选择成功')
}
})
}
// 处理地图选择返回的地址
const handleLocationSelected = (locationData) => {
formData.registeredAddress = locationData.address
formData.registeredAddressName = locationData.name
formData.longitude = locationData.longitude
formData.latitude = locationData.latitude
updateCompletion()
}
// 编辑企业介绍
const editCompanyIntro = () => {
currentEditField.value = 'companyIntro'
popupTitle.value = '企业信息介绍'
popupType.value = 'textarea'
popupValue.value = formData.companyIntro
popupPlaceholder.value = '请输入企业介绍'
popup.value?.open()
}
// 选择产业类型
const selectIndustry = () => {
uni.showActionSheet({
itemList: industryOptions,
success: (res) => {
formData.industryType = industryOptions[res.tapIndex]
updateCompletion()
$api.msg('产业类型选择成功')
}
})
}
// 选择企业类型
const selectEnterpriseType = () => {
console.log('点击企业类型,当前数据:', enterpriseTypeOptions.value)
console.log('字典store状态:', dictStore.state.nature)
// 获取企业类型选项,优先使用字典数据,失败时使用备用数据
let options = enterpriseTypeOptions.value
if (!options || !options.length) {
console.log('企业类型数据为空,尝试重新加载')
// 尝试重新加载字典数据
dictStore.getDictData().then(() => {
if (dictStore.state.nature && dictStore.state.nature.length > 0) {
selectEnterpriseType() // 递归调用
} else {
// 使用备用数据
console.log('使用备用企业类型数据')
options = fallbackEnterpriseTypes
showEnterpriseTypeSelector(options)
}
}).catch(() => {
// 使用备用数据
console.log('字典加载失败,使用备用企业类型数据')
options = fallbackEnterpriseTypes
showEnterpriseTypeSelector(options)
})
return
}
showEnterpriseTypeSelector(options)
}
// 显示企业类型选择器
const showEnterpriseTypeSelector = (options) => {
console.log('企业类型选项列表:', options)
openSelectPopup({
title: '企业类型',
maskClick: true,
data: [options],
success: (_, [value]) => {
console.log('选择的企业类型:', value)
formData.nature = value.value
formData.natureText = value.label
updateCompletion()
$api.msg('企业类型选择成功')
}
})
}
// 选择是否是就业见习基地
const selectEmploymentBase = () => {
uni.showActionSheet({
itemList: ['是', '否'],
success: (res) => {
formData.enterpriseType = res.tapIndex === 0
updateCompletion()
$api.msg('选择成功')
}
})
}
// 选择是否是本地企业
const selectLocalCompany = () => {
uni.showActionSheet({
itemList: ['是', '否'],
success: (res) => {
formData.isLocalCompany = res.tapIndex === 0
updateCompletion()
$api.msg('选择成功')
}
})
}
// 添加联系人
const addContact = () => {
if (formData.companyContactList.length < 3) {
formData.companyContactList.push({ contactPerson: '', contactPersonPhone: '' })
}
}
// 删除联系人
const deleteContact = (index) => {
if (formData.companyContactList.length <= 1) {
$api.msg('至少需要保留一个联系人')
return
}
uni.showModal({
title: '确认删除',
content: '确定要删除这个联系人吗?',
success: (res) => {
if (res.confirm) {
formData.companyContactList.splice(index, 1)
updateCompletion()
$api.msg('联系人已删除')
}
}
})
}
// 关闭弹窗
const closePopup = () => {
popup.value?.close()
popupValue.value = ''
}
// 确认弹窗
const confirmPopup = () => {
const field = currentEditField.value
if (field === 'companyIntro') {
formData.companyIntro = popupValue.value
}
updateCompletion()
closePopup()
}
// 取消
const cancel = () => {
uni.navigateBack()
}
// 确认提交
const confirm = () => {
// 验证必填字段
if (!formData.companyName.trim()) {
$api.msg('请输入企业名称')
return
}
if (!formData.socialCreditCode.trim()) {
$api.msg('请输入统一社会信用代码')
return
}
if (!formData.registeredAddress.trim()) {
$api.msg('请选择注册地点')
return
}
if (!formData.companyIntro.trim()) {
$api.msg('请输入企业介绍')
return
}
if (!formData.legalPersonName.trim()) {
$api.msg('请输入法人姓名')
return
}
if (!formData.nature.trim()) {
$api.msg('请选择企业类型')
return
}
if (formData.enterpriseType === null) {
$api.msg('请选择是否是就业见习基地')
return
}
if (!formData.legalIdCard.trim()) {
$api.msg('请输入法人身份证号')
return
}
if (!formData.legalPhone.trim()) {
$api.msg('请输入法人联系方式')
return
}
if (!formData.industryType.trim()) {
$api.msg('请选择产业类型')
return
}
if (formData.isLocalCompany === null) {
$api.msg('请选择是否是本地企业')
return
}
// 验证身份证号格式
const idCardRegex = /^[1-9]\d{5}(18|19|20)\d{2}((0[1-9])|(1[0-2]))(([0-2][1-9])|10|20|30|31)\d{3}[0-9Xx]$/
if (!idCardRegex.test(formData.legalIdCard)) {
$api.msg('请输入正确的身份证号')
return
}
// 验证法人电话格式
const phoneRegex = /^1[3-9]\d{9}$/
if (!phoneRegex.test(formData.legalPhone)) {
$api.msg('请输入正确的法人联系方式')
return
}
// 验证至少有一个联系人
const hasValidContact = formData.companyContactList.some(contact =>
contact.contactPerson.trim() && contact.contactPersonPhone.trim()
)
if (!hasValidContact) {
$api.msg('请至少添加一个联系人信息')
return
}
// 验证联系人电话格式
for (let contact of formData.companyContactList) {
if (contact.contactPerson.trim() && contact.contactPersonPhone.trim()) {
if (!phoneRegex.test(contact.contactPersonPhone)) {
$api.msg('请输入正确的手机号码')
return
}
}
}
// 提交数据
uni.showLoading({ title: '保存中...' })
// 构建提交数据,按照要求的字段映射
const companyData = {
name: formData.companyName,
code: formData.socialCreditCode,
registeredAddress: formData.registeredAddress,
description: formData.companyIntro,
legalPerson: formData.legalPersonName,
nature: formData.nature,
enterpriseType: formData.enterpriseType,
legalIdCard: formData.legalIdCard,
legalPhone: formData.legalPhone,
industryType: formData.industryType,
isLocalCompany: formData.isLocalCompany,
companyContactList: formData.companyContactList.filter(contact => contact.contactPerson.trim() && contact.contactPersonPhone.trim())
}
// 调用新的接口地址数据格式为company数组
const submitData = {
company: companyData
}
$api.createRequest('/app/user/registerUser', submitData, 'post')
.then((resData) => {
uni.hideLoading()
$api.msg('企业信息保存成功')
// 跳转到首页或企业相关页面
uni.reLaunch({
url: '/pages/index/index'
})
})
.catch((err) => {
uni.hideLoading()
$api.msg(err.msg || '保存失败,请重试')
})
}
onLoad(async (options) => {
console.log('企业信息补全页面开始加载')
try {
// 初始化字典数据
await dictStore.getDictData()
console.log('字典数据加载完成:', {
nature: dictStore.state.nature,
complete: dictStore.complete
})
} catch (error) {
console.error('字典数据加载失败:', error)
$api.msg('数据加载失败,请重试')
}
console.log('企业信息补全页面加载完成')
})
// 暴露方法给其他页面调用
defineExpose({
handleLocationSelected
})
</script>
<style lang="stylus" scoped>
.company-info-container
min-height: 100vh
background: #f5f5f5
padding-bottom: 120rpx
.header-info
background: #fff
padding: 40rpx 32rpx
display: flex
justify-content: space-between
align-items: center
margin-bottom: 20rpx
.progress-text
font-size: 32rpx
color: #000000
font-weight: 500
.progress-value
font-size: 32rpx
color: #256BFA
font-weight: 600
.form-content
background: #fff
margin-bottom: 20rpx
.form-item
padding: 32rpx
border-bottom: 1rpx solid #f0f0f0
display: flex
justify-content: space-between
align-items: center
&:last-child
border-bottom: none
&.clickable
cursor: pointer
.label
font-size: 28rpx
color: #000000
min-width: 200rpx
.input-field
flex: 1
font-size: 28rpx
color: #333
text-align: right
&::placeholder
color: #999
font-size: 26rpx
.input-con
flex: 1
font-size: 28rpx
color: #333
text-align: right
background: transparent
border: none
outline: none
&::placeholder
color: #999
font-size: 26rpx
.input-content
flex: 1
display: flex
justify-content: space-between
align-items: center
.input-text
font-size: 28rpx
color: #333
flex: 1
text-align: right
&.placeholder
color: #999
font-size: 26rpx
&.intro-text
max-height: 80rpx
overflow: hidden
text-overflow: ellipsis
white-space: nowrap
.contact-section
margin-top: 20rpx
.section-title
padding: 32rpx
font-size: 32rpx
color: #333
font-weight: 500
background: #fff
border-bottom: 1rpx solid #f0f0f0
.contact-group
background: #fff
margin-bottom: 20rpx
border-radius: 16rpx
overflow: hidden
.group-header
padding: 24rpx 32rpx
font-size: 28rpx
color: #000000
background: #f8f9fa
font-weight: 500
border-bottom: 1rpx solid #e8e8e8
display: flex
justify-content: space-between
align-items: center
.delete-btn
display: flex
align-items: center
padding: 8rpx 16rpx
background: #fff5f5
border: 1rpx solid #ff4757
border-radius: 8rpx
cursor: pointer
.delete-text
margin-left: 8rpx
font-size: 24rpx
color: #ff4757
.form-item
border-bottom: 1rpx solid #f0f0f0
&:last-child
border-bottom: none
.add-contact-btn
padding: 32rpx
display: flex
align-items: center
justify-content: center
color: #256BFA
font-size: 28rpx
background: #fff
border-top: 1rpx solid #f0f0f0
text
margin-left: 12rpx
.bottom-actions
position: fixed
bottom: 0
left: 0
right: 0
background: #fff
padding: 20rpx 32rpx
display: flex
gap: 20rpx
box-shadow: 0 -2rpx 10rpx rgba(0,0,0,0.1)
.cancel-btn, .confirm-btn
flex: 1
height: 80rpx
border-radius: 40rpx
font-size: 32rpx
border: none
.cancel-btn
background: #f5f5f5
color: #666
.confirm-btn
background: #256BFA
color: #fff
// 弹窗样式
.popup-content
width: 600rpx
background: #fff
border-radius: 20rpx
padding: 40rpx
.popup-title
font-size: 36rpx
color: #333
font-weight: 500
margin-bottom: 30rpx
text-align: center
.popup-input, .popup-textarea
width: 100%
padding: 20rpx
border: 1rpx solid #e0e0e0
border-radius: 10rpx
font-size: 28rpx
margin-bottom: 30rpx
box-sizing: border-box
text-align: center
.popup-textarea
height: 200rpx
.popup-actions
display: flex
gap: 20rpx
.popup-cancel, .popup-confirm
flex: 1
height: 70rpx
border-radius: 35rpx
font-size: 28rpx
border: none
.popup-cancel
background: #f5f5f5
color: #666
.popup-confirm
background: #256BFA
color: #fff
// 按钮重置样式
button::after
border: none
</style>

View File

@@ -1,7 +1,16 @@
<template>
<AppLayout title="AI+就业服务程序">
<tabcontrolVue :current="tabCurrent">
<template v-slot:tab0>
<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>
@@ -10,8 +19,8 @@
<button class="wxlogin" @click="loginTest">内测登录</button>
<view class="wxaddress">{{ config.appInfo.areaName }}公共就业和人才服务中心</view>
</view>
</template>
<template v-slot:tab1>
</swiper-item>
<swiper-item @touchmove.stop="false">
<view class="content-one">
<view>
<view class="content-title">
@@ -56,15 +65,23 @@
<view class="input-titile">学历</view>
<input class="input-con" v-model="state.educationText" disabled placeholder="本科" />
</view>
<view class="content-input">
<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
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>
</template>
<template v-slot:tab2>
</swiper-item>
<swiper-item @touchmove.stop="false">
<view class="content-one">
<view>
<view class="content-title">
@@ -110,15 +127,18 @@
</view>
<view class="next-btn" @tap="complete">开启求职之旅</view>
</view>
</template>
</tabcontrolVue>
</swiper-item>
</swiper>
</view>
</view>
<SelectJobs ref="selectJobsModel"></SelectJobs>
<SelectPopup ref="selectPopupRef"></SelectPopup>
</AppLayout>
</template>
<script setup>
import tabcontrolVue from './components/tabcontrol.vue';
import SelectJobs from '@/components/selectJobs/selectJobs.vue';
import SelectPopup from '@/components/selectPopup/selectPopup.vue';
import { reactive, inject, watch, ref, onMounted } from 'vue';
import { onLoad, onShow } from '@dcloudio/uni-app';
import useUserStore from '@/stores/useUserStore';
@@ -126,11 +146,30 @@ import useDictStore from '@/stores/useDictStore';
const { $api, navTo, config, IdCardValidator } = inject('globalFunction');
const { loginSetToken, getUserResume } = useUserStore();
const { getDictSelectOption, oneDictData } = useDictStore();
const openSelectPopup = inject('openSelectPopup');
// console.log(config.appInfo.areaName);
// #ifdef H5
const injectedOpenSelectPopup = inject('openSelectPopup', null);
// #endif
// status
const selectJobsModel = ref();
const tabCurrent = ref(0);
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: [],
@@ -154,6 +193,9 @@ const fromValue = reactive({
idcard: '',
});
//
const idCardError = ref('');
onLoad((parmas) => {
getTreeselect();
});
@@ -163,6 +205,26 @@ onMounted(() => {});
function changeSex(sex) {
fromValue.sex = sex;
}
//
function validateIdCard() {
const idCard = fromValue.idcard.trim();
//
if (!idCard) {
idCardError.value = '';
return;
}
// 使
const result = IdCardValidator.validate(idCard);
if (result.valid) {
idCardError.value = '';
} else {
idCardError.value = result.message;
}
}
function changeExperience() {
openSelectPopup({
title: '工作经验',
@@ -239,6 +301,19 @@ function changeJobs() {
}
function nextStep() {
//
const idCard = fromValue.idcard.trim();
if (!idCard) {
$api.msg('请输入身份证号码');
return;
}
const result = IdCardValidator.validate(idCard);
if (!result.valid) {
$api.msg(result.message);
return;
}
tabCurrent.value += 1;
}
@@ -304,6 +379,23 @@ function complete() {
</script>
<style lang="stylus" scoped>
.tab-container
height: 100%
width: 100%
display: flex
align-items: center
justify-content: center
flex-direction: row
.uni-margin-wrap
width: 100%
height: 100%
.swiper
width: 100%
height: 100%
.swiper-item
display: block;
width: 100%
height: 100%
.input-nx
position: relative
border-bottom: 2rpx solid #EBEBEB
@@ -451,6 +543,19 @@ function complete() {
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
@@ -488,3 +593,4 @@ function complete() {
text-align: center;
line-height: 90rpx
</style>

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

@@ -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

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

View File

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

View File

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

View File

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

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

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

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

57
static/iconfont/README.md Normal file
View File

@@ -0,0 +1,57 @@
# 阿里图标库文件说明
## 📁 目录结构
将从阿里图标库下载的文件放在这个目录下:
```
static/iconfont/
├── iconfont.css ← CSS 样式文件
├── iconfont.ttf ← 字体文件TrueType
├── iconfont.woff ← 字体文件Web Open Font Format
├── iconfont.woff2 ← 字体文件Web Open Font Format 2.0,推荐)
└── README.md ← 本说明文件
```
## 📝 使用步骤
### 1. 下载图标文件
1. 访问 https://www.iconfont.cn/
2. 登录并选择/创建项目
3. 添加需要的图标
4. 点击"下载至本地"
### 2. 复制文件
将下载包中的以下文件复制到本目录:
- `iconfont.css`
- `iconfont.ttf`
- `iconfont.woff`
- `iconfont.woff2`
### 3. 修改 CSS 路径
打开 `iconfont.css`,将字体文件路径修改为:
```css
@font-face {
font-family: "iconfont";
src: url('./iconfont.woff2') format('woff2'),
url('./iconfont.woff') format('woff'),
url('./iconfont.ttf') format('truetype');
}
```
### 4. 在 App.vue 中引入
已在 `App.vue` 中自动引入,无需额外操作。
## ⚠️ 注意事项
1. **必须使用本地文件**:微信小程序不支持在线字体链接
2. **路径必须正确**:确保字体文件路径指向正确位置
3. **文件完整性**:确保所有字体文件都已复制
4. **版本更新**:更新图标时替换所有文件
## 🔗 相关链接
- [阿里图标库](https://www.iconfont.cn/)
- [使用文档](../../docs/阿里图标库引入指南.md)

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,372 @@
<!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">&#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=1760941852498') format('woff2'),
url('iconfont.woff?t=1760941852498') format('woff'),
url('iconfont.ttf?t=1760941852498') 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-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-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,47 @@
@font-face {
font-family: "iconfont"; /* Project id 5044714 */
src: url('iconfont.woff2?t=1760941852498') format('woff2'),
url('iconfont.woff?t=1760941852498') format('woff'),
url('iconfont.ttf?t=1760941852498') format('truetype');
}
.iconfont {
font-family: "iconfont" !important;
font-size: 16px;
font-style: normal;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.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";
}

View File

@@ -0,0 +1,65 @@
{
"id": "5044714",
"name": "喀什APP",
"font_family": "iconfont",
"css_prefix_text": "icon-",
"description": "",
"glyphs": [
{
"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.

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

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