diff --git a/common/globalFunction.js b/common/globalFunction.js index 7434ea6..25e86ad 100644 --- a/common/globalFunction.js +++ b/common/globalFunction.js @@ -630,6 +630,10 @@ export function sm4Encrypt(key, value, mode = "hex") { } } +export function reloadBrowser() { + window.location.reload() +} + export const $api = { msg, @@ -679,5 +683,6 @@ export default { aes_Decrypt, sm2_Decrypt, sm2_Encrypt, - safeReLaunch + safeReLaunch, + reloadBrowser } \ No newline at end of file diff --git a/hook/useSystemPlayer.js b/hook/useSystemPlayer.js deleted file mode 100644 index ee164c2..0000000 --- a/hook/useSystemPlayer.js +++ /dev/null @@ -1,158 +0,0 @@ -import { - ref, - onUnmounted, - readonly -} from 'vue'; - -const defaultExtractSpeechText = (text) => text; - - -export function useTTSPlayer() { - const synth = window.speechSynthesis; - const isSpeaking = ref(false); - const isPaused = ref(false); - const utteranceRef = ref(null); - - const cleanup = () => { - isSpeaking.value = false; - isPaused.value = false; - utteranceRef.value = null; - }; - - /** - * @param {string} text - The text to be spoken. - * @param {object} [options] - Optional settings for the speech. - * @param {string} [options.lang] - Language (e.g., 'en-US', 'es-ES'). - * @param {number} [options.rate] - Speed (0.1 to 10, default 1). - * @param {number} [options.pitch] - Pitch (0 to 2, default 1). - * @param {SpeechSynthesisVoice} [options.voice] - A specific voice object. - * @param {function(string): string} [options.extractSpeechText] - A function to filter/clean the text before speaking. - */ - const speak = (text, options = {}) => { - if (!synth) { - console.error('SpeechSynthesis API is not supported in this browser.'); - return; - } - - if (isSpeaking.value) { - synth.cancel(); - } - - const filteredText = extractSpeechText(text); - - if (!filteredText || typeof filteredText !== 'string' || filteredText.trim() === '') { - console.warn('Text to speak is empty after filtering.'); - cleanup(); // Ensure state is clean - return; - } - - const newUtterance = new SpeechSynthesisUtterance(filteredText); // Use filtered text - utteranceRef.value = newUtterance; - - newUtterance.lang = 'zh-CN'; - newUtterance.rate = options.rate || 1; - newUtterance.pitch = options.pitch || 1; - if (options.voice) { - newUtterance.voice = options.voice; - } - - newUtterance.onstart = () => { - isSpeaking.value = true; - isPaused.value = false; - }; - - newUtterance.onpause = () => { - isPaused.value = true; - }; - newUtterance.onresume = () => { - isPaused.value = false; - }; - newUtterance.onend = () => { - cleanup(); - }; - newUtterance.onerror = (event) => { - console.error('SpeechSynthesis Error:', event.error); - cleanup(); - }; - - synth.speak(newUtterance); - }; - - const pause = () => { - if (synth && isSpeaking.value && !isPaused.value) { - synth.pause(); - } - }; - - const resume = () => { - if (synth && isPaused.value) { - synth.resume(); - } - }; - - const cancelAudio = () => { - if (synth) { - synth.cancel(); - } - cleanup(); - }; - - onUnmounted(() => { - cancelAudio(); - }); - - return { - speak, - pause, - resume, - cancelAudio, - isSpeaking: readonly(isSpeaking), - isPaused: readonly(isPaused), - }; -} - -function extractSpeechText(markdown) { - const jobRegex = /``` job-json\s*({[\s\S]*?})\s*```/g; - const jobs = []; - let match; - let lastJobEndIndex = 0; - let firstJobStartIndex = -1; - - // 提取岗位 json 数据及前后位置 - while ((match = jobRegex.exec(markdown)) !== null) { - const jobStr = match[1]; - try { - const job = JSON.parse(jobStr); - jobs.push(job); - if (firstJobStartIndex === -1) { - firstJobStartIndex = match.index; - } - lastJobEndIndex = jobRegex.lastIndex; - } catch (e) { - console.warn('JSON 解析失败', e); - } - } - - // 提取引导语(第一个 job-json 之前的文字) - const guideText = firstJobStartIndex > 0 ? - markdown.slice(0, firstJobStartIndex).trim() : - ''; - - // 提取结束语(最后一个 job-json 之后的文字) - const endingText = lastJobEndIndex < markdown.length ? - markdown.slice(lastJobEndIndex).trim() : - ''; - - // 岗位信息格式化为语音文本 - const jobTexts = jobs.map((job, index) => { - return `第 ${index + 1} 个岗位,岗位名称是:${job.jobTitle},公司是:${job.companyName},薪资:${job.salary},地点:${job.location},学历要求:${job.education},经验要求:${job.experience}。`; - }); - - // 拼接总语音内容 - const finalTextParts = []; - if (guideText) finalTextParts.push(guideText); - finalTextParts.push(...jobTexts); - if (endingText) finalTextParts.push(endingText); - - return finalTextParts.join('\n'); -} \ No newline at end of file diff --git a/pages/chat/components/ai-paging.vue b/pages/chat/components/ai-paging.vue index e94a544..e4868e2 100644 --- a/pages/chat/components/ai-paging.vue +++ b/pages/chat/components/ai-paging.vue @@ -273,9 +273,7 @@ import useScreenStore from '@/stores/useScreenStore' const screenStore = useScreenStore(); // 系统功能hook和阿里云hook import { useAudioRecorder } from '@/hook/useRealtimeRecorder.js'; -// import { useAudioRecorder } from '@/hook/useSystemSpeechReader.js'; import { useTTSPlayer } from '@/hook/useTTSPlayer.js'; -// import { useTTSPlayer } from '@/hook/useSystemPlayer.js'; // 全局 const { $api, navTo, throttle } = inject('globalFunction'); const emit = defineEmits(['onConfirm']); diff --git a/pages/index/components/index-refactor.vue b/pages/index/components/index-refactor.vue index 242a17e..185c693 100644 --- a/pages/index/components/index-refactor.vue +++ b/pages/index/components/index-refactor.vue @@ -11,7 +11,12 @@ - + 请告诉我想找什么工作 @@ -20,7 +25,7 @@ - + @@ -56,7 +61,7 @@ - + 简历匹配职位 @@ -65,7 +70,7 @@ - + 附近工作 好岗职等你来 @@ -254,11 +259,11 @@ import { reactive, inject, watch, ref, onMounted, watchEffect, nextTick, getCurrentInstance } from 'vue'; import img from '@/static/icon/filter.png'; import dictLabel from '@/components/dict-Label/dict-Label.vue'; -const { $api, navTo, vacanciesTo, formatTotal, throttle } = inject('globalFunction'); +const { $api, navTo, vacanciesTo, formatTotal, throttle, reloadBrowser } = inject('globalFunction'); import { onLoad, onShow } from '@dcloudio/uni-app'; import { storeToRefs } from 'pinia'; import useUserStore from '@/stores/useUserStore'; -const { userInfo, hasLogin ,isMachineEnv} = storeToRefs(useUserStore()); +const { userInfo, hasLogin, isMachineEnv } = storeToRefs(useUserStore()); import useDictStore from '@/stores/useDictStore'; const { getTransformChildren, oneDictData } = useDictStore(); import useLocationStore from '@/stores/useLocationStore'; @@ -271,7 +276,6 @@ const recommedIndexDb = useRecommedIndexedDBStore(); import config from '@/config'; import AIMatch from './AIMatch.vue'; - const { proxy } = getCurrentInstance(); const maskFirstEntry = ref(true); @@ -363,7 +367,7 @@ onMounted(() => { let firstEntry = uni.getStorageSync('firstEntry') === false ? false : true; // 默认未读 maskFirstEntry.value = firstEntry; getMatchTags(); - console.log(isMachineEnv.value,'+++++++++') + // console.log(isMachineEnv.value, '+++++++++'); }); async function getMatchTags() { diff --git a/stores/useLocationStore.js b/stores/useLocationStore.js index 17ca3d5..ab7b2fe 100644 --- a/stores/useLocationStore.js +++ b/stores/useLocationStore.js @@ -9,6 +9,11 @@ import { } from '@/common/globalFunction.js' import config from '../config'; +const defalutLongLat = { + longitude: 120.382665, + latitude: 36.066938 +} + const useLocationStore = defineStore("location", () => { // 定义状态 const longitudeVal = ref(null) // 经度 @@ -25,46 +30,39 @@ const useLocationStore = defineStore("location", () => { resole(data) }, fail: function(data) { - longitudeVal.value = 120.382665 - latitudeVal.value = 36.066938 - resole({ - longitude: 120.382665, - latitude: 36.066938 - }) + longitudeVal.value = defalutLongLat.longitude + latitudeVal.value = defalutLongLat.latitude + resole(defalutLongLat) msg('用户位置获取失败') + console.log('失败3', data) } }) } else { uni.getLocation({ type: 'gcj02', - highAccuracyExpireTime: 3000, - isHighAccuracy: true, - timeout: 2000, + // highAccuracyExpireTime: 3000, + // isHighAccuracy: true, + // timeout: 2000, success: function(data) { longitudeVal.value = Number(data.longitude) latitudeVal.value = Number(data.latitude) resole(data) }, fail: function(data) { - longitudeVal.value = 120.382665 - latitudeVal.value = 36.066938 - resole({ - longitude: 120.382665, - latitude: 36.066938 - }) + longitudeVal.value = defalutLongLat.longitude + latitudeVal.value = defalutLongLat.latitude + resole(defalutLongLat) msg('用户位置获取失败') + console.log('失败2', data) } }); } } catch (e) { - longitudeVal.value = 120.382665 - latitudeVal.value = 36.066938 - resole({ - longitude: 120.382665, - latitude: 36.066938 - }) + longitudeVal.value = defalutLongLat.longitude + latitudeVal.value = defalutLongLat.latitude + resole(defalutLongLat) msg('测试环境,使用模拟定位') - console.log('失败', data) + console.log('失败1', e) } }) } @@ -84,7 +82,7 @@ const useLocationStore = defineStore("location", () => { latitudeVal } -},{ +}, { unistorage: true, }) diff --git a/utils/fileValidator.js b/utils/fileValidator.js index 54034df..305f176 100644 --- a/utils/fileValidator.js +++ b/utils/fileValidator.js @@ -68,6 +68,22 @@ export class FileValidator { } } + /** + * 改进版:检查是否为有效的 UTF-8 文本 + */ + _isValidUTF8(buffer) { + try { + // fatal: true 会在遇到无效编码时抛出错误,而不是用 替换 + const decoder = new TextDecoder('utf-8', { + fatal: true + }); + decoder.decode(buffer); + return true; + } catch (e) { + return false; + } + } + /** * 辅助:ArrayBuffer 转 Hex 字符串 */ @@ -79,23 +95,79 @@ export class FileValidator { } /** - * 辅助:纯文本抽样检测 + * 【新增】统计 CSV 行数(严谨版:忽略引号内的换行符) + * 性能:对于 10MB 文件,现代浏览器处理通常在 100ms 以内 */ - _isCleanText(buffer) { - const bytes = new Uint8Array(buffer); - const checkLen = Math.min(bytes.length, 1000); - let suspiciousCount = 0; + _countCSVRows(buffer) { + const decoder = new TextDecoder('utf-8'); + const text = decoder.decode(buffer); - for (let i = 0; i < checkLen; i++) { - const byte = bytes[i]; - // 允许常见控制符: 9(Tab), 10(LF), 13(CR) - // 0-31 范围内其他的通常是二进制控制符 - if (byte < 32 && ![9, 10, 13].includes(byte)) { - suspiciousCount++; + let rowCount = 0; + let inQuote = false; + let len = text.length; + + // 遍历每一个字符 + for (let i = 0; i < len; i++) { + const char = text[i]; + + // 切换引号状态 + if (char === '"') { + inQuote = !inQuote; + } + // 只有在非引号状态下的换行符,才算作一行结束 + else if (char === '\n' && !inQuote) { + rowCount++; } } - // 如果可疑字符占比 < 5%,认为是纯文本 - return suspiciousCount / checkLen < 0.05; + + // 处理最后一行没有换行符的情况(且文件不为空) + if (len > 0 && text[len - 1] !== '\n') { + rowCount++; + } + + return rowCount; + } + + + /** + * 【核心】:校验纯文本内容 + * 1. 检查是否包含乱码 (非 UTF-8) + * 2. 针对特定格式 (JSON) 进行语法解析 + */ + _validateTextContent(buffer, extension) { + // 1. 尝试解码为 UTF-8 + let contentStr = ''; + try { + const decoder = new TextDecoder('utf-8', { + fatal: true + }); + contentStr = decoder.decode(buffer); + } catch (e) { + // 如果解码失败,说明包含非文本的二进制数据 + console.warn('UTF-8 解码失败', e); + return false; + } + + // 2. 检查是否存在过多的空字符 (二进制文件特征) + // 某些二进制文件可能勉强通过 UTF-8 解码,但会包含大量 \0 + if (contentStr.includes('\u0000')) { + return false; + } + + // 3. 针对特定后缀进行语法校验 (可选,更严格) + if (extension === 'json') { + try { + JSON.parse(contentStr); + } catch (e) { + console.warn('无效的 JSON 格式'); + return false; + } + } + + // 如果是 CSV,可以简单检查行数(可选) + // if (extension === 'csv') { ... } + + return true; } /** @@ -132,10 +204,23 @@ export class FileValidator { // 分支处理:纯文本 vs 二进制 if (expectedMagic === 'TYPE_TEXT') { - if (this._isCleanText(buffer)) { + if (this._validateTextContent(buffer, extension)) { isSafe = true; } else { - return reject(`文件异常:.${extension} 文件包含非法二进制内容`); + // 细化报错信息 + if (extension === 'json') { + return reject(`文件异常:不是有效的 JSON 文件`); + } + return reject(`文件异常:.${extension} 包含非法二进制内容或编码错误`); + } + + // 【新增】专门针对 CSV 的行数检查 + if (extension === 'csv' && this.csvMaxRows > 0) { + const rows = this._countCSVRows(buffer); + // 注意:这里通常把表头也算作 1 行,如果不算表头可以将 limit + 1 + if (rows > this.csvMaxRows) { + return reject(`CSV 行数超出限制 (当前 ${rows} 行,最大允许 ${this.csvMaxRows} 行)`); + } } } else { // 获取文件头 Hex (读取足够长的字节以覆盖最长的魔数,PNG需8字节) @@ -155,7 +240,13 @@ export class FileValidator { reader.onerror = () => reject('文件读取失败,无法校验'); // 读取前 1KB 进行判断 - reader.readAsArrayBuffer(file.slice(0, 1024)); + if (expectedMagic === 'TYPE_TEXT' && extension === 'json') { + // JSON 必须读全量才能 parse,建议限制 JSON 文件大小 + reader.readAsArrayBuffer(file); + } else { + // 图片/普通文本 读取前 2KB 足够判断头部和编码特征 + reader.readAsArrayBuffer(file.slice(0, 2048)); + } }); } }