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));
+ }
});
}
}