30 Commits

Author SHA1 Message Date
a8a9d792a3 Merge remote-tracking branch 'origin/main' 2026-02-02 13:22:25 +08:00
francis_fh
7634a24c3a 细节优化 2026-01-28 17:17:29 +08:00
francis_fh
a128926c21 首页九宫格样式优化 2026-01-28 11:44:11 +08:00
francis_fh
3387ea8dbc 首页上下滑动优化 2026-01-27 18:51:58 +08:00
francis_fh
05105488bd tabbar大小调整 2026-01-27 17:13:45 +08:00
francis_fh
910862cb5f 分享功能开发 2026-01-27 17:11:35 +08:00
francis_fh
fb88fdbb89 分享功能开发 2026-01-27 17:10:58 +08:00
francis_fh
1de0bacebf 1 2026-01-27 16:13:28 +08:00
francis_fh
95a23be4bf 岗位详情页面弹窗优化 2026-01-27 16:05:00 +08:00
francis_fh
40eb1b29e8 取消投递列表页面开发 2026-01-27 15:12:05 +08:00
francis_fh
dbdb330189 投递简历添加确认弹窗 2026-01-27 13:58:46 +08:00
francis_fh
e0f8faf757 Merge branch 'main' of http://124.243.245.42:3000/sdz/ks-app-employment-service 2026-01-27 11:04:32 +08:00
francis_fh
951905fd09 发布岗位新增字段 2026-01-27 11:04:30 +08:00
bde805e905 Merge branch 'main' of http://124.243.245.42:3000/sdz/ks-app-employment-service 2026-01-27 09:30:39 +08:00
3f714d4a69 首页岗位/政策左右结构 2026-01-27 09:27:55 +08:00
xuchao
360d4f96ea 首页技能培训入口名称改为技能课堂 2026-01-26 10:35:25 +08:00
francis_fh
a3bc821bc3 流式数据请求优化。 2026-01-24 18:29:47 +08:00
francis_fh
7a7aa33128 同时播放聊条音频的问题修复 2026-01-24 17:57:25 +08:00
francis_fh
cbf8bd7c41 H5卡片样式修复 2026-01-24 16:51:32 +08:00
francis_fh
07e0f3083b AI回复内容,岗位卡片的点击事件绑定问题解决 2026-01-24 16:47:11 +08:00
francis_fh
c4c6cea579 语音射别修复 2026-01-23 22:01:38 +08:00
c3d26cdd54 = 职业规划推荐 2026-01-23 18:46:13 +08:00
b030d45d49 = 职业规划推荐 2026-01-23 18:38:13 +08:00
a45b247496 = 职业规划推荐 2026-01-23 15:43:53 +08:00
francis_fh
ebb6bc6e33 bug修复 2026-01-23 13:10:40 +08:00
francis_fh
7bfc765a73 Revert "音频问题解决"
This reverts commit 34cad2543d.
2026-01-23 12:36:27 +08:00
francis_fh
34cad2543d 音频问题解决 2026-01-23 12:34:04 +08:00
francis_fh
134c900946 音频bug修复 2026-01-23 12:12:51 +08:00
francis_fh
5bf94a6223 Merge branch 'main' of http://124.243.245.42:3000/sdz/ks-app-employment-service 2026-01-22 18:58:20 +08:00
francis_fh
b92e3b8adb AI模块联调 2026-01-22 18:58:19 +08:00
43 changed files with 2840 additions and 1067 deletions

View File

@@ -7,9 +7,19 @@
import request from '@/utilsRc/request'
// 获取技能信息
export function getSkill(query) {
export function getCareerPath(query) {
return request({
url: '/jobSkillDet/getJobSkillWeight',
url: '/jobPath/getJobPathJobList',
method: 'get',
params: query,
baseUrlType: 'zytp'
})
}
// 获取技能信息
export function getSkillResult(query) {
return request({
url: '/jobDimScore/getJobDimScoreList',
method: 'get',
params: query,
baseUrlType: 'zytp'

View File

@@ -72,18 +72,18 @@ export const navTo = function(url, {
const pages = getCurrentPages();
if (pages.length >= 10) {
uni.redirectTo({
url: '/pages/complete-info/complete-info',
url: '/packageA/pages/complete-info/complete-info',
fail: (err) => {
console.error('页面跳转失败:', err);
}
});
} else {
uni.navigateTo({
url: '/pages/complete-info/complete-info',
url: '/packageA/pages/complete-info/complete-info',
fail: (err) => {
console.error('页面跳转失败:', err);
uni.redirectTo({
url: '/pages/complete-info/complete-info',
url: '/packageA/pages/complete-info/complete-info',
fail: (err2) => {
console.error('redirectTo也失败:', err2);
}

View File

@@ -59,7 +59,7 @@ const generateTabbarList = () => {
},
{
id: 2,
text: 'AI+',
text: '智能客服',
path: '/pages/chat/chat',
iconPath: '/static/tabbar/logo3.png',
selectedIconPath: '/static/tabbar/logo3.png',
@@ -104,7 +104,7 @@ const generateTabbarList = () => {
baseItems.splice(1, 0, {
id: 1,
text: '发布岗位',
path: '/pages/job/publishJob',
path: '/packageA/pages/job/publishJob',
iconPath: '/static/tabbar/post.png',
selectedIconPath: '/static/tabbar/posted.png',
centerItem: false,
@@ -173,9 +173,10 @@ const switchTab = (item, index) => {
// 检查是否为需要登录的页面
const loginRequiredPages = [
'/pages/job/publishJob',
'/packageA/pages/job/publishJob',
'/pages/mine/mine',
'/pages/mine/company-mine'
'/pages/mine/company-mine',
'/pages/msglog/msglog'
];
if (loginRequiredPages.includes(item.path)) {
@@ -190,7 +191,7 @@ const switchTab = (item, index) => {
}
// 已登录,处理特定页面的逻辑
if (item.path === '/pages/job/publishJob') {
if (item.path === '/packageA/pages/job/publishJob') {
// 检查企业信息是否完整
const cachedUserInfo = uni.getStorageSync('userInfo') || {};
const storeUserInfo = userInfo.value || {};
@@ -200,12 +201,12 @@ const switchTab = (item, index) => {
if (!currentUserInfo.company || currentUserInfo.company === null) {
// 企业信息为空,跳转到企业信息补全页面
uni.navigateTo({
url: '/pages/complete-info/company-info',
url: '/packageA/pages/complete-info/company-info',
});
} else {
// 企业信息完整,跳转到发布岗位页面
uni.navigateTo({
url: '/pages/job/publishJob',
url: '/packageA/pages/job/publishJob',
});
}
@@ -279,7 +280,7 @@ onMounted(() => {
bottom: 0;
left: 0;
right: 0;
height: 88rpx;
height: 100rpx;
background-color: #ffffff;
border-top: 1rpx solid #e5e5e5;
display: flex;
@@ -306,8 +307,8 @@ onMounted(() => {
}
.tabbar-icon {
width: 44rpx;
height: 44rpx;
width: 50rpx;
height: 50rpx;
margin-bottom: 4rpx;
position: relative;
}
@@ -318,7 +319,7 @@ onMounted(() => {
}
.tabbar-text {
font-size: 20rpx;
font-size: 22rpx;
line-height: 1;
transition: color 0.3s ease;
}
@@ -332,14 +333,14 @@ onMounted(() => {
position: absolute;
top: 4rpx;
right: 20rpx;
min-width: 30rpx;
height: 30rpx;
min-width: 32rpx;
height: 32rpx;
background-color: #ff4444;
color: #fff;
font-size: 18rpx;
border-radius: 15rpx;
font-size: 19rpx;
border-radius: 16rpx;
text-align: center;
line-height: 30rpx;
line-height: 32rpx;
padding: 0 10rpx;
transform: scale(0.8);
}
@@ -347,8 +348,8 @@ onMounted(() => {
/* 中间按钮特殊样式 */
.tabbar-item:has(.center-item) {
.tabbar-icon {
width: 60rpx;
height: 60rpx;
width: 68rpx;
height: 68rpx;
margin-bottom: 0;
}
}

View File

@@ -2,17 +2,44 @@
<view class="markdown-body">
<!-- 根据不同平台使用不同的渲染方式 -->
<!-- #ifdef MP-WEIXIN -->
<rich-text class="markdownRich" id="markdown-content" :nodes="renderedHtml" @itemclick="handleItemClick" />
<!-- 微信小程序端使用v-for遍历岗位卡片列表每个卡片单独渲染支持点击事件 -->
<scroll-view class="markdownRich" id="markdown-content">
<!-- 渲染普通markdown内容 -->
<rich-text :nodes="renderedHtml" />
<!-- 单独渲染岗位卡片支持点击事件 -->
<view v-if="localJobCardsList.length > 0" class="job-cards-container">
<view
v-for="(card, index) in localJobCardsList"
:key="index"
class="custom-card"
@tap="navigateToJobDetail(card.jobId)"
>
<view class="card-title">
<text class="title-text">{{ card.jobTitle }}</text>
<view class="card-salary">{{ card.salary }}</view>
</view>
<view class="card-company">{{ card.location }}·{{ card.companyName }}</view>
<view class="card-info">
<view class="info-item">
<view class="card-tag">{{ card.education }}</view>
<view class="card-tag">{{ card.experience }}</view>
</view>
<view class="info-item">查看详情<view class="position-nav"></view></view>
</view>
</view>
</view>
</scroll-view>
<!-- #endif -->
<!-- #ifndef MP-WEIXIN -->
<view class="markdown-body" v-html="renderedHtml"></view>
<view class="markdown-body" v-html="renderedHtml" @click="handleH5Click"></view>
<!-- #endif -->
</view>
</template>
<script setup>
import { computed, onMounted, inject } from 'vue';
import { parseMarkdown, codeDataList } from '@/utils/markdownParser';
import { computed, onMounted, inject, ref, watch, nextTick } from 'vue';
import { parseMarkdown, codeDataList, jobCardsList } from '@/utils/markdownParser';
const { navTo } = inject('globalFunction');
const props = defineProps({
content: {
@@ -26,30 +53,191 @@ const props = defineProps({
});
const renderedHtml = computed(() => parseMarkdown(props.content));
const markdownContainer = ref(null);
// 响应式岗位卡片列表,用于微信小程序端单独渲染
const localJobCardsList = ref([]);
const handleItemClick = (e) => {
let { attrs } = e.detail.node;
console.log(attrs);
let { 'data-copy-index': codeDataIndex, 'data-job-id': jobId, class: className } = attrs;
switch (className) {
case 'custom-card':
return navTo('/packageA/pages/post/post?jobId=' + jobId);
case 'custom-more':
return navTo('/packageA/pages/moreJobs/moreJobs?jobId=' + jobId);
case 'copy-btn':
uni.setClipboardData({
data: codeDataList[codeDataIndex],
showToast: false,
success() {
uni.showToast({
title: '复制成功',
icon: 'none',
});
},
});
break;
// 监听content的变化当内容更新时解析岗位卡片
watch(() => props.content, (newContent) => {
if (newContent) {
// 直接调用parseMarkdown来触发jobCardsList的更新
parseMarkdown(newContent);
// 将解析器生成的岗位卡片列表赋值给响应式数据
localJobCardsList.value = [...jobCardsList];
console.log('Content changed, jobCardsList updated:', localJobCardsList.value);
}
}, { immediate: true });
// 同时监听renderedHtml的变化作为备份
watch(() => renderedHtml.value, (newVal) => {
console.log('renderedHtml changed, jobCardsList from parser:', jobCardsList);
// 将解析器生成的岗位卡片列表赋值给响应式数据
localJobCardsList.value = [...jobCardsList];
});
// 微信小程序端导航到岗位详情页面
const navigateToJobDetail = (jobId) => {
console.log('navigateToJobDetail called with jobId:', jobId);
if (jobId && jobId !== 'undefined' && jobId !== 'null') {
// 跳转到岗位详情页面
uni.navigateTo({
url: `/packageA/pages/post/post?jobId=${jobId}`,
success: (res) => {
console.log('navigateTo success:', res);
},
fail: (err) => {
console.error('navigateTo failed:', err);
// 如果navigateTo失败尝试redirectTo
uni.redirectTo({
url: `/packageA/pages/post/post?jobId=${jobId}`,
success: (res2) => {
console.log('redirectTo success:', res2);
},
fail: (err2) => {
console.error('redirectTo also failed:', err2);
// 如果还是失败,显示错误提示
uni.showToast({
title: '跳转失败,请稍后重试',
icon: 'none'
});
}
});
}
});
} else {
console.error('Invalid jobId:', jobId);
uni.showToast({
title: '岗位信息不完整',
icon: 'none'
});
}
};
// 微信小程序端点击事件处理已移除改用v-for遍历岗位卡片列表每个卡片单独渲染支持点击事件
// 微信小程序端和H5端的jobId取值方式一致都是从JSON数据的appJobUrl字段提取
// 不同的是微信小程序端现在使用v-for遍历渲染而H5端使用v-html渲染
// H5平台点击事件处理
// 微信小程序端和H5端的jobId取值方式一致都是从JSON数据的appJobUrl字段提取
// 不同的是H5端可以直接从DOM属性获取而微信小程序端使用v-for遍历渲染
const handleH5Click = (e) => {
console.log('H5 click event triggered:', e.target);
let target = e.target;
// 查找最接近的带有class的元素
while (target && target.tagName !== 'BODY') {
console.log('Checking target:', target, 'className:', target.className, 'tagName:', target.tagName);
// 直接使用closest方法查找不依赖className
const cardElement = target.closest('.custom-card');
const moreElement = target.closest('.custom-more');
console.log('Found elements:', { cardElement, moreElement });
if (cardElement) {
// 尝试多种方式获取jobId
let jobId = cardElement.getAttribute('data-job-id');
console.log('Found custom-card, data-job-id attribute:', jobId);
// 如果data-job-id为空尝试从onclick事件中提取jobId
if (!jobId) {
const onclick = cardElement.getAttribute('onclick');
if (onclick) {
const match = onclick.match(/jobId=(\w+)/);
if (match && match[1]) {
jobId = match[1];
console.log('Extracted jobId from onclick:', jobId);
}
}
}
if (jobId) {
console.log('Final jobId for navigation:', jobId);
try {
// 直接使用uni.navigateTo避免navTo函数的潜在问题
uni.navigateTo({
url: `/packageA/pages/post/post?jobId=${jobId}`,
success: (res) => {
console.log('navigateTo success:', res);
},
fail: (err) => {
console.error('navigateTo failed:', err);
// 如果navigateTo失败尝试redirectTo
uni.redirectTo({
url: `/packageA/pages/post/post?jobId=${jobId}`,
success: (res2) => {
console.log('redirectTo success:', res2);
},
fail: (err2) => {
console.error('redirectTo also failed:', err2);
}
});
}
});
} catch (error) {
console.error('Navigation error:', error);
}
return;
} else {
console.error('No jobId found for custom-card');
}
} else if (moreElement) {
// 尝试多种方式获取jobId
let jobId = moreElement.getAttribute('data-job-id');
console.log('Found custom-more, data-job-id attribute:', jobId);
// 如果data-job-id为空尝试从onclick事件中提取jobId
if (!jobId) {
const onclick = moreElement.getAttribute('onclick');
if (onclick) {
const match = onclick.match(/jobId=(\w+)/);
if (match && match[1]) {
jobId = match[1];
console.log('Extracted jobId from onclick:', jobId);
}
}
}
if (jobId) {
console.log('Final jobId for more jobs:', jobId);
try {
// 直接使用uni.navigateTo避免navTo函数的潜在问题
uni.navigateTo({
url: `/packageA/pages/moreJobs/moreJobs?jobId=${jobId}`,
success: (res) => {
console.log('navigateTo success:', res);
},
fail: (err) => {
console.error('navigateTo failed:', err);
// 如果navigateTo失败尝试redirectTo
uni.redirectTo({
url: `/packageA/pages/moreJobs/moreJobs?jobId=${jobId}`,
success: (res2) => {
console.log('redirectTo success:', res2);
},
fail: (err2) => {
console.error('redirectTo also failed:', err2);
}
});
}
});
} catch (error) {
console.error('Navigation error:', error);
}
return;
} else {
console.error('No jobId found for custom-more');
}
}
target = target.parentElement;
}
};
// 移除旧的事件监听逻辑,改用全局点击处理
onMounted(() => {
console.log('onMounted called');
});
</script>
<style lang="scss">
@@ -322,107 +510,475 @@ ol {
padding: 0
/* #endif */
/* H5端和小程序端样式优化 */
.custom-card
background: #FFFFFF
box-shadow: 0rpx 0rpx 8rpx 0rpx rgba(0,0,0,0.04)
border-radius: 20rpx
padding: 28rpx 24rpx
font-weight: 400
font-size: 28rpx
color: #333333
margin-bottom: 20rpx
position: relative
display: flex
flex-direction: column
/* 确保在小程序中边距正确应用 */
/* #ifdef MP-WEIXIN */
margin-left: auto
margin-right: auto
width: 100%
box-sizing: border-box
/* #endif */
.card-title
font-weight: 600
display: flex
align-items: center
justify-content: space-between
margin-bottom: 16rpx
.title-text
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif
max-width: calc(100% - 160rpx)
overflow: hidden
text-overflow: ellipsis
font-size: 30rpx
line-height: 1.4
.card-salary
font-family: DIN-Medium
font-size: 28rpx
color: #FF6E1C
line-height: 1.4
background: #FFFFFF !important
box-shadow: 0rpx 0rpx 8rpx 0rpx rgba(0,0,0,0.04) !important
border-radius: 20rpx !important
padding: 28rpx 24rpx !important
font-weight: 400 !important
font-size: 28rpx !important
color: #333333 !important
margin-bottom: 22rpx !important
position: relative !important
display: block !important
flex-direction: column !important
/* 确保在所有平台中边距正确应用 */
margin-left: auto !important
margin-right: auto !important
width: 100% !important
box-sizing: border-box !important
text-decoration: none !important
overflow: hidden !important
.card-company
margin-bottom: 22rpx
max-width: 100%
overflow: hidden
text-overflow: ellipsis
color: #6C7282
line-height: 1.4
.custom-card .card-title
font-weight: 600 !important
display: flex !important
align-items: center !important
justify-content: space-between !important
margin-bottom: 16rpx !important
.custom-card .card-title .title-text
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif !important
max-width: calc(100% - 160rpx) !important
overflow: hidden !important
text-overflow: ellipsis !important
font-size: 32rpx !important
line-height: 1.4 !important
white-space: nowrap !important
margin-bottom: 0 !important
.custom-card .card-title .card-salary
font-family: DIN-Medium !important
font-size: 32rpx !important
color: #4C6EFB !important
line-height: 1.4 !important
font-weight: 500 !important
margin-bottom: 0 !important
.custom-card .card-company
margin-bottom: 18rpx !important
max-width: 100% !important
overflow: hidden !important
text-overflow: ellipsis !important
color: #6C7282 !important
line-height: 1.4 !important
white-space: nowrap !important
font-size: 28rpx !important
margin-top: 0 !important
display: block !important
.custom-card .card-tags
display: flex !important
flex-wrap: wrap !important
margin-bottom: 24rpx !important
.custom-card .card-tag
font-weight: 400 !important
font-size: 24rpx !important
color: #6C7282 !important
width: fit-content !important
background: #F4F4F4 !important
border-radius: 4rpx !important
padding: 6rpx 20rpx !important
margin-right: 20rpx !important
margin-bottom: 14rpx !important
display: inline-flex !important
align-items: center !important
justify-content: center !important
height: 30rpx !important
line-height: 30rpx !important
.custom-card .card-bottom
display: flex !important
justify-content: space-between !important
font-size: 24rpx !important
color: #6C7282 !important
margin-top: 0 !important
margin-bottom: 0 !important
.custom-card .card-bottom .info-item
display: flex !important
align-items: center !important
justify-content: center !important
margin-bottom: 0 !important
.custom-card .card-info
display: flex !important
align-items: center !important
justify-content: space-between !important
padding-right: 40rpx !important
.custom-card .card-info .info-item
display: flex !important
position: relative !important
align-items: center !important
.custom-card .card-info .info-item:last-child
color: #256BFA !important
font-size: 28rpx !important
padding-right: 10rpx !important
.custom-card .position-nav
position: absolute !important
right: -10rpx !important
top: 50% !important
transform: translateY(-50%) !important
.custom-card .position-nav::before
position: absolute !important
left: 0 !important
top: -4rpx !important
content: '' !important
width: 4rpx !important
height: 16rpx !important
border-radius: 2rpx !important
background: #256BFA !important
transform: translate(0, -50%) rotate(-45deg) !important
.custom-card .position-nav::after
position: absolute !important
left: 0 !important
top: -4rpx !important
content: '' !important
width: 4rpx !important
height: 16rpx !important
border-radius: 2rpx !important
background: #256BFA !important
transform: rotate(45deg) !important
/* 为微信小程序专门优化的样式选择器 */
/* #ifdef MP-WEIXIN */
.job-cards-container .custom-card
background: #FFFFFF !important
box-shadow: 0rpx 0rpx 8rpx 0rpx rgba(0,0,0,0.04) !important
border-radius: 20rpx !important
padding: 28rpx 24rpx !important
font-weight: 400 !important
font-size: 28rpx !important
color: #333333 !important
margin-bottom: 22rpx !important
position: relative !important
display: block !important
flex-direction: column !important
margin-left: auto !important
margin-right: auto !important
width: 100% !important
box-sizing: border-box !important
text-decoration: none !important
overflow: hidden !important
.job-cards-container .custom-card .card-title
font-weight: 600 !important
display: flex !important
align-items: center !important
justify-content: space-between !important
margin-bottom: 16rpx !important
.job-cards-container .custom-card .card-title .title-text
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif !important
max-width: calc(100% - 160rpx) !important
overflow: hidden !important
text-overflow: ellipsis !important
font-size: 32rpx !important
line-height: 1.4 !important
white-space: nowrap !important
margin-bottom: 0 !important
.job-cards-container .custom-card .card-title .card-salary
font-family: DIN-Medium !important
font-size: 32rpx !important
color: #4C6EFB !important
line-height: 1.4 !important
font-weight: 500 !important
margin-bottom: 0 !important
.job-cards-container .custom-card .card-company
margin-bottom: 18rpx !important
max-width: 100% !important
overflow: hidden !important
text-overflow: ellipsis !important
color: #6C7282 !important
line-height: 1.4 !important
white-space: nowrap !important
font-size: 28rpx !important
margin-top: 0 !important
display: block !important
.job-cards-container .custom-card .card-info
display: flex !important
align-items: center !important
justify-content: space-between !important
padding-right: 40rpx !important
.job-cards-container .custom-card .card-info .info-item
display: flex !important
position: relative !important
align-items: center !important
.job-cards-container .custom-card .card-info .info-item:last-child
color: #256BFA !important
font-size: 28rpx !important
padding-right: 10rpx !important
.job-cards-container .custom-card .card-info .info-item .card-tag
font-weight: 400 !important
font-size: 24rpx !important
color: #6C7282 !important
width: fit-content !important
background: #F4F4F4 !important
border-radius: 4rpx !important
padding: 6rpx 20rpx !important
margin-right: 20rpx !important
margin-bottom: 14rpx !important
display: inline-flex !important
align-items: center !important
justify-content: center !important
height: 30rpx !important
line-height: 30rpx !important
.job-cards-container .custom-card .position-nav
position: absolute !important
right: -10rpx !important
top: 50% !important
transform: translateY(-50%) !important
.job-cards-container .custom-card .position-nav::before
position: absolute !important
left: 0 !important
top: -4rpx !important
content: '' !important
width: 4rpx !important
height: 16rpx !important
border-radius: 2rpx !important
background: #256BFA !important
transform: translate(0, -50%) rotate(-45deg) !important
.job-cards-container .custom-card .position-nav::after
position: absolute !important
left: 0 !important
top: -4rpx !important
content: '' !important
width: 4rpx !important
height: 16rpx !important
border-radius: 2rpx !important
background: #256BFA !important
transform: rotate(45deg) !important
/* #endif */
/* 额外的H5端样式优化 */
/* #ifndef MP-WEIXIN */
/* 确保样式能正确应用到v-html生成的内容 */
.markdown-body {
/* 重置v-html容器样式 */
display: block !important;
.card-info
display: flex
align-items: center
justify-content: space-between
padding-right: 40rpx
.info-item
display: flex
position: relative
align-items: center
&:last-child
color: #256BFA
font-size: 28rpx
padding-right: 10rpx
/* 为v-html生成的a.custom-card标签添加基础样式 */
a.custom-card {
display: flex !important;
flex-direction: column !important;
margin-bottom: 22rpx !important;
background: #FFFFFF !important;
box-shadow: 0rpx 0rpx 8rpx 0rpx rgba(0,0,0,0.04) !important;
border-radius: 20rpx !important;
padding: 28rpx 24rpx !important;
font-weight: 400 !important;
font-size: 28rpx !important;
color: #333333 !important;
text-decoration: none !important;
overflow: hidden !important;
box-sizing: border-box !important;
width: calc(100% - 0rpx) !important;
max-width: 100% !important;
}
.position-nav
position: absolute
right: -10rpx
top: 50%
transform: translateY(-50%)
/* 卡片标题样式 */
a.custom-card .card-title {
font-weight: 600 !important;
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
margin-bottom: 16rpx !important;
}
.position-nav::before
position: absolute
left: 0
top: -4rpx
content: ''
width: 4rpx
height: 16rpx
border-radius: 2rpx
background: #256BFA
transform: translate(0, -50%) rotate(-45deg)
a.custom-card .card-title .title-text {
font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif !important;
font-size: 32rpx !important;
line-height: 1.4 !important;
color: #333333 !important;
max-width: calc(100% - 160rpx) !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
white-space: nowrap !important;
margin-bottom: 0 !important;
}
.position-nav::after
position: absolute
left: 0
top: -4rpx
content: ''
width: 4rpx
height: 16rpx
border-radius: 2rpx
background: #256BFA
transform: rotate(45deg)
a.custom-card .card-title .card-salary {
font-family: DIN-Medium !important;
font-size: 32rpx !important;
color: #4C6EFB !important;
line-height: 1.4 !important;
font-weight: 500 !important;
margin-bottom: 0 !important;
}
.card-tag
font-weight: 500
font-size: 24rpx
color: #333333
width: fit-content
background: #F4F4F4
border-radius: 4rpx
padding: 4rpx 20rpx
margin-right: 16rpx
margin-bottom: 0
/* 公司信息样式 */
a.custom-card .card-company {
margin-bottom: 18rpx !important;
font-size: 28rpx !important;
color: #6C7282 !important;
line-height: 1.4 !important;
max-width: 100% !important;
overflow: hidden !important;
text-overflow: ellipsis !important;
white-space: nowrap !important;
margin-top: 0 !important;
display: block !important;
}
/* 标签容器样式 */
a.custom-card .card-tags {
display: flex !important;
flex-wrap: wrap !important;
margin-bottom: 24rpx !important;
}
/* 单个标签样式 */
a.custom-card .card-tag {
font-weight: 400 !important;
font-size: 24rpx !important;
color: #6C7282 !important;
width: fit-content !important;
background: #F4F4F4 !important;
border-radius: 4rpx !important;
padding: 6rpx 20rpx !important;
margin-right: 20rpx !important;
margin-bottom: 14rpx !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
height: 30rpx !important;
line-height: 30rpx !important;
}
/* 卡片底部样式 */
a.custom-card .card-bottom {
display: flex !important;
justify-content: space-between !important;
font-size: 24rpx !important;
color: #6C7282 !important;
margin-top: 0 !important;
margin-bottom: 0 !important;
}
a.custom-card .card-bottom .info-item {
display: flex !important;
align-items: center !important;
justify-content: center !important;
margin-bottom: 0 !important;
}
/* 卡片信息区域样式 */
a.custom-card .card-info {
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
padding-right: 40rpx !important;
}
a.custom-card .card-info .info-item {
display: flex !important;
align-items: center !important;
}
a.custom-card .card-info .info-item:last-child {
color: #256BFA !important;
font-size: 28rpx !important;
padding-right: 10rpx !important;
position: relative !important;
}
/* 查看详情箭头样式 */
a.custom-card .position-nav {
position: absolute !important;
right: -10rpx !important;
top: 50% !important;
transform: translateY(-50%) !important;
}
a.custom-card .position-nav::before {
position: absolute !important;
left: 0 !important;
top: -4rpx !important;
content: '' !important;
width: 4rpx !important;
height: 16rpx !important;
border-radius: 2rpx !important;
background: #256BFA !important;
transform: translate(0, -50%) rotate(-45deg) !important;
}
a.custom-card .position-nav::after {
position: absolute !important;
left: 0 !important;
top: -4rpx !important;
content: '' !important;
width: 4rpx !important;
height: 16rpx !important;
border-radius: 2rpx !important;
background: #256BFA !important;
transform: rotate(45deg) !important;
}
/* 查看更多按钮样式 */
a.custom-more {
display: flex !important;
justify-content: center !important;
align-items: center !important;
color: #FFFFFF !important;
background: linear-gradient(135deg, #256BFA 0%, #9E74FD 100%) !important;
border-radius: 50rpx !important;
padding: 20rpx 32rpx !important;
margin: 20rpx 0 !important;
font-size: 28rpx !important;
font-weight: 600 !important;
box-shadow: 0rpx 8rpx 24rpx rgba(37, 107, 250, 0.3) !important;
transition: all 0.3s ease !important;
position: relative !important;
overflow: hidden !important;
text-decoration: none !important;
box-sizing: border-box !important;
width: 100% !important;
}
a.custom-more .more-icon {
width: 32rpx !important;
height: 32rpx !important;
background: url('@/static/svg/seemore.svg') center center no-repeat !important;
background-size: 100% 100% !important;
margin-left: 12rpx !important;
filter: brightness(0) invert(1) !important;
}
a.custom-more::before {
content: '' !important;
position: absolute !important;
top: 0 !important;
left: -100% !important;
width: 100% !important;
height: 100% !important;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent) !important;
transition: left 0.5s ease !important;
}
a.custom-more:active {
transform: translateY(2rpx) !important;
box-shadow: 0rpx 4rpx 16rpx rgba(37, 107, 250, 0.4) !important;
}
a.custom-more:active::before {
left: 100% !important;
}
}
/* #endif */
</style>

View File

@@ -110,7 +110,7 @@ const generateTabbarList = () => {
baseItems.splice(1, 0, {
id: 1,
text: '发布岗位',
path: '/pages/job/publishJob',
path: '/packageA/pages/job/publishJob',
iconPath: '../../static/tabbar/post.png',
selectedIconPath: '../../static/tabbar/posted.png',
centerItem: false,

View File

@@ -40,6 +40,22 @@
</view>
</view>
<!-- 机构类型选择仅单位角色显示 -->
<view v-if="userType === 0" class="org-type-select">
<view class="org-type-title">请选择机构类型</view>
<view class="org-type-options">
<view
v-for="option in orgTypeOptions"
:key="option.value"
class="org-type-item"
:class="{ active: orgType === option.value }"
@click="selectOrgType(option.value)"
>
<view class="org-type-text">{{ option.label }}</view>
</view>
</view>
</view>
<!-- 授权说明 -->
<view class="auth-tips">
<view class="tip-item">
@@ -99,21 +115,50 @@
</template>
<script setup>
import { ref, inject } from 'vue';
import { ref, inject, onMounted } from 'vue';
import useUserStore from '@/stores/useUserStore';
import useDictStore from '@/stores/useDictStore';
import { tabbarManager } from '@/utils/tabbarManager';
const { $api } = inject('globalFunction');
const { loginSetToken } = useUserStore();
const dictStore = useDictStore();
const popup = ref(null);
const userType = ref(null); // 用户角色1-求职者0-企业
const orgType = ref(null); // 机构类型
const orgTypeOptions = ref([]); // 机构类型选项
const emit = defineEmits(['success', 'cancel']);
// 获取机构类型字典
const getOrgTypeDict = async () => {
try {
const options = await dictStore.getDictSelectOption('org_type');
orgTypeOptions.value = options;
} catch (error) {
console.error('获取机构类型字典失败:', error);
// 使用备用数据
orgTypeOptions.value = [
{ label: '有限责任公司', value: '1' },
{ label: '股份有限公司', value: '2' },
{ label: '个人独资企业', value: '3' },
{ label: '合伙企业', value: '4' },
{ label: '外商投资企业', value: '5' },
{ label: '其他', value: '6' }
];
}
};
// 组件挂载时获取字典数据
onMounted(() => {
getOrgTypeDict();
});
// 打开弹窗
const open = () => {
popup.value?.open();
userType.value = null; // 重置角色选择
orgType.value = null; // 重置机构类型选择
};
// 关闭弹窗
@@ -125,6 +170,12 @@ const close = () => {
// 选择角色
const selectRole = (type) => {
userType.value = type;
orgType.value = null; // 切换角色时重置机构类型选择
};
// 选择机构类型
const selectOrgType = (type) => {
orgType.value = type;
};
// 验证角色是否已选择
@@ -133,6 +184,13 @@ const validateRole = () => {
$api.msg('请先选择您的角色');
return false;
}
// 验证机构类型是否已选择(仅单位角色)
if (userType.value === 0 && orgType.value === null) {
$api.msg('请选择机构类型');
return false;
}
return true;
};
@@ -147,6 +205,12 @@ const getPhoneNumber = (e) => {
$api.msg('请先选择您的角色');
return true;
}
// 验证机构类型是否已选择(仅单位角色)
if (userType.value === 0 && orgType.value === null) {
$api.msg('请选择机构类型');
return true;
}
uni.login({
provider: 'weixin',
success: (loginRes) => {
@@ -161,7 +225,8 @@ const getPhoneNumber = (e) => {
code,
encryptedData,
iv,
userType: userType.value
userType: userType.value,
orgType: orgType.value
}, 'post').then((resData) => {
uni.hideLoading();
console.log(resData, 'resume.idCard');
@@ -188,12 +253,12 @@ const getPhoneNumber = (e) => {
if (userType.value === 1 && !resData.idCard) {
// 求职者跳转到个人信息补全页面
uni.navigateTo({
url: '/pages/complete-info/complete-info?step=1'
url: '/packageA/pages/complete-info/complete-info?step=1'
});
} else if (userType.value === 0 && !resData.idCard) {
// 招聘者跳转到企业信息补全页面
uni.navigateTo({
url: '/pages/complete-info/company-info'
url: '/packageA/pages/complete-info/company-info'
});
}
}
@@ -250,7 +315,8 @@ const wxLogin = () => {
// 调用后端接口进行登录
$api.createRequest('/app/appLogin', {
code: loginRes.code,
userType: userType.value
userType: userType.value,
orgType: orgType.value
}, 'post').then((resData) => {
if (resData.token) {
loginSetToken(resData.token).then((resume) => {
@@ -271,12 +337,12 @@ const wxLogin = () => {
if (userType.value === 1) {
// 求职者跳转到个人信息补全页面
uni.navigateTo({
url: '/pages/complete-info/complete-info?step=1'
url: '/packageA/pages/complete-info/complete-info?step=1'
});
} else if (userType.value === 0) {
// 招聘者跳转到企业信息补全页面
uni.navigateTo({
url: '/pages/complete-info/company-info'
url: '/packageA/pages/complete-info/company-info'
});
}
}
@@ -325,12 +391,12 @@ const testLogin = () => {
if (userType.value === 1) {
// 求职者跳转到个人信息补全页面
uni.navigateTo({
url: '/pages/complete-info/complete-info?step=1'
url: '/packageA/pages/complete-info/complete-info?step=1'
});
} else if (userType.value === 0) {
// 招聘者跳转到企业信息补全页面
uni.navigateTo({
url: '/pages/complete-info/company-info'
url: '/packageA/pages/complete-info/company-info'
});
}
}
@@ -372,7 +438,7 @@ defineExpose({
overflow: hidden
.modal-content
padding: 60rpx 40rpx 40rpx
padding: 40rpx 40rpx 40rpx
position: relative
.close-btn
@@ -388,11 +454,11 @@ defineExpose({
.auth-header
text-align: center
margin-bottom: 40rpx
margin-bottom: 20rpx
.auth-logo
width: 120rpx
height: 120rpx
width: 90rpx
height: 90rpx
margin: 0 auto 24rpx
.auth-title
@@ -407,7 +473,6 @@ defineExpose({
.role-select
margin-bottom: 32rpx
.role-title
font-size: 28rpx
font-weight: 500
@@ -446,6 +511,41 @@ defineExpose({
color: #333333
font-weight: 500
.org-type-select
margin-bottom: 22rpx
.org-type-title
font-size: 28rpx
font-weight: 500
color: #333333
margin-bottom: 20rpx
text-align: center
.org-type-options
display: flex
flex-wrap: wrap
gap: 16rpx
.org-type-item
display:inline-block
background: #F7F8FA
border: 2rpx solid #E5E5E5
border-radius: 10rpx
padding: 10rpx 10rpx
transition: all 0.3s ease
cursor: pointer
text-align: center
&.active
background: #F0F5FF
border-color: #256BFA
box-shadow: 0 4rpx 12rpx rgba(37, 107, 250, 0.15)
.org-type-text
font-size: 22rpx
color: #333333
font-weight: 500
.auth-tips
background: #F7F8FA
border-radius: 16rpx

View File

@@ -20,6 +20,7 @@ export function useAudioRecorder() {
const recognizedText = ref('')
const lastFinalText = ref('')
const isRecognizing = ref(false) // 识别状态,暴露给外部
let audioStream = null
let audioContext = null
@@ -132,11 +133,15 @@ export function useAudioRecorder() {
}
case 'TranscriptionCompleted': {
lastFinalText.value = ''
isRecognizing.value = false // 识别完成,重置状态
// console.log('识别全部完成')
cleanup()
break
}
case 'TaskFailed': {
console.error('识别失败:', msg?.header?.status_text)
isRecognizing.value = false // 识别失败,重置状态
cleanup()
break
}
default:
@@ -151,7 +156,104 @@ export function useAudioRecorder() {
if (isRecording.value) return
// #ifdef MP-WEIXIN
$api.msg('小程序暂不支持语音识别功能');
try {
recognizedText.value = ''
lastFinalText.value = ''
// 开始录音时不设置isRecognizing为true只有在停止录音后才保持isRecognizing为true
const recorderManager = uni.getRecorderManager()
// 监听录音完成事件
recorderManager.onStop(async (res) => {
console.log('小程序录音完成:', res)
try {
// 停止录音后设置isRecognizing为true显示loading
isRecognizing.value = true
// 打印请求配置,便于调试
console.log('准备上传语音识别请求配置:', {
url: config.vioceBaseURl,
name: 'file',
method: 'POST',
fileType: 'audio',
filePath: res.tempFilePath
})
// 上传录音文件到服务器进行语音识别
const uploadResult = await uni.uploadFile({
url: config.vioceBaseURl,
filePath: res.tempFilePath,
name: 'file',
fileType: 'audio',
method: 'POST' // 显式设置为POST请求
})
console.log('语音识别上传结果:', uploadResult)
if (uploadResult.statusCode === 200) {
try {
const result = JSON.parse(uploadResult.data)
console.log('语音识别结果:', result)
if (result.code === 200 && result.data) {
recognizedText.value = result.data
console.log('语音识别成功,识别结果:', recognizedText.value)
// 语音识别成功后,自动发送消息
// 这里需要触发一个事件,让父组件知道识别成功
// 或者直接调用发送消息的方法
isRecognizing.value = false // 识别成功,重置状态
} else {
console.error('语音识别返回错误:', result.message || '未知错误')
$api.msg('语音识别失败,请重试')
isRecognizing.value = false // 识别失败,重置状态
}
} catch (parseErr) {
console.error('语音识别结果解析失败:', parseErr)
$api.msg('语音识别失败,请重试')
isRecognizing.value = false // 解析失败,重置状态
}
} else {
console.error('语音识别请求失败,状态码:', uploadResult.statusCode)
$api.msg('语音识别失败,请重试')
isRecognizing.value = false // 请求失败,重置状态
}
} catch (err) {
console.error('语音识别上传失败:', err)
$api.msg('语音识别失败,请重试')
isRecognizing.value = false // 上传失败,重置状态
}
})
// 监听录音错误事件
recorderManager.onError((err) => {
console.error('小程序录音错误:', err)
$api.msg('录音失败,请重试');
cleanup()
})
// 微信小程序录音API
await recorderManager.start({
duration: 60000, // 最长录音60秒
sampleRate: 16000,
numberOfChannels: 1,
encodeBitRate: 96000,
format: 'mp3'
})
isRecording.value = true
recordingDuration.value = 0
durationTimer = setInterval(() => recordingDuration.value++, 1000)
// 监听录音事件(可选)
recorderManager.onFrameRecorded((res) => {
// 更新音量显示
volumeLevel.value = res.volume || 0
audioDataForDisplay.value = Array(16).fill(volumeLevel.value)
})
} catch (err) {
console.error('小程序录音启动失败:', err)
$api.msg('录音启动失败,请重试');
cleanup()
}
return;
// #endif
@@ -164,6 +266,7 @@ export function useAudioRecorder() {
recognizedText.value = ''
lastFinalText.value = ''
// 开始录音时不设置isRecognizing为true只有在停止录音后才保持isRecognizing为true
await connectWebSocket()
audioStream = await navigator.mediaDevices.getUserMedia({
@@ -201,6 +304,7 @@ export function useAudioRecorder() {
durationTimer = setInterval(() => recordingDuration.value++, 1000)
} catch (err) {
console.error('启动失败:', err)
isRecognizing.value = false // 启动失败,重置状态
cleanup()
}
// #endif
@@ -210,6 +314,12 @@ export function useAudioRecorder() {
if (!isRecording.value || isStopping.value) return
isStopping.value = true
// #ifdef MP-WEIXIN
uni.getRecorderManager().stop()
// 小程序中录音停止后会触发onStop事件在onStop事件中处理识别结果和状态重置
// #endif
// #ifdef H5
if (websocket?.readyState === WebSocket.OPEN) {
websocket.send(JSON.stringify({
header: {
@@ -218,18 +328,39 @@ export function useAudioRecorder() {
message_id: generateUUID()
}
}))
websocket.close()
// H5中不立即调用cleanup等待识别完成
}
// #endif
cleanup()
// 只清理录音相关资源,不重置识别状态
clearInterval(durationTimer)
audioStream?.getTracks().forEach(track => track.stop())
audioContext?.close()
audioStream = null
audioContext = null
audioInput = null
scriptProcessor = null
isRecording.value = false
isSocketConnected.value = false
isStopping.value = false
}
const cancelRecording = () => {
if (!isRecording.value || isStopping.value) return
isStopping.value = true
// #ifdef MP-WEIXIN
uni.getRecorderManager().stop()
// #endif
// #ifdef H5
websocket?.close()
// #endif
// 取消录音时重置所有状态
cleanup()
isRecognizing.value = false
isStopping.value = false
}
@@ -249,16 +380,22 @@ export function useAudioRecorder() {
isRecording.value = false
isSocketConnected.value = false
isRecognizing.value = false // 停止录音,重置识别状态
}
onUnmounted(() => {
if (isRecording.value) stopRecording()
})
const reset = () => {
cleanup()
}
return {
isRecording,
isStopping,
isSocketConnected,
isRecognizing,
recordingDuration,
audioDataForDisplay,
volumeLevel,
@@ -266,6 +403,7 @@ export function useAudioRecorder() {
lastFinalText,
startRecording,
stopRecording,
cancelRecording
cancelRecording,
reset
}
}

File diff suppressed because it is too large Load Diff

20
main.js
View File

@@ -9,20 +9,13 @@ import globalFunction from '@/common/globalFunction'
import '@/lib/string-similarity.min.js'
import similarityJobs from '@/utils/similarity_Job.js';
import config from '@/config.js';
import shareMixin from './mixins/share.js';
// 组件
import AppLayout from './components/AppLayout/AppLayout.vue';
import Empty from './components/empty/empty.vue';
import NoBouncePage from '@/components/NoBouncePage/NoBouncePage.vue'
import MsgTips from '@/components/MsgTips/MsgTips.vue'
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';
import uniIcons from './uni_modules/uni-icons/components/uni-icons/uni-icons.vue'
import uniPopup from './uni_modules/uni-popup/components/uni-popup/uni-popup.vue'
import uniDataSelect from './uni_modules/uni-data-select/components/uni-data-select/uni-data-select.vue'
import uniSwipeAction from './uni_modules/uni-swipe-action/components/uni-swipe-action/uni-swipe-action.vue'
import uniSwipeActionItem from './uni_modules/uni-swipe-action/components/uni-swipe-action-item/uni-swipe-action-item.vue'
import storeRc from './utilsRc/store/index.js'
import {processFileUrl,} from '@/utilsRc/common.js'
// iconfont.css 已在 App.vue 中通过 @import 引入,无需在此处重复引入
@@ -44,18 +37,13 @@ import { getDict } from '@/apiRc/system/dict.js';
export function createApp() {
const app = createSSRApp(App)
// 注册全局分享混入
app.mixin(shareMixin)
app.component('AppLayout', AppLayout)
app.component('Empty', Empty)
app.component('NoBouncePage', NoBouncePage)
app.component('MsgTips', MsgTips)
app.component('SelectPopup', SelectPopup)
app.component('RenderJobs', RenderJobs)
app.component('RenderCompanys', RenderCompanys)
app.component('uni-icons', uniIcons)
app.component('uni-popup', uniPopup)
app.component('uni-data-select', uniDataSelect)
app.component('uni-swipe-action', uniSwipeAction)
app.component('uni-swipe-action-item', uniSwipeActionItem)
app.config.globalProperties.$processFileUrl = processFileUrl;

16
mixins/share.js Normal file
View File

@@ -0,0 +1,16 @@
export default {
onShareAppMessage() {
return {
title: '喀什智慧就业平台',
path: '/pages/index/index',
imageUrl: ''
};
},
onShareTimeline() {
return {
title: '喀什智慧就业平台',
path: '/pages/index/index',
imageUrl: ''
};
}
};

View File

@@ -0,0 +1,99 @@
<template>
<AppLayout :show-bg-image="false" :use-scroll-view="false">
<view class="collection-content">
<scroll-view scroll-y class="main-scroll" @scrolltolower="getJobList('add')">
<view class="one-cards">
<view class="mian">
<renderJobs
seeDate="shareTime"
v-if="pageState.list.length"
:list="pageState.list"
:longitude="longitudeVal"
:latitude="latitudeVal"
></renderJobs>
<empty v-else pdTop="200"></empty>
</view>
</view>
</scroll-view>
</view>
</AppLayout>
</template>
<script setup>
import dictLabel from '@/components/dict-Label/dict-Label.vue';
import AppLayout from '@/components/AppLayout/AppLayout.vue';
import { reactive, inject, watch, ref, onMounted } from 'vue';
import { onLoad, onShow, onReachBottom } from '@dcloudio/uni-app';
import useUserStore from '@/stores/useUserStore';
const { $api, navTo, vacanciesTo } = inject('globalFunction');
import { storeToRefs } from 'pinia';
import useLocationStore from '@/stores/useLocationStore';
const { longitudeVal, latitudeVal } = storeToRefs(useLocationStore());
const userStore = useUserStore();
const state = reactive({});
const pageState = reactive({
page: 0,
list: [],
total: 0,
maxPage: 1,
pageSize: 10,
});
onLoad(() => {
console.log('onLoad');
getJobList('refresh');
});
onReachBottom(() => {
getJobList();
});
function navToPost(jobId) {
navTo(`/packageA/pages/post/post?jobId=${encodeURIComponent(jobId)}`);
}
function getJobList(type = 'add') {
if (type === 'refresh') {
pageState.page = 1;
pageState.maxPage = 1;
}
if (type === 'add' && pageState.page < pageState.maxPage) {
pageState.page += 1;
}
let params = {
current: pageState.page,
pageSize: pageState.pageSize,
};
$api.createRequest('/app/job/selectCencalList', params, 'GET').then((resData) => {
const { rows, total } = resData;
if (type === 'add') {
const str = pageState.pageSize * (pageState.page - 1);
const end = pageState.list.length;
const reslist = rows;
pageState.list.splice(str, end, ...reslist);
} else {
pageState.list = rows;
}
pageState.total = resData.total;
pageState.maxPage = Math.ceil(pageState.total / pageState.pageSize);
console.log(pageState.list);
});
}
</script>
<style lang="stylus" scoped>
.collection-content
height: 100%
display: flex
flex-direction: column
.main-scroll{
flex: 1
overflow: hidden
}
.one-cards{
padding: 0 28rpx 20rpx 28rpx;
background: #f4f4f4
}
</style>

View File

@@ -364,7 +364,7 @@ const changeSkillName = (index) => {
state.currentEditingSkillIndex = index;
//
uni.navigateTo({
url: `/pages/complete-info/skill-search?selected=${encodeURIComponent(JSON.stringify([]))}`
url: `/packageA/pages/complete-info/skill-search?selected=${encodeURIComponent(JSON.stringify([]))}`
});
};

View File

@@ -76,6 +76,18 @@
<view class="picker-text" data-placeholder="请选择学历要求">{{ selectedEducation || '请选择学历要求' }}</view>
</picker>
</view>
<view class="form-group">
<view class="label">人员类型</view>
<picker
mode="selector"
:range="staffTypes"
range-key="label"
@change="onStaffTypeChange"
class="picker"
>
<view class="picker-text" data-placeholder="请选择人员类型">{{ selectedStaffType || '请选择人员类型' }}</view>
</picker>
</view>
<view class="form-group">
<view class="label">工作经验</view>
<picker
@@ -302,6 +314,7 @@ const formData = reactive({
jobLocationAreaCode: '', //
education: '', //
experience: '', //
staffType: '', //
images: [], //
contacts: [
{
@@ -324,6 +337,7 @@ const experienceLevels = ref([]);
const workDistricts = ref([]);
const workLocations = ref([]);
const jobCategories = ref([]); //
const staffTypes = ref([]); //
//
const selectedEducation = ref('');
@@ -333,6 +347,7 @@ const selectedWorkLocation = ref('');
const selectedJobCategory = ref('');
const selectedJobTypeLabel = ref(''); //
const selectedJobTypeIds = ref(''); // ID
const selectedStaffType = ref(''); //
//
const scrollViewHeight = ref('calc(100vh - 200rpx)');
@@ -451,6 +466,11 @@ const initFormData = async () => {
}
console.log('岗位分类选项:', jobCategories.value);
// - staffType
const staffTypeDict = await dictStore.getDictSelectOption('staff_type');
console.log('从字典获取的人员类型数据:', staffTypeDict);
staffTypes.value = staffTypeDict;
// ID
if (userStore.userInfo && userStore.userInfo.id) {
formData.companyId = userStore.userInfo.id;
@@ -512,6 +532,14 @@ const onJobCategoryChange = (e) => {
formData.type = selectedItem.value; // type
};
// change
const onStaffTypeChange = (e) => {
const index = e.detail.value;
const selectedItem = staffTypes.value[index];
selectedStaffType.value = selectedItem.label;
formData.staffType = selectedItem.value; // staffType
};
//
const openJobTypeSelector = () => {
if (!jobTypeSelector.value) return;
@@ -706,6 +734,7 @@ const publishJob = async () => {
jobCategory: formData.jobCategory, //
companyId: formData.companyId,
companyName: formData.companyName,
staffType: formData.staffType, //
jobContactList: formData.contacts.filter(contact => contact.name.trim() && contact.phone.trim()).map(contact => ({
contactPerson: contact.name,
contactPersonPhone: contact.phone,
@@ -760,6 +789,7 @@ const validateForm = () => {
{ field: 'minSalary', message: '请输入最小薪资' },
{ field: 'maxSalary', message: '请输入最大薪资' },
{ field: 'education', message: '请选择学历要求' },
{ field: 'staffType', message: '请选择人员类型' },
{ field: 'experience', message: '请选择工作经验' },
{ field: 'jobLocation', message: '请选择工作地点' },
{ field: 'jobLocationAreaCode', message: '请选择工作区县' },

View File

@@ -472,7 +472,7 @@ function changeSkillName(index) {
// 将当前已选中的技能名称传递给查询页面
const selectedSkills = state.skills.map(skill => skill.name).filter(name => name);
uni.navigateTo({
url: `/pages/complete-info/skill-search?selected=${encodeURIComponent(JSON.stringify(selectedSkills))}`
url: `/packageA/pages/complete-info/skill-search?selected=${encodeURIComponent(JSON.stringify(selectedSkills))}`
});
}

View File

@@ -32,9 +32,9 @@
<!-- 控制栏 -->
<view class="controls">
<!-- <text @click="togglePlay">{{ isPlaying ? '暂停' : '播放' }}</text>
<text @click="toggleFullScreen">{{ isFullScreen ? '退出全屏' : '全屏' }}</text> -->
<text @click="close">关闭</text>
<!-- <view class="control-text" @click="togglePlay">{{ isPlaying ? '暂停' : '播放' }}</view>
<view class="control-text" @click="toggleFullScreen">{{ isFullScreen ? '退出全屏' : '全屏' }}</view> -->
<view class="control-text" @click="close">关闭</view>
</view>
</view>
</template>
@@ -180,7 +180,7 @@ defineExpose({ open });
justify-content: space-around;
}
.controls text {
.controls .control-text {
color: #fff;
font-size: 24rpx;
padding: 8rpx 16rpx;

View File

@@ -218,10 +218,22 @@
<view style="height: 34px"></view>
<template #footer>
<view class="footer">
<view class="btn-wq button-click" @click="jobApply">投递简历</view>
<view class="btn-wq button-click" @click="jobApply">{{ jobInfo.isApply === 1 ? '取消投递简历' : '投递简历' }}</view>
</view>
</template>
<VideoPlayer ref="videoPalyerRef" />
<!-- 确认弹窗 -->
<view v-if="showConfirmDialog" class="confirm-dialog">
<view class="dialog-content">
<view class="dialog-title">{{ jobInfo.isApply === 1 ? '确认取消投递' : '确认投递' }}</view>
<view class="dialog-message">{{ jobInfo.isApply === 1 ? '确定要取消投递此职位吗?' : '确定要投递此职位吗?' }}</view>
<view class="dialog-buttons">
<view class="btn-cancel button-click" @click="hideDialog">取消</view>
<view class="btn-confirm button-click" @click="confirmAction">确认</view>
</view>
</view>
</view>
</AppLayout>
</template>
@@ -229,7 +241,7 @@
import point from '@/static/icon/point.png';
import VideoPlayer from './component/videoPlayer.vue';
import { reactive, inject, watch, ref, onMounted, computed } from 'vue';
import { onLoad, onShow, onHide } from '@dcloudio/uni-app';
import { onLoad, onShow, onHide, onShareAppMessage, onShareTimeline } from '@dcloudio/uni-app';
import dictLabel from '@/components/dict-Label/dict-Label.vue';
import RadarMap from './component/radarMap.vue';
import { storeToRefs } from 'pinia';
@@ -266,6 +278,7 @@ const raderData = ref({
});
const videoPalyerRef = ref(null);
const explainUrlRef = ref('');
const showConfirmDialog = ref(false);
// 申请人列表直接使用接口返回的applyUsers数组
@@ -351,10 +364,10 @@ function getCompanyIsAJobs(companyId) {
}
function getTextWidth(text, size = 12) {
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
context.font = `${12}px Arial`;
return -(context.measureText(text).width / 2) - 20; // 计算文字中心点
// 在小程序环境中document 对象不存在,使用估算方法
// 简单估算:每个字符大约占 8px 宽度
const estimatedWidth = text.length * 8;
return -(estimatedWidth / 2) - 20; // 计算文字中心点
}
function getCompetivetuveness(jobId) {
@@ -422,24 +435,53 @@ function jobApply() {
$api.msg('请您先登录');
return;
}
// 显示确认弹窗
showConfirmDialog.value = true;
}
// 隐藏弹窗
function hideDialog() {
showConfirmDialog.value = false;
}
// 确认操作
function confirmAction() {
const jobId = jobInfo.value.jobId;
if (jobInfo.value.isApply === 1) {
// 取消投递
$api.createRequest(`/app/job/applyJobCencal`, { jobId }, 'DELETE').then((resData) => {
$api.msg('取消投递成功');
getDetail(jobId); // 刷新职位信息
showConfirmDialog.value = false;
});
} else {
// 确认投递
$api.createRequest(`/app/job/apply/${jobId}`, {}, 'GET').then((resData) => {
$api.msg('申请成功');
getDetail(jobId); // 刷新职位信息
showConfirmDialog.value = false;
});
}
}
// 确认投递
function confirmApply() {
const jobId = jobInfo.value.jobId;
$api.createRequest(`/app/job/apply/${jobId}`, {}, 'GET').then((resData) => {
getDetail(jobId);
$api.msg('申请成功');
const jobUrl = jobInfo.value.jobUrl;
// return window.open(jobUrl);
});
// if (jobInfo.value.isApply) {
// const jobUrl = jobInfo.value.jobUrl;
// return window.open(jobUrl);
// } else {
// $api.createRequest(`/app/job/apply/${jobId}`, {}, 'GET').then((resData) => {
// getDetail(jobId);
// $api.msg('申请成功');
// const jobUrl = jobInfo.value.jobUrl;
// return window.open(jobUrl);
// });
// }
$api.msg('申请成功');
const jobUrl = jobInfo.value.jobUrl;
// return window.open(jobUrl);
showConfirmDialog.value = false;
});
}
// 取消投递
function cancelApply() {
const jobId = jobInfo.value.jobId;
$api.createRequest(`/app/job/applyJobCencal`, { jobId }, 'DELETE').then((resData) => {
$api.msg('取消投递成功');
showConfirmDialog.value = false;
});
}
// 取消/收藏岗位
@@ -524,6 +566,24 @@ function handleCompanyDetailClick() {
$api.msg('没有企业信息');
}
}
// 分享给朋友
onShareAppMessage(() => {
return {
title: '喀什智慧就业平台',
path: '/pages/index/index',
imageUrl: ''
};
});
// 分享到朋友圈
onShareTimeline(() => {
return {
title: '喀什智慧就业平台',
path: '/pages/index/index',
imageUrl: ''
};
});
</script>
<style lang="stylus" scoped>
@@ -966,4 +1026,70 @@ for i in 0..100
font-weight: bold;
color: #333;
}
// 确认弹窗样式
.confirm-dialog {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.dialog-content {
width: 80%;
max-width: 500rpx;
background-color: #fff;
border-radius: 20rpx;
padding: 40rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.dialog-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
margin-bottom: 30rpx;
}
.dialog-message {
font-size: 28rpx;
color: #666;
margin-bottom: 40rpx;
text-align: center;
}
.dialog-buttons {
width: 100%;
display: flex;
justify-content: space-between;
gap: 20rpx;
}
.btn-cancel, .btn-confirm {
flex: 1;
height: 80rpx;
border-radius: 12rpx;
display: flex;
justify-content: center;
align-items: center;
font-size: 28rpx;
font-weight: 500;
}
.btn-cancel {
background-color: #f5f5f5;
color: #666;
}
.btn-confirm {
background-color: #256BFA;
color: #fff;
}
</style>

View File

@@ -1,11 +1,11 @@
<!--
* @Date: 2025-10-16 15:15:47
* @LastEditors: lip
* @LastEditTime: 2026-01-14 21:33:18
* @LastEditors: shirlwang
* @LastEditTime: 2026-01-27 09:30:18
-->
<template>
<!-- @scroll="handleScroll" @scrolltolower="scrollBottom" -->
<scroll-view :scroll-y="true" class="container" style="background-image: url('../../../packageRc/static/pageBg.png');">
<scroll-view :scroll-y="true" class="container" style="background-image: url('../../../packageRc/static/pageBgIndex.png');">
<view style="padding: 40rpx 28rpx;">
<!-- #ifdef MP-WEIXIN -->
<view class="kinggang">
@@ -31,54 +31,69 @@
</view>
</view>
<!-- #endif -->
<view class="tabs">
<view class="tab" :class="{active: pageState.type == ''}" @click="changeJobType('')">岗位列表</view>
<view class="tab" :class="{active: pageState.type == 2}" @click="changeJobType(2)">实习实训</view>
<view class="tab" :class="{active: pageState.type == 3}" @click="changeJobType(3)">社区实践</view>
</view>
<view class="titles">
<view class="title-item" :class="{active: activeTitle == 1}" @click="activeTitle = 1,getJobRecommed()"><view>推荐岗位</view></view>
<view class="title-item" :class="{active: activeTitle == 2}" @click="activeTitle = 2,getJobList()"><view>热门岗位</view></view>
</view>
<view v-for="(item, index) in jobList" :key="index" @click="nextDetail(item)" class="job-list">
<view class="top-line">
<view class="salary">{{item.minSalary}}-{{item.maxSalary}}/</view>
<view class="time"><uni-icons color="#A2A2A2" type="info" size="12"></uni-icons>发布日期{{ item.postingDate }}</view>
<view class="showtab">
<view class="tabItem" @click="changeType(1)">
<image src="/packageRc/static/gw.png"/>
<image v-show="tabType == 1" class="activeImg" src="/packageRc/static/activeTangle.png"/>
</view>
<view class="title">{{ item.jobTitle }}</view>
<view class="infos">
<view>
<dict-Label dictType="education" :value="item.education"></dict-Label>
<view class="tabItem" @click="changeType(2)">
<image src="/packageRc/static/zc.png"/>
<image v-show="tabType == 2" class="activeImg" src="/packageRc/static/activeTangle.png"/>
</view>
</view>
<template v-if="tabType == 1">
<view class="tabs">
<view class="tab" :class="{active: pageState.type == ''}" @click="changeJobType('')">岗位列表</view>
<view class="tab" :class="{active: pageState.type == 2}" @click="changeJobType(2)">实习实训</view>
<view class="tab" :class="{active: pageState.type == 3}" @click="changeJobType(3)">社区实践</view>
</view>
<view class="titles">
<view class="title-item" :class="{active: activeTitle == 1}" @click="activeTitle = 1,getJobRecommed()"><view>推荐岗位</view></view>
<view class="title-item" :class="{active: activeTitle == 2}" @click="activeTitle = 2,getJobList()"><view>热门岗位</view></view>
</view>
<view v-for="(item, index) in jobList" :key="index" @click="nextDetail(item)" class="job-list">
<view class="top-line">
<view class="salary">{{item.minSalary}}-{{item.maxSalary}}/</view>
<view class="time"><uni-icons color="#A2A2A2" type="info" size="12"></uni-icons>发布日期{{ item.postingDate }}</view>
</view>
<view>
<dict-Label dictType="experience" :value="item.experience"></dict-Label>
<view class="title">{{ item.jobTitle }}</view>
<view class="infos">
<view>
<dict-Label dictType="education" :value="item.education"></dict-Label>
</view>
<view>
<dict-Label dictType="experience" :value="item.experience"></dict-Label>
</view>
<view>{{ item.jobLocation }}</view>
</view>
<view class="bottom-line">
<view><uni-icons color="#A2A2A2" type="person" size="12"></uni-icons>{{item.vacancies}}</view>
<view>{{ item.companyName }}</view>
</view>
<view>{{ item.jobLocation }}</view>
</view>
<view class="bottom-line">
<view><uni-icons color="#A2A2A2" type="person" size="12"></uni-icons>{{item.vacancies}}</view>
<view>{{ item.companyName }}</view>
<view class="view-more-btn" @click="viewMore">查看更多内容</view>
</template>
<template v-else>
<view class="titles" style="justify-content: space-between;">
<view class="title-item active"><view>政策专区</view></view>
<view @click="toPolicyList">{{'查看更多 >'}}</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 @click="toPolicyList">{{'查看更多 >'}}</view>
</view>
<view v-for="(item, index) in policyList" :key="index" class="job-list" @click="toPolicyDetail(item)">
<view class="sign">推荐</view>
<view class="title">
<image src="../../../packageRc/static/zcLeft.png"/>
{{item.zcmc}}</view>
<view class="infos">
<view v-if="item.zcLevel">{{item.zcLevel}}</view>
<view v-if="item.sourceUnit">{{item.sourceUnit}}</view>
<view v-for="(item, index) in policyList" :key="index" class="job-list" @click="toPolicyDetail(item)">
<view class="sign">推荐</view>
<view class="title">
<image src="../../../packageRc/static/zcLeft.png"/>
{{item.zcmc}}</view>
<view class="infos">
<view v-if="item.zcLevel">{{item.zcLevel}}</view>
<view v-if="item.sourceUnit">{{item.sourceUnit}}</view>
</view>
<view class="bottom-line">
<view><uni-icons color="#A2A2A2" type="info" size="12"></uni-icons>发布日期:{{item.createTime}}</view>
</view>
</view>
<view class="bottom-line">
<view><uni-icons color="#A2A2A2" type="info" size="12"></uni-icons>发布日期:{{item.createTime}}</view>
</view>
</view>
</template>
</view>
</scroll-view>
</template>
@@ -94,7 +109,10 @@ function getPolicy() {
policyList.value = res.rows
})
}
let tabType = ref(1)
function changeType(type) {
tabType.value = type
}
function toPolicyList() {
navTo(`/packageRc/pages/policy/policyList?zclx=1`)
}
@@ -377,4 +395,22 @@ view{box-sizing: border-box;display: block;}
margin: 0 auto;
margin-bottom: 20rpx;
}
.showtab{
display: flex;
justify-content: space-between;
margin-bottom: 40rpx;
.tabItem{
position: relative;
width: calc(50% - 8rpx);
height: 144rpx;
}
.activeImg{
position: absolute;
width: 143rpx;
height: 18rpx;
bottom: -24rpx;
right: 50%;
transform: translateX(50%);
}
}
</style>

View File

@@ -70,7 +70,7 @@
</view>
<view class="company-grid">
<view class="company-item press-button" @click="navTo('/pages/job/publishJob')">
<view class="company-item press-button" @click="navTo('/packageA/pages/job/publishJob')">
<view class="company-icon company-icon-1">
<uni-icons type="plus-filled" size="32" color="#FFFFFF"></uni-icons>
</view>
@@ -731,7 +731,7 @@ const handleLoginSuccess = () => {
// 处理附近工作点击
const handleNearbyClick = () => {
if (checkLogin()) {
navTo("/pages/nearby/nearby");
navTo("/packageA/pages/nearby/nearby");
}
};

Binary file not shown.

After

Width:  |  Height:  |  Size: 734 B

BIN
packageRc/static/gw.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

BIN
packageRc/static/zc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -31,67 +31,20 @@
}
},
{
"path": "pages/complete-info/complete-info",
"path": "pages/search/search",
"style": {
"navigationBarTitleText": "补全信息"
}
},
{
"path": "pages/complete-info/company-info",
"style": {
"navigationBarTitleText": "企业信息"
}
},
{
"path": "pages/complete-info/components/map-location-picker",
"style": {
"navigationBarTitleText": "选择地址"
}
},
{
"path": "pages/complete-info/skill-search",
"style": {
"navigationBarTitleText": "技能查询"
}
},
{
"path": "pages/nearby/nearby",
"style": {
"navigationBarTitleText": "附近",
"navigationBarBackgroundColor": "#4778EC",
"navigationBarTextStyle": "white"
}
},
{
"path": "pages/job/publishJob",
"style": {
"navigationBarTitleText": "发布岗位"
}
},
{
"path": "pages/job/companySearch",
"style": {
"navigationBarTitleText": "选择企业",
"disableScroll": false,
"enablePullDownRefresh": false,
"backgroundColor": "#f5f5f5"
"navigationBarTitleText": "搜索职位"
}
},
{
"path": "pages/chat/chat",
"style": {
"navigationBarTitleText": "AI+",
"navigationBarTitleText": "智能客服",
"navigationBarBackgroundColor": "#4778EC",
"navigationBarTextStyle": "white",
"enablePullDownRefresh": false
}
},
{
"path": "pages/search/search",
"style": {
"navigationBarTitleText": "搜索职位"
}
},
{
"path": "pages/service/career-planning",
"style": {
@@ -161,6 +114,53 @@
{
"root": "packageA",
"pages": [
{
"path": "pages/complete-info/complete-info",
"style": {
"navigationBarTitleText": "补全信息"
}
},
{
"path": "pages/complete-info/company-info",
"style": {
"navigationBarTitleText": "企业信息"
}
},
{
"path": "pages/complete-info/components/map-location-picker",
"style": {
"navigationBarTitleText": "选择地址"
}
},
{
"path": "pages/complete-info/skill-search",
"style": {
"navigationBarTitleText": "技能查询"
}
},
{
"path": "pages/nearby/nearby",
"style": {
"navigationBarTitleText": "附近",
"navigationBarBackgroundColor": "#4778EC",
"navigationBarTextStyle": "white"
}
},
{
"path": "pages/job/publishJob",
"style": {
"navigationBarTitleText": "发布岗位"
}
},
{
"path": "pages/job/companySearch",
"style": {
"navigationBarTitleText": "选择企业",
"disableScroll": false,
"enablePullDownRefresh": false,
"backgroundColor": "#f5f5f5"
}
},
{
"path": "pages/addWorkExperience/addWorkExperience",
"style": {
@@ -312,6 +312,13 @@
"navigationBarBackgroundColor": "#4778EC",
"navigationBarTextStyle": "white"
}
},
{
"path": "pages/cancelApplication/cancelApplication",
"style": {
"navigationBarTitleText": "取消投递",
"navigationBarBackgroundColor": "#FFFFFF"
}
}
]
},

View File

@@ -61,6 +61,7 @@
<view class="main-header">
<image src="/static/icon/Hamburger-button.png" @click="toggleDrawer"></image>
<view class="title">{{ config.appInfo.areaName }}岗位推荐</view>
<!-- <view class="title">智能客服</view> -->
<image src="/static/icon/Comment-one.png" @click="addNewDialogue"></image>
</view>
</header>

View File

@@ -65,6 +65,26 @@ const centerIndex = ref(0);
// 动画帧ID
let animationId = null;
// 为小程序环境提供requestAnimationFrame兼容
const requestAnimationFramePolyfill = (callback) => {
// #ifdef MP-WEIXIN
return setTimeout(callback, 16); // 约60fps
// #endif
// #ifdef H5
return requestAnimationFrame(callback);
// #endif
};
// 为小程序环境提供cancelAnimationFrame兼容
const cancelAnimationFramePolyfill = (id) => {
// #ifdef MP-WEIXIN
clearTimeout(id);
// #endif
// #ifdef H5
cancelAnimationFrame(id);
// #endif
};
// 格式化显示时间
const formattedTime = computed(() => {
const mins = Math.floor(props.recordingTime / 60)
@@ -125,7 +145,7 @@ const updateWaveform = () => {
}
}
animationId = requestAnimationFrame(updateWaveform);
animationId = requestAnimationFramePolyfill(updateWaveform);
};
// 更新单个波形条
@@ -157,14 +177,14 @@ const updateWaveBar = (index, value) => {
// 开始动画
const startAnimation = () => {
if (!animationId) {
animationId = requestAnimationFrame(updateWaveform);
animationId = requestAnimationFramePolyfill(updateWaveform);
}
};
// 停止动画
const stopAnimation = () => {
if (animationId) {
cancelAnimationFrame(animationId);
cancelAnimationFramePolyfill(animationId);
animationId = null;
}
};

View File

@@ -133,16 +133,30 @@
<view class="chat-item self" v-if="isRecording">
<view class="message">{{ recognizedText }} {{ lastFinalText }}</view>
</view>
<!-- 语音正在识别提示 -->
<!-- <view>{{isRecognizing}}</view> -->
<view class="chat-item self" v-if="isRecognizing">
<view class="message msg-loading">
<view class="loading-content">
<view class="ai-loading">
<view></view>
<view></view>
<view></view>
</view>
<text class="loading-text">正在识别语音...</text>
</view>
</view>
</view>
<view v-if="isTyping" class="self">
<view class="message msg-loading">
<div class="loading-content">
<span class="ai-loading">
<span></span>
<span></span>
<span></span>
</span>
<view class="loading-content">
<view class="ai-loading">
<view></view>
<view></view>
<view></view>
</view>
<text class="loading-text">AI正在思考中...</text>
</div>
</view>
</view>
</view>
</view>
@@ -175,9 +189,6 @@
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
@touchcancel="handleTouchCancel"
:catchtouchstart="true"
:catchtouchmove="true"
:catchtouchend="true"
v-show="isVoice"
type="default"
>
@@ -283,7 +294,7 @@ import {
getCurrentInstance,
} from 'vue';
import { storeToRefs } from 'pinia';
// import config from '@/config.js';
// 移除重复导入使用从globalFunction注入的config
import useChatGroupDBStore from '@/stores/userChatGroupStore';
import MdRender from '@/components/md-render/md-render.vue';
import CollapseTransition from '@/components/CollapseTransition/CollapseTransition.vue';
@@ -294,11 +305,11 @@ import FileIcon from './fileIcon.vue';
import FileText from './fileText.vue';
import { useAudioRecorder } from '@/hook/useRealtimeRecorder.js';
import { useTTSPlayer } from '@/hook/useTTSPlayer.js';
import successIcon from '@/static/icon/success.png';
// 全局
const { $api, navTo, throttle, config } = inject('globalFunction');
const emit = defineEmits(['onConfirm']);
const { messages, isTyping, textInput, chatSessionID } = storeToRefs(useChatGroupDBStore());
import successIcon from '@/static/icon/success.png';
// hook
const {
isRecording,
@@ -309,8 +320,32 @@ const {
volumeLevel,
recognizedText,
lastFinalText,
recordingDuration,
isRecognizing,
reset
} = useAudioRecorder();
// 监听语音识别结果变化,自动发送消息
watch(
() => recognizedText.value,
(newVal) => {
if (newVal && newVal.trim()) {
console.log('监听到语音识别结果变化,自动发送消息:', newVal);
sendMessage(newVal);
}
}
);
// 监听isRecognizing状态显示提示
watch(
() => isRecognizing.value,
(newVal) => {
if (newVal) {
$api.msg('正在识别语音...');
}
}
);
const { speak, pause, resume, isSpeaking, isPaused, cancelAudio } = useTTSPlayer(config.speechSynthesis);
// 获取组件实例(用于小程序 SelectorQuery
@@ -362,6 +397,7 @@ onMounted(async () => {
changeQueries();
scrollToBottom();
isAudioPermission.value = await requestMicPermission();
reset(); // 重置语音识别状态
});
const requestMicPermission = async () => {
@@ -443,11 +479,16 @@ const sendMessage = (text) => {
console.log('📝 Has job info:', hasJobInfo);
// 开始朗读当前消息
speechIndex.value = index;
readMarkdown(message.displayText, index);
speechIndex.value = index;
readMarkdown(message.displayText, index, { immediate: false });
// 一旦开始朗读就设置speechIndex避免重复调用
speechIndex.value = index;
} else {
console.log('⏳ Waiting for more content before TTS, current length:', message.displayText.length);
}
} else {
// 已经开始朗读这条消息,不再重复调用
console.log('⏭️ Already speaking this message, skipping duplicate TTS call');
}
}
},
@@ -472,7 +513,7 @@ const sendMessage = (text) => {
// 开始朗读完整的内容
speechIndex.value = lastMessageIndex;
readMarkdown(lastMessage.displayText, lastMessageIndex);
readMarkdown(lastMessage.displayText, lastMessageIndex, { immediate: true });
}
}
},
@@ -684,17 +725,22 @@ const handleTouchEnd = () => {
if (status.value === 'cancel') {
console.log('取消发送');
cancelRecording();
status.value = 'idle';
} else {
stopRecording();
if (isAudioPermission.value) {
if (recognizedText.value) {
sendMessage(recognizedText.value);
} else {
// 主要根据录音时长判断,而不是完全依赖识别结果
// 由于setInterval是异步的这里需要考虑计时延迟
const actualDuration = recordingDuration.value > 0 ? recordingDuration.value : (isRecording.value ? 0.5 : 0);
if (actualDuration < 1) {
$api.msg('说话时长太短');
status.value = 'idle';
} else {
// 状态管理由useAudioRecorder hook内部处理
status.value = 'idle';
}
}
}
status.value = 'idle';
};
const handleTouchCancel = () => {
@@ -740,7 +786,10 @@ function confirmFeeBack(value) {
// 防抖定时器
let ttsDebounceTimer = null;
function readMarkdown(value, index) {
// 保存上一次调用的文本内容避免重复调用TTS
let lastSpeechText = '';
function readMarkdown(value, index, options = {}) {
console.log('🎤 readMarkdown called');
console.log('📝 Text to speak:', value ? value.substring(0, 100) + '...' : 'No text');
console.log('🔢 Index:', index);
@@ -753,40 +802,37 @@ function readMarkdown(value, index) {
clearTimeout(ttsDebounceTimer);
}
// 如果当前正在播放其他消息,先停止
if (speechIndex.value !== index && speechIndex.value !== 0) {
console.log('🛑 Stopping current speech and starting new one');
speechIndex.value = index;
speak(value);
return;
}
// 总是先停止当前播放,无论是不是同一消息
console.log('🛑 Always stopping current speech before starting new one');
speechIndex.value = index;
// 如果当前正在播放且暂停了,直接恢复
if (isPaused.value && isSpeaking.value) {
console.log('▶️ Resuming paused speech');
resume();
return;
}
// 如果当前正在播放且没有暂停,不需要重新开始
if (isSpeaking.value && !isPaused.value) {
console.log('🔊 Already speaking, no need to restart');
return;
}
// 使用防抖避免频繁调用TTS
ttsDebounceTimer = setTimeout(() => {
console.log('🎵 Starting new speech');
console.log('🎵 Calling speak function with text length:', value ? value.length : 0);
try {
speak(value);
console.log('✅ Speak function called successfully');
} catch (error) {
console.error('❌ Error calling speak function:', error);
// 立即调用speak不使用防抖延迟
const speakNow = () => {
// 检查文本内容是否发生变化避免重复调用TTS
if (value !== lastSpeechText) {
console.log('🎵 Starting new speech');
console.log('🎵 Calling speak function with text length:', value ? value.length : 0);
try {
speak(value);
console.log('✅ Speak function called successfully');
// 更新上一次调用的文本内容
lastSpeechText = value;
} catch (error) {
console.error('❌ Error calling speak function:', error);
}
} else {
console.log('🔄 Same text as last speech, skipping duplicate TTS call');
}
}, 300); // 300ms防抖延迟
};
// 改进防抖逻辑,确保在短时间内只调用一次
if (options.immediate) {
// 如果是onComplete回调立即播放
speakNow();
} else {
// 对于流式数据,总是使用防抖,避免频繁调用
ttsDebounceTimer = setTimeout(speakNow, 500); // 延长防抖时间到500ms
}
}
function stopMarkdown(value, index) {
console.log('⏸️ stopMarkdown called for index:', index);
@@ -1118,6 +1164,11 @@ image-margin-top = 40rpx
-moz-user-select:none;
-ms-user-select:none;
touch-action: none; /* 禁用默认滚动 */
position: fixed;
left: 0;
right: 0;
bottom: 160rpx; /* 为底部导航栏留出空间 */
z-index: 9999; /* 确保高于其他元素 */
.record-tip
font-weight: 400;
color: #909090;
@@ -1299,7 +1350,7 @@ image-margin-top = 40rpx
}
/* 三个点的样式 - 使用标准CSS语法不使用嵌套 */
.ai-loading span {
.ai-loading view {
display: inline-block;
width: 12rpx;
height: 12rpx;
@@ -1311,15 +1362,15 @@ image-margin-top = 40rpx
}
/* 为每个点设置不同的动画延迟 */
.ai-loading span:nth-child(1) {
.ai-loading view:nth-child(1) {
animation-delay: -0.32s;
}
.ai-loading span:nth-child(2) {
.ai-loading view:nth-child(2) {
animation-delay: -0.16s;
}
.ai-loading span:nth-child(3) {
.ai-loading view:nth-child(3) {
animation-delay: 0s;
}
</style>

View File

@@ -1,5 +1,5 @@
<template>
<view class="app-container">
<view class="app-container" @touchstart="handleTouchStart" @touchmove="handleTouchMove">
<!-- #ifdef MP-WEIXIN -->
<!-- 小程序背景图片 -->
<image class="mp-background" src="/static/icon/background2.png" mode="aspectFill"></image>
@@ -110,7 +110,7 @@
<view class="service-icon service-icon-5">
<IconfontIcon name="jinengpeixun" :size="48" color="#FFFFFF" />
</view>
<view class="service-title">技能培训</view>
<view class="service-title">技能课堂</view>
</view>
<view class="service-item press-button" @click="handleServiceClick('skill-evaluation')">
<view class="service-icon service-icon-6">
@@ -136,12 +136,7 @@
</view>
<view class="service-title">虚拟面试</view>
</view>
<view class="service-item press-button" style="justify-content:normal" @click="goRc()">
<view class="service-icon service-icon-9">
<IconfontIcon name="Graduation-simple-" :size="32" color="#FFFFFF" />
</view>
<view class="service-title" style="overflow:unset">高校毕业生<br/>智慧就业服务</view>
</view>
<view class="service-item press-button" @click="handleServiceClick('career-planning')">
<view class="service-icon service-icon-11">
<image class="service-icon-img" src="/static/icon/antOutline.png" mode="aspectFit"></image>
@@ -185,6 +180,12 @@
</view>
<view class="service-title">评价机构信息</view>
</view>
<view class="service-item press-button" style="justify-content:normal" @click="goRc()">
<view class="service-icon service-icon-9">
<IconfontIcon name="Graduation-simple-" :size="32" color="#FFFFFF" />
</view>
<view class="service-title" style="overflow:unset">高校毕业生<br/>智慧就业服务</view>
</view>
</view>
<!-- #endif -->
</view>
@@ -197,7 +198,7 @@
</view>
<view class="company-grid">
<view class="company-item press-button" @click="navTo('/pages/job/publishJob')">
<view class="company-item press-button" @click="navTo('/packageA/pages/job/publishJob')">
<view class="company-icon company-icon-1">
<uni-icons type="plus-filled" size="32" color="#FFFFFF"></uni-icons>
</view>
@@ -229,7 +230,7 @@
<view class="nav-filter" :class="{ 'sticky-filter': shouldStickyFilter }" v-if="shouldShowJobSeekerContent">
<view class="filter-top" @touchmove.stop.prevent>
<view class="filter-top">
<scroll-view :scroll-x="true" :show-scrollbar="false" class="tab-scroll">
<view class="jobs-left">
<view
@@ -286,7 +287,6 @@
@scrolltolower="scrollBottom"
:enable-back-to-top="false"
:scroll-with-animation="false"
@touchmove.stop.prevent
>
<view class="falls" v-if="list.length">
<!-- #ifdef MP-WEIXIN -->
@@ -567,10 +567,48 @@ const lastScrollTop = ref(0);
const scrollTop = ref(0);
// 当用户与筛选/导航交互时,临时锁定头部显示状态,避免因数据刷新导致回弹显示
const isInteractingWithFilter = ref(false);
// 触摸事件状态
const touchStartY = ref(0);
const touchMoveY = ref(0);
// 滚动阈值配置
const HIDE_THRESHOLD = 50; // 隐藏顶部区域的滚动阈值(降低阈值,更容易触发)
const SHOW_THRESHOLD = 5; // 显示顶部区域的滚动阈值(接近顶部)
const STICKY_THRESHOLD = 80; // 筛选区域吸顶的滚动阈值
const TOUCH_MOVE_THRESHOLD = 30; // 触摸滑动阈值,用于判断是否为有效滑动
// 处理触摸开始事件
function handleTouchStart(e) {
// 记录触摸起始位置
touchStartY.value = e.touches[0].clientY;
}
// 处理触摸移动事件
function handleTouchMove(e) {
// 记录触摸移动位置
touchMoveY.value = e.touches[0].clientY;
// 计算滑动距离
const diffY = touchStartY.value - touchMoveY.value;
// 当向上滑动超过阈值时,隐藏顶部区域
if (diffY > TOUCH_MOVE_THRESHOLD) {
if (!shouldHideTop.value) {
shouldHideTop.value = true;
}
if (!shouldStickyFilter.value) {
shouldStickyFilter.value = true;
}
}
// 当向下滑动超过阈值且在顶部附近时,显示顶部区域
else if (diffY < -TOUCH_MOVE_THRESHOLD && scrollTop.value <= SHOW_THRESHOLD) {
if (shouldHideTop.value && !isInteractingWithFilter.value) {
shouldHideTop.value = false;
}
if (shouldStickyFilter.value) {
shouldStickyFilter.value = false;
}
}
}
// 简化的滚动处理函数
function handleScroll(e) {
@@ -744,7 +782,7 @@ onMounted(() => {
console.log('收到citySelected事件选择的城市:', city);
selectedCity.value = city;
// 可以在这里添加根据城市筛选职位的逻辑
conditionSearch.value.jobLocationAreaCode = city.code;
conditionSearch.value.regionCode = city.code;
getJobRecommend('refresh');
});
@@ -807,13 +845,13 @@ onLoad(() => {
const handleNearbyClick = (options ) => {
// #ifdef MP-WEIXIN
if (checkLogin()) {
navTo('/pages/nearby/nearby');
navTo('/packageA/pages/nearby/nearby');
}
// #endif
// #ifdef H5
const token = options.token || uni.getStorageSync('zkr-token');
if (token) {
navTo('/pages/nearby/nearby');
navTo('/packageA/pages/nearby/nearby');
}
// #endif
};
@@ -1511,10 +1549,10 @@ defineExpose({ loadData });
color: #256BFA
// 服务功能网格样式
.service-grid
padding: 20rpx 28rpx
padding: 10rpx 28rpx
display: grid
grid-template-columns: 1fr 1fr 1fr 1fr
grid-gap: 20rpx
grid-gap: 10rpx
.service-item
display: flex
flex-direction: column
@@ -1522,12 +1560,12 @@ defineExpose({ loadData });
justify-content: center
height: 120rpx
background: transparent
padding: 10px 0px
padding: 2rpx 0px
.service-icon
width: 88rpx
height: 88rpx
border-radius: 12rpx
margin-bottom: 8rpx
width: 62rpx
height: 62rpx
border-radius: 10rpx
margin-bottom: 14rpx
flex-shrink: 0
.service-icon-1
background: linear-gradient(180deg, #FF8E8E 0%, #E53E3E 100%)

View File

@@ -47,6 +47,10 @@
<view class="mini-num">{{ counts.fairCollecitonCount }}</view>
<view class="mini-text">预约</view>
</view>
<view class="numbe-item button-click" @click="navTo('/packageA/pages/cancelApplication/cancelApplication')">
<view class="mini-num">{{ counts.applyCencalCount || 0 }}</view>
<view class="mini-text">取消投递</view>
</view>
</view>
<view class="mini-cards">
<view class="card-top btn-feel">
@@ -201,7 +205,7 @@ function seeDetail() {
function goToJobHelper() {
// 跳转到求职者信息补全页面
navTo('/pages/complete-info/complete-info');
navTo('/packageA/pages/complete-info/complete-info');
}
// 跳转到素质测评
function goCa(){

View File

@@ -43,30 +43,47 @@ const store = useSkillDevelopmentStore();
</button>
</div>
</div>
<div class="content-section">
<div v-if="store.careerPaths.length" class="content-section career-section">
<div class="section-title">
<span class="title-text">职业路径</span>
</div>
<div class="">
<scroll-view scroll-x>
<div class="career-paths">
<template v-for="(item, index) in store.careerPaths" :key="item.label">
<div
:class="{ 'career-active': store.currentCareer && store.currentCareer.value === item.value }"
class="career-path-item"
@click="store.eventResult(index)"
>
<div
class="career-index"
>
<span class="index">{{ item.index }}</span>
</div>
<div class="career-label">{{ item.label }}</div>
</div>
</template>
</div>
</scroll-view>
</div>
</div>
<div v-if="store.careerPaths.length" class="content-section">
<div class="section-title">
<uni-icons color="#000000" size="18" type="person-filled"></uni-icons>
<span class="title-text">技能发展路径</span>
<span class="title-text">{{ store.currentCareerLabel }}技能</span>
</div>
<div class="intro-text">
基于您的当前职业和目标职业,以下是您需要重点发展的技能:
</div>
<div class="skill-list">
<div v-if="store.result.length === 0" class="empty-text">暂无数据</div>
<div v-for="(skill, index) in store.result" v-else :key="index" class="skill-item">
<div class="skill-header">
<span class="skill-name">{{ skill.name }}</span>
<div class="skill-info">
<span class="skill-score">技能分数: {{ skill.score }}</span>
<span class="skill-weight">权重: {{ skill.weight }}</span>
</div>
</div>
<div v-if="skill.tags && skill.tags.length > 0" class="skill-tags">
<div v-for="(tag, tagIndex) in skill.tags" :key="tagIndex" class="skill-tag">
{{ tag }}
<div v-for="(item, index) in store.result" :key="index" class="skill-card">
<div class="skill-label">{{ item.label }}</div>
<div class="skill-detail">
<div v-for="d in item.children" :key="d.label" class="skill-detail-item">
<div class="skill-detail-item-label">{{ d.label }}</div>
<div class="skill-detail-item-value">
<div class="value-item">技能得分{{ d.value }}</div>
<div class="value-item">权重{{ d.weight }}</div>
</div>
</div>
</div>
</div>
@@ -98,6 +115,7 @@ const store = useSkillDevelopmentStore();
background-color: #FFFFFF;
border-radius: 16rpx;
padding: 28rpx;
margin-bottom: 20rpx;
box-sizing: border-box;
overflow: visible;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.08);
@@ -135,6 +153,7 @@ const store = useSkillDevelopmentStore();
button::after {
border: none;
}
.section-title {
font-size: 32rpx;
color: #000000;
@@ -148,115 +167,189 @@ button::after {
}
}
.career-paths {
display: grid;
grid-auto-flow: column;
gap: 20px;
padding-bottom: 10px;
.career-path-item {
display: flex;
flex-direction: column;
align-items: center;
width: 80px;
padding: 5px;
font-weight: 700;
cursor: pointer;
.career-index {
position: relative;
display: flex;
justify-content: center;
align-items: center;
width: 48px;
height: 48px;
font-size: 20px;
color: #fff;
border-radius: 50%;
.index {
position: relative;
z-index: 10;
}
&:before,
&:after {
content: '';
position: absolute;
}
&:before {
inset: 0;
z-index: 1;
background: #6c64e7;
border: 3px solid #fff;
border-radius: 50%;
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.3);
}
&:after {
content: '';
position: absolute;
top: 50%;
right: 50%;
z-index: 0;
width: 100px;
border-bottom: 4px solid #ccc;
}
}
.career-label {
margin-top: 10px;
font-size: 16px;
text-align: center;
color: #6c64e7;
}
&.career-active {
.career-index {
&:before {
background: #22c55e;
}
&:after {
border-color: #22c55e;
}
}
.career-label {
color: #000;
}
& + .career-path-item {
.career-index {
&:after {
border-color: #22c55e;
}
}
}
}
&:first-child {
.career-index {
font-size: 16px;
&:after {
display: none;
}
}
}
&:last-child {
.career-index {
font-size: 16px;
}
}
}
}
</style>
<style lang="scss" scoped>
.title-text {
font-size: 32rpx;
font-weight: 600;
color: #167CF1;
}
.intro-text {
font-size: 24rpx;
line-height: 34rpx;
color: rgba(154, 154, 154, 1);
text-align: left;
font-family: 'PingFangSC-Bold', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
font-weight: 600;
margin-bottom: 90rpx;
width: 672rpx;
}
.skill-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.skill-item {
width: 100%;
max-width: 796rpx;
min-height: 162rpx;
line-height: 40rpx;
border-radius: 20rpx;
background-color: rgba(239, 239, 239, 1);
color: rgba(16, 16, 16, 1);
font-size: 28rpx;
text-align: center;
padding: 24rpx;
.skill-card {
margin-bottom: 14px;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: center;
overflow: visible;
position: relative;
font-weight: 700;
.skill-list {
display: flex;
flex-direction: column;
gap: 20rpx;
}
.skill-item {
width: 100%;
max-width: 796rpx;
min-height: 162rpx;
line-height: 40rpx;
border-radius: 20rpx;
background-color: rgba(239, 239, 239, 1);
color: rgba(16, 16, 16, 1);
font-size: 28rpx;
text-align: center;
padding: 24rpx;
box-sizing: border-box;
display: flex;
flex-direction: column;
justify-content: center;
overflow: visible;
position: relative;
}
.skill-label {
margin-bottom: 10px;
font-size: 16px;
color: #94a3b8;
}
.skill-detail {
display: flex;
flex-direction: column;
gap: 8px;
.skill-detail-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px;
border-radius: 3px;
background: #c1d2ea;
}
.skill-detail-item-label {
width: 55%;
word-wrap: break-word;
white-space: pre-wrap;
word-break: break-all;
color: #334155;
}
.skill-detail-item-value {
width: 45%;
padding-left: 6px;
color: #4f46e5;
box-sizing: border-box;
.value-item {
text-wrap: nowrap;
}
}
}
}
.skill-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
width: 100%;
box-sizing: border-box;
}
.skill-name {
font-size: 32rpx;
line-height: 46rpx;
color: rgb(16, 16, 16);
text-align: left;
font-family: 'PingFangSC-Bold', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif;
font-weight: 600;
flex: 1;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.skill-info {
display: flex;
align-items: center;
gap: 12rpx;
flex-shrink: 0;
margin-left: 16rpx;
}
.skill-score {
font-size: 24rpx;
line-height: 34rpx;
border-radius: 10rpx;
background-color: rgba(49, 100, 239, 0.1);
color: rgba(44, 101, 247, 1);
text-align: center;
padding: 6rpx 12rpx;
white-space: nowrap;
}
.skill-weight {
font-size: 24rpx;
line-height: 34rpx;
border-radius: 10rpx;
background-color: rgba(49, 100, 239, 0.1);
color: rgba(44, 101, 247, 1);
text-align: center;
padding: 6rpx 12rpx;
white-space: nowrap;
}
.skill-tags {
display: flex;
flex-wrap: wrap;
gap: 12rpx;
}
.skill-tag {
background-color: rgba(49, 100, 239, 0.1);
color: rgba(44, 101, 247, 1);
padding: 6rpx 12rpx;
border-radius: 8rpx;
font-size: 24rpx;
line-height: 34rpx;
text-align: center;
white-space: nowrap;
}
</style>

View File

@@ -81,7 +81,7 @@ export const useCareerRecommendationStore = defineStore('career-recommendation',
professions.value = data.map((d) => {
return {
label: d.name,
value: `${ d.jobId }`
value: d.name
};
});
} catch (e) {

View File

@@ -1,7 +1,7 @@
import { computed, ref, watch } from 'vue';
import { defineStore } from 'pinia';
import { getCurrentPosition, getPath } from '@/apiRc/service/careerPath';
import { getSkill } from '@/apiRc/service/skillDevelopment';
import { getCareerPath, getSkillResult } from '@/apiRc/service/skillDevelopment';
export const useSkillDevelopmentStore = defineStore('skill-development', () => {
@@ -62,10 +62,21 @@ export const useSkillDevelopmentStore = defineStore('skill-development', () => {
const paths = ref([]);
const pathsRef = computed(() => {
return paths.value.filter((d) => {
return `${ d.startJobId }` === profession.value;
const [startJobId] = d.value.split('-');
return startJobId === profession.value;
});
});
const careerPaths = ref([]);
const currentCareer = ref(null);
const currentCareerIndex = ref(0);
const currentCareerLabel = computed(() => {
if (!currentCareer.value) {
return '';
}
return currentCareer.value.label;
});
const result = ref([]);
const fetchData = async () => {
@@ -108,8 +119,8 @@ export const useSkillDevelopmentStore = defineStore('skill-development', () => {
paths.value = data.map((d) => {
return {
label: d.endJob,
value: d.endJob,
startJobId: d.startJobId
value: `${d.startJobId}-${d.endJobId}`
};
});
} catch (e) {
@@ -117,21 +128,17 @@ export const useSkillDevelopmentStore = defineStore('skill-development', () => {
}
};
const fetchResult = async () => {
const fetchCareerPaths = async () => {
if (!targetCareer.value) {
return;
}
const current = professionsRef.value.find((d) => d.value === profession.value);
const target = pathsRef.value.find((d) => d.value === targetCareer.value);
if (!current || !target) {
return;
}
const [startJobId, endJobId] = targetCareer.value.split('-');
const params = {
currentJobName: current.label,
targetJobName: target.label
startJobId,
endJobId
};
try {
const { code, msg, data } = await getSkill(params);
const { code, msg, data } = await getCareerPath(params);
if (code !== 0) {
uni.showToast({
title: msg,
@@ -139,20 +146,68 @@ export const useSkillDevelopmentStore = defineStore('skill-development', () => {
});
return;
}
if (typeof data !== 'undefined' && Array.isArray(data) && data.length > 0 && data[ 0 ]) {
const excludes = data[ 0 ].currentSkillDetList.map((d) => d.skillId);
result.value = data[ 0 ].targetSkillDetList
.filter((d) => !excludes.includes(d.skillId))
.map((d) => {
return {
type: d.skillType,
title: d.skillName,
name: d.skillName,
weight: d.skillWeight,
score: d.skillScore
};
});
if (!data) {
return;
}
careerPaths.value = data.map((d, i) => {
let index = `${i}`;
let fontSize = 26;
if (i === 0) {
index = '起点';
fontSize = 18;
}
if (i === data.length - 1) {
index = '终点';
fontSize = 18;
}
return {
index,
label: d.name,
value: d.jobId,
fontSize
};
});
if (careerPaths.value[0]) {
void eventResult(0);
}
} catch (e) {
console.warn(e);
careerPaths.value = [];
}
};
const eventResult = async (index) => {
currentCareerIndex.value = index;
currentCareer.value = careerPaths.value[index] ?? null;
const params = {
jobId: currentCareer.value?.value
};
try {
const { code, msg, data } = await getSkillResult(params);
if (code !== 0) {
uni.showToast({
title: msg,
icon: 'none'
});
return;
}
if (!data) {
return;
}
result.value = data.map((d) => {
return {
label: d.secDimName,
value: d.secDimId,
children: d.skillDetList.map((d) => {
return {
label: d.skillName,
value: d.skillScore,
weight: d.skillWeight
};
})
};
});
} catch (e) {
console.warn(e);
}
@@ -180,7 +235,7 @@ export const useSkillDevelopmentStore = defineStore('skill-development', () => {
});
return;
}
void fetchResult();
void fetchCareerPaths();
};
const eventProfession = (e) => {
@@ -190,6 +245,7 @@ export const useSkillDevelopmentStore = defineStore('skill-development', () => {
professionLabel.value = item.label;
targetCareer.value = '';
targetCareerLabel.value = '';
careerPaths.value = [];
result.value = [];
};
@@ -198,6 +254,7 @@ export const useSkillDevelopmentStore = defineStore('skill-development', () => {
const item = pathsRef.value[ e.detail.value ];
targetCareer.value = item.value;
targetCareerLabel.value = item.label;
careerPaths.value = [];
result.value = [];
};
@@ -223,9 +280,14 @@ export const useSkillDevelopmentStore = defineStore('skill-development', () => {
targetCareer,
targetCareerLabel,
pathsRef,
careerPaths,
currentCareer,
currentCareerLabel,
currentCareerIndex,
result,
eventProfession,
eventTargetCareer,
eventSearch
eventSearch,
eventResult
};
});

View File

@@ -49,14 +49,12 @@ export function navigateToLoginPage(options = {}) {
return;
case 'h5':
if (loginType === 'idCard') {
// H5端身份证号码登录
loginPage = '/pages/login/id-card-login';
} else {
// H5端账号密码登录
loginPage = '/pages/login/h5-login';
}
break;
uni.showToast({
title: '请先登录',
icon: 'none',
duration: 2000
});
return;
case 'app':
// App端使用微信授权登录

View File

@@ -1,157 +1,195 @@
import MarkdownIt from '@/lib/markdown-it.min.js';
import hljs from "@/lib/highlight/highlight-uni.min.js";
import parseHtml from '@/lib/html-parser.js';
// import DOMPurify from '@/lib/dompurify@3.2.4es.js';
export let codeDataList = []
export let jobMoreMap = new Map()
const md = new MarkdownIt({
html: true, // 允许 HTML 标签
linkify: true, // 自动解析 URL
typographer: true, // 美化标点符号
tables: true,
breaks: true, // 让 \n 自动换行
langPrefix: 'language-', // 代码高亮前缀
// 如果结果以 <pre ... 开头,内部包装器则会跳过。
highlight: function(str, lang) {
if (lang === 'job-json') {
const result = safeExtractJson(str);
if (result) { // json解析成功
const jobId = result.appJobUrl.split('jobId=')[1]
let domContext = `<a class="custom-card" data-job-id="${jobId}"><div class="card-title"><span class="title-text">${result.jobTitle}</span><div class="card-salary">${result.salary}</div></div><div class="card-company">${result.location}·${result.companyName}</div><div class="card-info"><div class="info-item"><div class="card-tag">${result.education}</div><div class="card-tag">${result.experience}</div></div><div class="info-item">查看详情<div class="position-nav"></div></div></div></a>`
if (result.data) {
jobMoreMap.set(jobId, result.data)
domContext += `<a class="custom-more" data-job-id="${jobId}">查看更多岗位<div class="more-icon"></div></a>`
}
return domContext
}
}
// 代码块
let preCode = ""
try {
preCode = hljs.highlightAuto(str).value
} catch (err) {
preCode = md.utils.escapeHtml(str);
}
// 以换行进行分割 , 按行拆分代码
const lines = preCode.split(/\n/).slice(0, -1);
const html = lines
.map((line, index) =>
line ?
`<li><span class="line-num" data-line="${index + 1}"></span>${line}</li>` :
'<li></li>'
)
.join('');
// 代码复制功能
const cacheIndex = codeDataList.length;
codeDataList.push(str);
return `
<div class="code-container">
<div class="code-header">
<span class="lang-label">${lang || 'plaintext'}</span>
<a class="copy-btn" data-copy-index="${cacheIndex}">复制代码</a>
</div>
<pre class="hljs"><code><ol>${html}</ol></code></pre>
</div>
`;
}
})
function extractFirstJson(text) {
let stack = [];
let startIndex = -1;
let endIndex = -1;
for (let i = 0; i < text.length; i++) {
const char = text[i];
if (char === '{') {
if (stack.length === 0) startIndex = i; // 记录第一个 '{' 的位置
stack.push(char);
} else if (char === '}') {
stack.pop();
if (stack.length === 0) {
endIndex = i; // 找到配对的 '}'
break;
}
}
}
if (startIndex !== -1 && endIndex !== -1) {
const jsonString = text.slice(startIndex, endIndex + 1);
try {
const jsonObject = JSON.parse(jsonString);
return jsonObject;
} catch (e) {
return null; // 如果不是有效的 JSON
}
}
return null; // 如果没有找到有效的 JSON 对象
}
function safeExtractJson(text) {
try {
const jsonObject = extractFirstJson(text);
return jsonObject
} catch (e) {
console.error('JSON 解析失败:', e);
}
return null;
}
export function clearJobMoreMap() { // 切换对话清空
jobMoreMap.clear()
}
export function parseMarkdown(content) {
if (!content) {
return [] //处理特殊情况,比如网络异常导致的响应的 content 的值为空
}
// 过滤掉<think>标签及其内容这些是AI内部思考过程不应该显示给用户
// 1. 处理原始标签(支持多行)
content = content.replace(/<\s*think\s*>[\s\S]*?<\s*\/\s*think\s*>/gi, '')
// 2. 处理HTML编码的标签
content = content.replace(/&lt;\s*think\s*&gt;[\s\S]*?&lt;\s*\/\s*think\s*&gt;/gi, '')
// 3. 处理部分编码的标签
content = content.replace(/&lt;\s*think\s*>/gi, '')
content = content.replace(/<\s*\/\s*think\s*&gt;/gi, '')
codeDataList = []
const unsafeHtml = md.render(content || '')
// 在markdown渲染后再次过滤确保没有遗漏
let filteredHtml = unsafeHtml
// 1. 处理原始标签(支持多行)
filteredHtml = filteredHtml.replace(/<\s*think\s*>[\s\S]*?<\s*\/\s*think\s*>/gi, '')
// 2. 处理HTML编码的标签
filteredHtml = filteredHtml.replace(/&lt;\s*think\s*&gt;[\s\S]*?&lt;\s*\/\s*think\s*&gt;/gi, '')
// 3. 处理部分编码的标签
filteredHtml = filteredHtml.replace(/&lt;\s*think\s*>/gi, '')
filteredHtml = filteredHtml.replace(/<\s*\/\s*think\s*&gt;/gi, '')
// 4. 单独处理剩余的think标签对
filteredHtml = filteredHtml.replace(/&lt;think&gt;/gi, '')
filteredHtml = filteredHtml.replace(/&lt;\/think&gt;/gi, '')
filteredHtml = filteredHtml.replace(/<think>/gi, '')
filteredHtml = filteredHtml.replace(/<\/think>/gi, '')
// 根据平台返回不同的内容格式
// 微信小程序返回rich-text组件支持的nodes格式
// H5直接返回HTML字符串避免HTML解析错误
if (process.env.UNI_PLATFORM === 'mp-weixin') {
try {
return parseHtml(filteredHtml)
} catch (error) {
console.error('HTML解析失败:', error)
// 解析失败时返回空数组,避免页面崩溃
return []
}
} else {
// H5端直接返回HTML字符串
return filteredHtml
}
}
import MarkdownIt from '@/lib/markdown-it.min.js';
import hljs from "@/lib/highlight/highlight-uni.min.js";
import parseHtml from '@/lib/html-parser.js';
// import DOMPurify from '@/lib/dompurify@3.2.4es.js';
export let codeDataList = []
export let jobMoreMap = new Map()
export let jobCardsList = []
const md = new MarkdownIt({
html: true, // 允许 HTML 标签
linkify: true, // 自动解析 URL
typographer: true, // 美化标点符号
tables: true,
breaks: true, // 让 \n 自动换行
langPrefix: 'language-', // 代码高亮前缀
// 如果结果以 <pre ... 开头,内部包装器则会跳过。
highlight: function(str, lang) {
if (lang === 'job-json') {
const result = safeExtractJson(str);
if (result) { // json解析成功
let jobId = result.appJobUrl;
// If appJobUrl contains 'jobId=', extract the value after it, otherwise use it directly
if (jobId && jobId.includes('jobId=')) {
jobId = jobId.split('jobId=')[1];
// 如果还有额外的参数只取jobId部分
if (jobId.includes('&')) {
jobId = jobId.split('&')[0];
}
}
console.log('Job JSON result:', result, 'Extracted jobId:', jobId);
// 确保jobId有效
if (!jobId || jobId === 'undefined' || jobId === 'null') {
console.error('Invalid jobId extracted:', jobId, 'from appJobUrl:', result.appJobUrl);
// 尝试从其他字段获取jobId
if (result.jobId) {
jobId = result.jobId;
console.log('Using jobId from result.jobId:', jobId);
}
}
// 添加到岗位卡片列表,供小程序端单独渲染
jobCardsList.push({
jobId,
jobTitle: result.jobTitle,
salary: result.salary,
location: result.location,
companyName: result.companyName,
education: result.education,
experience: result.experience
});
// 生成岗位卡片HTML注意微信小程序rich-text组件只支持部分HTML属性
// 使用普通的href属性微信小程序rich-text会将其转换为可点击链接
// 添加data-job-id属性方便获取jobId
// 为所有平台添加onclick事件微信小程序可能会忽略但H5端会生效
// 内联基础样式确保在所有平台上正确显示使用px单位以确保H5兼容性
let domContext = `<a class="custom-card" href="/packageA/pages/post/post?jobId=${jobId}" data-job-id="${jobId}" data-jobid="${jobId}" onclick="if(typeof uni !== 'undefined'){uni.navigateTo({url: '/packageA/pages/post/post?jobId=${jobId}'});return false;}" style="display: flex; flex-direction: column; margin-bottom: 11px; background: #FFFFFF; box-shadow: 0 0 4px rgba(0,0,0,0.04); border-radius: 10px; padding: 14px 12px; font-weight: 400; font-size: 14px; color: #333333; text-decoration: none; overflow: hidden; box-sizing: border-box; width: 100%; max-width: 100%;"><div class="card-title" style="font-weight: 600; display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px;"><span class="title-text" style="font-family: 'PingFangSC-Medium', 'PingFang SC', 'Helvetica Neue', Helvetica, Arial, 'Microsoft YaHei', sans-serif; font-size: 16px; line-height: 1.4; color: #333333; max-width: calc(100% - 80px); overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${result.jobTitle}</span><div class="card-salary" style="font-family: DIN-Medium; font-size: 16px; color: #4C6EFB; line-height: 1.4; font-weight: 500;">${result.salary}</div></div><div class="card-company" style="margin-bottom: 9px; font-size: 14px; color: #6C7282; line-height: 1.4; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">${result.location}·${result.companyName}</div><div class="card-info" style="display: flex; align-items: center; justify-content: space-between; padding-right: 20px;"><div class="info-item" style="display: flex; position: relative; align-items: center;"><div class="card-tag" style="font-weight: 400; font-size: 12px; color: #6C7282; width: fit-content; background: #F4F4F4; border-radius: 2px; padding: 3px 10px; margin-right: 10px; display: inline-flex; align-items: center; justify-content: center; height: 15px; line-height: 15px;">${result.education}</div><div class="card-tag" style="font-weight: 400; font-size: 12px; color: #6C7282; width: fit-content; background: #F4F4F4; border-radius: 2px; padding: 3px 10px; margin-right: 10px; display: inline-flex; align-items: center; justify-content: center; height: 15px; line-height: 15px;">${result.experience}</div></div><div class="info-item" style="color: #256BFA; font-size: 14px; padding-right: 5px; position: relative;">查看详情<div class="position-nav" style="position: absolute; right: -5px; top: 50%; transform: translateY(-50%);"></div></div></div></a>`
if (result.data) {
jobMoreMap.set(jobId, result.data)
domContext += `<a class="custom-more" href="/packageA/pages/moreJobs/moreJobs?jobId=${jobId}" data-job-id="${jobId}" data-jobid="${jobId}" onclick="if(typeof uni !== 'undefined'){uni.navigateTo({url: '/packageA/pages/moreJobs/moreJobs?jobId=${jobId}'});return false;}" style="display: flex; justify-content: center; align-items: center; color: #FFFFFF; background: linear-gradient(135deg, #256BFA 0%, #9E74FD 100%); border-radius: 25px; padding: 10px 16px; margin: 10px 0; font-size: 14px; font-weight: 600; box-shadow: 0 4px 12px rgba(37, 107, 250, 0.3); text-decoration: none; box-sizing: border-box; width: 100%;">查看更多岗位<div class="more-icon" style="width: 16px; height: 16px; background: url('@/static/svg/seemore.svg') center center no-repeat; background-size: 100% 100%; margin-left: 6px;"></div></a>`
}
return domContext
}
}
// 代码块
let preCode = ""
try {
preCode = hljs.highlightAuto(str).value
} catch (err) {
preCode = md.utils.escapeHtml(str);
}
// 以换行进行分割 , 按行拆分代码
const lines = preCode.split(/\n/).slice(0, -1);
const html = lines
.map((line, index) =>
line ?
`<li><span class="line-num" data-line="${index + 1}"></span>${line}</li>` :
'<li></li>'
)
.join('');
// 代码复制功能
const cacheIndex = codeDataList.length;
codeDataList.push(str);
return `
<div class="code-container">
<div class="code-header">
<span class="lang-label">${lang || 'plaintext'}</span>
<a class="copy-btn" data-copy-index="${cacheIndex}">复制代码</a>
</div>
<pre class="hljs"><code><ol>${html}</ol></code></pre>
</div>
`;
}
})
function extractFirstJson(text) {
let stack = [];
let startIndex = -1;
let endIndex = -1;
for (let i = 0; i < text.length; i++) {
const char = text[i];
if (char === '{') {
if (stack.length === 0) startIndex = i; // 记录第一个 '{' 的位置
stack.push(char);
} else if (char === '}') {
stack.pop();
if (stack.length === 0) {
endIndex = i; // 找到配对的 '}'
break;
}
}
}
if (startIndex !== -1 && endIndex !== -1) {
const jsonString = text.slice(startIndex, endIndex + 1);
try {
const jsonObject = JSON.parse(jsonString);
return jsonObject;
} catch (e) {
return null; // 如果不是有效的 JSON
}
}
return null; // 如果没有找到有效的 JSON 对象
}
function safeExtractJson(text) {
try {
const jsonObject = extractFirstJson(text);
return jsonObject
} catch (e) {
console.error('JSON 解析失败:', e);
}
return null;
}
export function clearJobMoreMap() { // 切换对话清空
jobMoreMap.clear()
}
export function parseMarkdown(content) {
if (!content) {
return [] //处理特殊情况,比如网络异常导致的响应的 content 的值为空
}
// 过滤掉<think>标签及其内容这些是AI内部思考过程不应该显示给用户
// 1. 处理原始标签(支持多行)
content = content.replace(/<\s*think\s*>[\s\S]*?<\s*\/\s*think\s*>/gi, '')
// 2. 处理HTML编码的标签
content = content.replace(/&lt;\s*think\s*&gt;[\s\S]*?&lt;\s*\/\s*think\s*&gt;/gi, '')
// 3. 处理部分编码的标签
content = content.replace(/&lt;\s*think\s*>/gi, '')
content = content.replace(/<\s*\/\s*think\s*&gt;/gi, '')
codeDataList = []
jobCardsList = [] // 清空岗位卡片列表,避免重复
const unsafeHtml = md.render(content || '')
// 在markdown渲染后再次过滤确保没有遗漏
let filteredHtml = unsafeHtml
// 1. 处理原始标签(支持多行)
filteredHtml = filteredHtml.replace(/<\s*think\s*>[\s\S]*?<\s*\/\s*think\s*>/gi, '')
// 2. 处理HTML编码的标签
filteredHtml = filteredHtml.replace(/&lt;\s*think\s*&gt;[\s\S]*?&lt;\s*\/\s*think\s*&gt;/gi, '')
// 3. 处理部分编码的标签
filteredHtml = filteredHtml.replace(/&lt;\s*think\s*>/gi, '')
filteredHtml = filteredHtml.replace(/<\s*\/\s*think\s*&gt;/gi, '')
// 4. 单独处理剩余的think标签对
filteredHtml = filteredHtml.replace(/&lt;think&gt;/gi, '')
filteredHtml = filteredHtml.replace(/&lt;\/think&gt;/gi, '')
filteredHtml = filteredHtml.replace(/<think>/gi, '')
filteredHtml = filteredHtml.replace(/<\/think>/gi, '')
// 根据平台返回不同的内容格式
// 微信小程序返回rich-text组件支持的nodes格式
// H5直接返回HTML字符串避免HTML解析错误
if (process.env.UNI_PLATFORM === 'mp-weixin') {
try {
return parseHtml(filteredHtml)
} catch (error) {
console.error('HTML解析失败:', error)
// 解析失败时返回空数组,避免页面崩溃
return []
}
} else {
// H5端直接返回HTML字符串
return filteredHtml
}
}

View File

@@ -51,11 +51,100 @@ function StreamRequestMiniProgram(url, data = {}, onDataReceived, onError, onCom
}
});
// UTF-8解码函数用于微信小程序真机环境
function utf8Decode(uint8Array) {
let result = '';
let i = 0;
const len = uint8Array.length;
while (i < len) {
const byte1 = uint8Array[i];
// 1字节字符 (0xxxxxxx)
if (byte1 < 0x80) {
result += String.fromCharCode(byte1);
i++;
}
// 2字节字符 (110xxxxx 10xxxxxx)
else if (byte1 >= 0xC0 && byte1 < 0xE0) {
if (i + 1 < len) {
const byte2 = uint8Array[i + 1];
if (byte2 >= 0x80 && byte2 < 0xC0) {
const codePoint = ((byte1 & 0x1F) << 6) | (byte2 & 0x3F);
result += String.fromCharCode(codePoint);
i += 2;
continue;
}
}
// 无效的UTF-8序列跳过
result += '<27>';
i++;
}
// 3字节字符 (1110xxxx 10xxxxxx 10xxxxxx)
else if (byte1 >= 0xE0 && byte1 < 0xF0) {
if (i + 2 < len) {
const byte2 = uint8Array[i + 1];
const byte3 = uint8Array[i + 2];
if ((byte2 >= 0x80 && byte2 < 0xC0) && (byte3 >= 0x80 && byte3 < 0xC0)) {
const codePoint = ((byte1 & 0x0F) << 12) | ((byte2 & 0x3F) << 6) | (byte3 & 0x3F);
result += String.fromCharCode(codePoint);
i += 3;
continue;
}
}
// 无效的UTF-8序列跳过
result += '<27>';
i++;
}
// 4字节字符 (11110xxx 10xxxxxx 10xxxxxx 10xxxxxx)
else if (byte1 >= 0xF0 && byte1 < 0xF8) {
if (i + 3 < len) {
const byte2 = uint8Array[i + 1];
const byte3 = uint8Array[i + 2];
const byte4 = uint8Array[i + 3];
if ((byte2 >= 0x80 && byte2 < 0xC0) && (byte3 >= 0x80 && byte3 < 0xC0) && (byte4 >= 0x80 && byte4 < 0xC0)) {
let codePoint = ((byte1 & 0x07) << 18) | ((byte2 & 0x3F) << 12) | ((byte3 & 0x3F) << 6) | (byte4 & 0x3F);
// 处理UTF-16代理对
if (codePoint >= 0x10000) {
codePoint -= 0x10000;
const highSurrogate = (codePoint >> 10) + 0xD800;
const lowSurrogate = (codePoint & 0x3FF) + 0xDC00;
result += String.fromCharCode(highSurrogate, lowSurrogate);
} else {
result += String.fromCharCode(codePoint);
}
i += 4;
continue;
}
}
// 无效的UTF-8序列跳过
result += '<27>';
i++;
}
// 无效的UTF-8序列跳过
else {
result += '<27>';
i++;
}
}
return result;
}
// 监听分块数据
requestTask.onChunkReceived((res) => {
try {
const decoder = new TextDecoder('utf-8');
const chunk = decoder.decode(new Uint8Array(res.data));
// 微信小程序兼容处理微信小程序不支持TextDecoder使用自定义UTF-8解码
let chunk = '';
if (typeof TextDecoder !== 'undefined') {
// 支持TextDecoder的环境如开发者工具
const decoder = new TextDecoder('utf-8');
chunk = decoder.decode(new Uint8Array(res.data));
} else {
// 微信小程序真机环境使用自定义UTF-8解码函数
const uint8Array = new Uint8Array(res.data);
chunk = utf8Decode(uint8Array);
}
console.log('📦 收到分块数据:', chunk);
buffer += chunk;