diff --git a/components/CustomTabBar/CustomTabBar.vue b/components/CustomTabBar/CustomTabBar.vue index 444ecd7..a393352 100644 --- a/components/CustomTabBar/CustomTabBar.vue +++ b/components/CustomTabBar/CustomTabBar.vue @@ -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', @@ -175,7 +175,8 @@ const switchTab = (item, index) => { const loginRequiredPages = [ '/packageA/pages/job/publishJob', '/pages/mine/mine', - '/pages/mine/company-mine' + '/pages/mine/company-mine', + '/pages/msglog/msglog' ]; if (loginRequiredPages.includes(item.path)) { @@ -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; } } diff --git a/components/md-render/md-render.vue b/components/md-render/md-render.vue index 1865468..7568564 100644 --- a/components/md-render/md-render.vue +++ b/components/md-render/md-render.vue @@ -2,17 +2,44 @@ - + + + + + + + + + + {{ card.jobTitle }} + {{ card.salary }} + + {{ card.location }}·{{ card.companyName }} + + + {{ card.education }} + {{ card.experience }} + + 查看详情 + + + + - + diff --git a/components/wxAuthLogin/WxAuthLogin.vue b/components/wxAuthLogin/WxAuthLogin.vue index 5053a47..a251881 100644 --- a/components/wxAuthLogin/WxAuthLogin.vue +++ b/components/wxAuthLogin/WxAuthLogin.vue @@ -40,6 +40,22 @@ + + + 请选择机构类型 + + + {{ option.label }} + + + + @@ -99,21 +115,50 @@ + + \ No newline at end of file diff --git a/packageA/pages/job/publishJob.vue b/packageA/pages/job/publishJob.vue index 5cc13dc..977e0b6 100644 --- a/packageA/pages/job/publishJob.vue +++ b/packageA/pages/job/publishJob.vue @@ -76,6 +76,18 @@ {{ selectedEducation || '请选择学历要求' }} + + 人员类型 + + {{ selectedStaffType || '请选择人员类型' }} + + 工作经验 { } 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: '请选择工作区县' }, diff --git a/packageA/pages/post/component/videoPlayer.vue b/packageA/pages/post/component/videoPlayer.vue index d118dba..862d7eb 100644 --- a/packageA/pages/post/component/videoPlayer.vue +++ b/packageA/pages/post/component/videoPlayer.vue @@ -32,9 +32,9 @@ - - 关闭 + + 关闭 @@ -180,7 +180,7 @@ defineExpose({ open }); justify-content: space-around; } -.controls text { +.controls .control-text { color: #fff; font-size: 24rpx; padding: 8rpx 16rpx; diff --git a/packageA/pages/post/post.vue b/packageA/pages/post/post.vue index 16514e1..ce931eb 100644 --- a/packageA/pages/post/post.vue +++ b/packageA/pages/post/post.vue @@ -218,10 +218,22 @@ + + + + + {{ jobInfo.isApply === 1 ? '确认取消投递' : '确认投递' }} + {{ jobInfo.isApply === 1 ? '确定要取消投递此职位吗?' : '确定要投递此职位吗?' }} + + 取消 + 确认 + + + @@ -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: '' + }; +}); diff --git a/packageRc/pages/index/index.vue b/packageRc/pages/index/index.vue index 6c8799c..103c6cc 100644 --- a/packageRc/pages/index/index.vue +++ b/packageRc/pages/index/index.vue @@ -1,11 +1,11 @@ @@ -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%); + } +} \ No newline at end of file diff --git a/packageRc/static/activeTangle.png b/packageRc/static/activeTangle.png new file mode 100644 index 0000000..18e2824 Binary files /dev/null and b/packageRc/static/activeTangle.png differ diff --git a/packageRc/static/gw.png b/packageRc/static/gw.png new file mode 100644 index 0000000..3319d6f Binary files /dev/null and b/packageRc/static/gw.png differ diff --git a/packageRc/static/pageBgIndex.png b/packageRc/static/pageBgIndex.png new file mode 100644 index 0000000..5158235 Binary files /dev/null and b/packageRc/static/pageBgIndex.png differ diff --git a/packageRc/static/zc.png b/packageRc/static/zc.png new file mode 100644 index 0000000..89ea0d1 Binary files /dev/null and b/packageRc/static/zc.png differ diff --git a/pages.json b/pages.json index 516b86c..58fc301 100644 --- a/pages.json +++ b/pages.json @@ -39,7 +39,7 @@ { "path": "pages/chat/chat", "style": { - "navigationBarTitleText": "AI+", + "navigationBarTitleText": "智能客服", "navigationBarBackgroundColor": "#4778EC", "navigationBarTextStyle": "white", "enablePullDownRefresh": false @@ -312,6 +312,13 @@ "navigationBarBackgroundColor": "#4778EC", "navigationBarTextStyle": "white" } + }, + { + "path": "pages/cancelApplication/cancelApplication", + "style": { + "navigationBarTitleText": "取消投递", + "navigationBarBackgroundColor": "#FFFFFF" + } } ] }, diff --git a/pages/chat/chat.vue b/pages/chat/chat.vue index e43c8fa..1c42cab 100644 --- a/pages/chat/chat.vue +++ b/pages/chat/chat.vue @@ -61,6 +61,7 @@ {{ config.appInfo.areaName }}岗位推荐 + diff --git a/pages/chat/components/WaveDisplay.vue b/pages/chat/components/WaveDisplay.vue index b0d190c..30a1dd5 100644 --- a/pages/chat/components/WaveDisplay.vue +++ b/pages/chat/components/WaveDisplay.vue @@ -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; } }; diff --git a/pages/chat/components/ai-paging.vue b/pages/chat/components/ai-paging.vue index 48cfa47..24352c0 100644 --- a/pages/chat/components/ai-paging.vue +++ b/pages/chat/components/ai-paging.vue @@ -133,6 +133,20 @@ {{ recognizedText }} {{ lastFinalText }} + + + + + + + + + + + 正在识别语音... + + + @@ -175,9 +189,6 @@ @touchmove="handleTouchMove" @touchend="handleTouchEnd" @touchcancel="handleTouchCancel" - :catchtouchstart="true" - :catchtouchmove="true" - :catchtouchend="true" v-show="isVoice" type="default" > @@ -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; diff --git a/pages/index/components/index-one.vue b/pages/index/components/index-one.vue index ac83904..cdd8b45 100644 --- a/pages/index/components/index-one.vue +++ b/pages/index/components/index-one.vue @@ -1,5 +1,5 @@