diff --git a/App.vue b/App.vue index 620f282..49182bf 100644 --- a/App.vue +++ b/App.vue @@ -24,6 +24,7 @@ onLaunch((options) => { getUserInfo(); useUserStore().changMiniProgramAppStatus(false); useUserStore().changMachineEnv(false); + useLocationStore().getLocationLoop()//循环获取定位 return; } if (isY9MachineType()) { @@ -32,7 +33,18 @@ onLaunch((options) => { useUserStore().logOutApp(); useUserStore().changMiniProgramAppStatus(true); useUserStore().changMachineEnv(true); - useLocationStore().getLocation(); + (function loop() { + console.log('📍一体机尝试获取定位') + useLocationStore().getLocation().then(({longitude,latitude})=>{ + console.log(`✅一体机获取定位成功:lng:${longitude},lat${latitude}`) + }) + .catch(err=>{ + console.log('❌一体机获取定位失败,30s后尝试重新获取') + setTimeout(() => { + loop() + }, 3000); + }) + })() uQRListen = new IncreaseRevie(); inactivityManager = new GlobalInactivityManager(handleInactivity, 60 * 1000); inactivityManager.start(); @@ -40,6 +52,7 @@ onLaunch((options) => { } // 正式上线去除此方法 console.warn('浏览器环境'); + useLocationStore().getLocationLoop()//循环获取定位 useUserStore().changMiniProgramAppStatus(true); useUserStore().changMachineEnv(false); useUserStore().initSeesionId(); //更新 @@ -57,6 +70,7 @@ onLaunch((options) => { onMounted(() => {}); + onShow(() => { console.log('App Show'); }); diff --git a/packageA/pages/personalInfo/personalInfo.vue b/packageA/pages/personalInfo/personalInfo.vue index 952b2d6..9eb1737 100644 --- a/packageA/pages/personalInfo/personalInfo.vue +++ b/packageA/pages/personalInfo/personalInfo.vue @@ -82,6 +82,7 @@ const { userInfo } = storeToRefs(useUserStore()); const { getUserResume } = useUserStore(); const { dictLabel, oneDictData } = useDictStore(); const openSelectPopup = inject('openSelectPopup'); +import { FileValidator } from '@/static/js/fileValidator.js'; //文件校验 const percent = ref('0%'); const state = reactive({ @@ -278,15 +279,26 @@ function selectAvatar() { sizeType: ['original', 'compressed'], sourceType: ['album', 'camera'], count: 1, - success: ({ tempFilePaths, tempFiles }) => { - $api.uploadFile(tempFilePaths[0], true) - .then((res) => { - res = JSON.parse(res); - if (res.msg) fromValue.avatar = res.msg; - }) - .catch((err) => { - $api.msg('上传失败'); - }); + success: async (res) => { + const tempFilePaths = res.tempFilePaths; + const file = res.tempFiles[0]; + + const imageValidator = new FileValidator(); + + try { + await imageValidator.validate(file); + + $api.uploadFile(tempFilePaths[0], true) + .then((res) => { + res = JSON.parse(res); + if (res.msg) fromValue.avatar = res.msg; + }) + .catch((err) => { + $api.msg('上传失败'); + }); + } catch (error) { + $api.msg(error); + } }, fail: (error) => {}, }); diff --git a/pages/chat/components/ai-paging.vue b/pages/chat/components/ai-paging.vue index e4868e2..a72a13f 100644 --- a/pages/chat/components/ai-paging.vue +++ b/pages/chat/components/ai-paging.vue @@ -281,6 +281,8 @@ const { messages, isTyping, textInput, chatSessionID } = storeToRefs(useChatGrou import successIcon from '@/static/icon/success.png'; import useUserStore from '@/stores/useUserStore'; const { isMachineEnv } = storeToRefs(useUserStore()); + +import {FileValidator} from "@/static/js/fileValidator.js" //文件校验 // hook // 语音识别 const { @@ -536,22 +538,29 @@ function uploadCamera(type = 'camera') { count: 1, //默认9 sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有 sourceType: [type], //从相册选择 - success: function (res) { + success: async (res)=> { const tempFilePaths = res.tempFilePaths; const file = res.tempFiles[0]; - // 继续上传 - $api.uploadFile(tempFilePaths[0], true).then((resData) => { - resData = JSON.parse(resData); - console.log(file.type,'++') - if (isImage(file.type)) { - filesList.value.push({ - url: resData.msg, - type: file.type, - name: file.name, - }); - textInput.value = state.uploadFileTips; - } - }); + + const imageValidator = new FileValidator() + try { + await imageValidator.validate(file) + + $api.uploadFile(tempFilePaths[0], true).then((resData) => { + resData = JSON.parse(resData); + console.log(file.type,'++') + if (isImage(file.type)) { + filesList.value.push({ + url: resData.msg, + type: file.type, + name: file.name, + }); + textInput.value = state.uploadFileTips; + } + }); + } catch (error) { + $api.msg(error) + } }, }); } @@ -560,25 +569,30 @@ function getUploadFile(type = 'camera') { if (VerifyNumberFiles()) return; uni.chooseFile({ count: 1, - success: (res) => { + success: async(res) => { const tempFilePaths = res.tempFilePaths; const file = res.tempFiles[0]; const allowedTypes = config.allowedFileTypes || []; const size = $api.formatFileSize(file.size); - if (!allowedTypes.includes(file.type)) { - return $api.msg('仅支持 txt md word pdf ppt csv excel 格式类型'); - } - // 继续上传 - $api.uploadFile(tempFilePaths[0], true).then((resData) => { - resData = JSON.parse(resData); - filesList.value.push({ - url: resData.msg, - type: file.type, - name: file.name, - size: size, + + const imageValidator = new FileValidator({allowedExtensions:config.allowedFileTypes}) + + try{ + await imageValidator.validate(file) + + $api.uploadFile(tempFilePaths[0], true).then((resData) => { + resData = JSON.parse(resData); + filesList.value.push({ + url: resData.msg, + type: file.type, + name: file.name, + size: size, + }); + textInput.value = state.uploadFileTips; }); - textInput.value = state.uploadFileTips; - }); + }catch(error){ + $api.msg(error) + } }, }); } diff --git a/pages/index/components/index-refactor.vue b/pages/index/components/index-refactor.vue index 185c693..9bd0d5e 100644 --- a/pages/index/components/index-refactor.vue +++ b/pages/index/components/index-refactor.vue @@ -470,7 +470,6 @@ const { columnCount, columnSpace } = useColumnCount(() => { getJobRecommend('refresh'); nextTick(() => { waterfallsFlowRef.value?.refresh?.(); - useLocationStore().getLocation(); }); }); diff --git a/pages/index/components/index-two.vue b/pages/index/components/index-two.vue index 4ea08ea..14c80c0 100644 --- a/pages/index/components/index-two.vue +++ b/pages/index/components/index-two.vue @@ -129,7 +129,6 @@ const { columnCount, columnSpace } = useColumnCount(() => { pageSize.value = 10 * (columnCount.value - 1); nextTick(() => { waterfallsFlowRef.value?.refresh?.(); - useLocationStore().getLocation(); }); }); diff --git a/pages/search/search.vue b/pages/search/search.vue index 0b27ae2..75fdce9 100644 --- a/pages/search/search.vue +++ b/pages/search/search.vue @@ -151,7 +151,6 @@ const { columnCount, columnSpace } = useColumnCount(() => { pageSize.value = 10 * (columnCount.value - 1); nextTick(() => { waterfallsFlowRef.value?.refresh?.(); - useLocationStore().getLocation(); }); }); diff --git a/static/js/fileValidator.js b/static/js/fileValidator.js new file mode 100644 index 0000000..bace6b7 --- /dev/null +++ b/static/js/fileValidator.js @@ -0,0 +1,170 @@ +const KNOWN_SIGNATURES = { + png: '89504E470D0A1A0A', + jpg: 'FFD8FF', + jpeg: 'FFD8FF', + gif: '47494638', + webp: '52494646', + docx: '504B0304', + xlsx: '504B0304', + pptx: '504B0304', + doc: 'D0CF11E0', + xls: 'D0CF11E0', + ppt: 'D0CF11E0', + pdf: '25504446', + txt: 'TYPE_TEXT', + csv: 'TYPE_TEXT', + md: 'TYPE_TEXT', + json: 'TYPE_TEXT', +}; +export class FileValidator { + version = '1.0.0'; + signs = Object.keys(KNOWN_SIGNATURES); + constructor(options = {}) { + this.maxSizeMB = options.maxSizeMB || 10; + if (options.allowedExtensions && Array.isArray(options.allowedExtensions)) { + this.allowedConfig = {}; + options.allowedExtensions.forEach((ext) => { + const key = ext.toLowerCase(); + if (KNOWN_SIGNATURES[key]) { + this.allowedConfig[key] = KNOWN_SIGNATURES[key]; + } else { + console.warn(`[FileValidator] 未知的文件类型: .${key},已忽略`); + } + }); + } else { + this.allowedConfig = { + ...KNOWN_SIGNATURES, + }; + } + } + _isValidUTF8(buffer) { + try { + const decoder = new TextDecoder('utf-8', { + fatal: true, + }); + decoder.decode(buffer); + return true; + } catch (e) { + return false; + } + } + _bufferToHex(buffer) { + return Array.prototype.map + .call(new Uint8Array(buffer), (x) => ('00' + x.toString(16)).slice(-2)) + .join('') + .toUpperCase(); + } + _countCSVRows(buffer) { + const decoder = new TextDecoder('utf-8'); + const text = decoder.decode(buffer); + 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++; + } + } + if (len > 0 && text[len - 1] !== '\n') { + rowCount++; + } + return rowCount; + } + _validateTextContent(buffer, extension) { + let contentStr = ''; + try { + const decoder = new TextDecoder('utf-8', { + fatal: true, + }); + contentStr = decoder.decode(buffer); + } catch (e) { + console.warn('UTF-8 解码失败', e); + return false; + } + if (contentStr.includes('\0')) { + return false; + } + if (extension === 'json') { + try { + JSON.parse(contentStr); + } catch (e) { + console.warn('无效的 JSON 格式'); + return false; + } + } + return true; + } + validate(file) { + return new Promise((resolve, reject) => { + if (!file || !file.name) return reject('无效的文件对象'); + if (file.size > this.maxSizeMB * 1024 * 1024) { + return reject(`文件大小超出限制 (最大 ${this.maxSizeMB}MB)`); + } + const fileName = file.name.toLowerCase(); + const extension = fileName.substring(fileName.lastIndexOf('.') + 1); + const expectedMagic = this.allowedConfig[extension]; + if (!expectedMagic) { + return reject(`不支持的文件格式: .${extension}`); + } + const reader = new FileReader(); + reader.onload = (e) => { + const buffer = e.target.result; + let isSafe = false; + if (expectedMagic === 'TYPE_TEXT') { + if (this._validateTextContent(buffer, extension)) { + isSafe = true; + } else { + if (extension === 'json') { + return reject(`文件异常:不是有效的 JSON 文件`); + } + return reject(`文件异常:.${extension} 包含非法二进制内容或编码错误`); + } + if (extension === 'csv' && this.csvMaxRows > 0) { + const rows = this._countCSVRows(buffer); + if (rows > this.csvMaxRows) { + return reject(`CSV 行数超出限制 (当前 ${rows} 行,最大允许 ${this.csvMaxRows} 行)`); + } + } + } else { + const fileHeader = this._bufferToHex(buffer.slice(0, 8)); + if (fileHeader.startsWith(expectedMagic)) { + isSafe = true; + } else { + return reject(`文件可能已被篡改 (真实类型与 .${extension} 不符)`); + } + } + if (isSafe) resolve(true); + }; + reader.onerror = () => reject('文件读取失败,无法校验'); + if (expectedMagic === 'TYPE_TEXT' && extension === 'json') { + reader.readAsArrayBuffer(file); + } else { + reader.readAsArrayBuffer(file.slice(0, 2048)); + } + }); + } +} + + +// 【demo】 +// 如果传入了 allowedExtensions,则只使用传入的;否则使用全部 KNOWN_SIGNATURES +// const imageValidator = new FileValidator({ +// maxSizeMB: 5, +// allowedExtensions: ['png', 'jpg', 'jpeg'], +// }); + +// imageValidator +// .validate(file) +// .then(() => { +// statusDiv.textContent = `检测通过: ${file.name}`; +// statusDiv.style.color = 'green'; +// console.log('图片校验通过,开始上传...'); +// // upload(file)... +// }) +// .catch((err) => { +// statusDiv.textContent = `检测失败: ${err}`; +// statusDiv.style.color = 'red'; +// }); \ No newline at end of file diff --git a/static/upload.html b/static/upload.html index 3c4e6da..97826c0 100644 --- a/static/upload.html +++ b/static/upload.html @@ -1,1252 +1,1389 @@ - - - - - - 文件上传 - - - -
-
-

文件上传

-

选择文件上传到终端

-
- -
- -
- ⚠️ -
加载中...
-
- - -
- ⚠️ -
已超过文件上传总数限制
-
- -
-

📎 选择文件

-
-
📁
-
点击选择文件
-
支持图片、文档、文本等格式
- -
-
- -
-

📋 已选文件

-
    -
    - -
    - - -
    -
    - - -
    - - -
    -
    -
    上传中,请稍候...
    -
    - - -
    - -

    上传成功!

    -

    文件已成功上传到电脑端

    - -
    - - -
    - 预览图片 - -
    - - - - + + + + + + 文件上传 + + + + +
    +
    +

    文件上传

    +

    选择文件上传到终端

    +
    + +
    + +
    + ⚠️ +
    加载中...
    +
    + + +
    + ⚠️ +
    已超过文件上传总数限制
    +
    + +
    +

    📎 选择文件

    +
    +
    📁
    +
    点击选择文件
    +
    支持图片、文档、文本等格式
    + +
    +
    + +
    +

    📋 已选文件

    +
      +
      + +
      + + +
      +
      + + +
      + + +
      +
      +
      上传中,请稍候...
      +
      + + +
      + + +
      + 预览图片 + +
      + + + + \ No newline at end of file diff --git a/stores/useLocationStore.js b/stores/useLocationStore.js index ab7b2fe..b4d312d 100644 --- a/stores/useLocationStore.js +++ b/stores/useLocationStore.js @@ -11,13 +11,15 @@ import config from '../config'; const defalutLongLat = { longitude: 120.382665, - latitude: 36.066938 + latitude: 36.066938, } const useLocationStore = defineStore("location", () => { // 定义状态 const longitudeVal = ref(null) // 经度 const latitudeVal = ref(null) //纬度 + const timer = ref(null) + const count = ref(0) function getLocation() { // 获取经纬度两个平台 return new Promise((resole, reject) => { @@ -66,6 +68,21 @@ const useLocationStore = defineStore("location", () => { } }) } + function getLocationLoop(gap = 1000 * 60 * 2) { + console.log(`🔄开始循环获取定位,间隔:${Math.floor(gap/1000)}秒`) + const run = () => { + count.value++ + console.log(`📍第${count.value}次获取定位`) + getLocation() + } + run() + timer.value = setInterval(run,gap); + } + + function clearGetLocationLoop(params) { + clearInterval(timer.value) + timer.value = null + } function longitude() { return longitudeVal.value @@ -78,9 +95,10 @@ const useLocationStore = defineStore("location", () => { // 导入 return { getLocation, + getLocationLoop, + clearGetLocationLoop, longitudeVal, - latitudeVal - + latitudeVal, } }, { unistorage: true, diff --git a/utils/fileValidator.js b/utils/fileValidator.js deleted file mode 100644 index 25fe856..0000000 --- a/utils/fileValidator.js +++ /dev/null @@ -1,210 +0,0 @@ -const KNOWN_SIGNATURES = { - png: "89504E470D0A1A0A", - jpg: "FFD8FF", - jpeg: "FFD8FF", - gif: "47494638", - webp: "52494646", - docx: "504B0304", - xlsx: "504B0304", - pptx: "504B0304", - doc: "D0CF11E0", - xls: "D0CF11E0", - ppt: "D0CF11E0", - pdf: "25504446", - txt: "TYPE_TEXT", - csv: "TYPE_TEXT", - md: "TYPE_TEXT", - json: "TYPE_TEXT" -}; -export class FileValidator { - version = "1.0.0"; - signs = Object.keys(KNOWN_SIGNATURES); - constructor(options = {}) { - this.maxSizeMB = options.maxSizeMB || 10; - if (options.allowedExtensions && Array.isArray(options.allowedExtensions)) { - this.allowedConfig = {}; - options.allowedExtensions.forEach(ext => { - const key = ext.toLowerCase(); - if (KNOWN_SIGNATURES[key]) { - this.allowedConfig[key] = KNOWN_SIGNATURES[key] - } else { - console.warn(`[FileValidator] 未知的文件类型: .${key},已忽略`) - } - }) - } else { - this.allowedConfig = { - ...KNOWN_SIGNATURES - } - } - } - _isValidUTF8(buffer) { - try { - const decoder = new TextDecoder("utf-8", { - fatal: true - }); - decoder.decode(buffer); - return true - } catch (e) { - return false - } - } - _bufferToHex(buffer) { - return Array.prototype.map.call(new Uint8Array(buffer), x => ("00" + x.toString(16)).slice(-2)).join("") - .toUpperCase() - } - _countCSVRows(buffer) { - const decoder = new TextDecoder("utf-8"); - const text = decoder.decode(buffer); - 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++ - } - } - if (len > 0 && text[len - 1] !== "\n") { - rowCount++ - } - return rowCount - } - _validateTextContent(buffer, extension) { - let contentStr = ""; - try { - const decoder = new TextDecoder("utf-8", { - fatal: true - }); - contentStr = decoder.decode(buffer) - } catch (e) { - console.warn("UTF-8 解码失败", e); - return false - } - if (contentStr.includes("\0")) { - return false - } - if (extension === "json") { - try { - JSON.parse(contentStr) - } catch (e) { - console.warn("无效的 JSON 格式"); - return false - } - } - return true - } - validate(file) { - return new Promise((resolve, reject) => { - if (!file || !file.name) return reject("无效的文件对象"); - if (file.size > this.maxSizeMB * 1024 * 1024) { - return reject(`文件大小超出限制 (最大 ${this.maxSizeMB}MB)`) - } - const fileName = file.name.toLowerCase(); - const extension = fileName.substring(fileName.lastIndexOf(".") + 1); - const expectedMagic = this.allowedConfig[extension]; - if (!expectedMagic) { - return reject(`不支持的文件格式: .${extension}`) - } - const reader = new FileReader; - reader.onload = e => { - const buffer = e.target.result; - let isSafe = false; - if (expectedMagic === "TYPE_TEXT") { - if (this._validateTextContent(buffer, extension)) { - isSafe = true - } else { - if (extension === "json") { - return reject(`文件异常:不是有效的 JSON 文件`) - } - return reject(`文件异常:.${extension} 包含非法二进制内容或编码错误`) - } - if (extension === "csv" && this.csvMaxRows > 0) { - const rows = this._countCSVRows(buffer); - if (rows > this.csvMaxRows) { - return reject(`CSV 行数超出限制 (当前 ${rows} 行,最大允许 ${this.csvMaxRows} 行)`) - } - } - } else { - const fileHeader = this._bufferToHex(buffer.slice(0, 8)); - if (fileHeader.startsWith(expectedMagic)) { - isSafe = true - } else { - return reject(`文件可能已被篡改 (真实类型与 .${extension} 不符)`) - } - } - if (isSafe) resolve(true) - }; - reader.onerror = () => reject("文件读取失败,无法校验"); - if (expectedMagic === "TYPE_TEXT" && extension === "json") { - reader.readAsArrayBuffer(file) - } else { - reader.readAsArrayBuffer(file.slice(0, 2048)) - } - }) - } -} - -// FileValidator 使用文档 -// FileValidator 是一个用于浏览器端的 JavaScript 文件校验类。它提供了比简单的后缀名检查更安全的文件验证机制。 -// 主要特性 -// 真实类型检测:通过读取文件二进制头部的“魔数”来验证文件类型,防止后缀名伪造。 -// 文本内容安全:检测文本文件是否为有效的 UTF-8 编码,防止乱码或二进制文件伪装。 -// JSON 语法校验:针对 JSON 文件,会自动尝试解析以确保格式正确。 -// CSV 行数限制:支持限制 CSV 文件的最大行数(需手动配置)。 -// 大小限制:内置文件大小检查(MB)。 -// 按需配置:支持自定义允许的文件扩展名列表。 -// 使用方法 - -// import { FileValidator } from './fileValidator.js'; - - -// 1. 初始化校验器 -// const validator = new FileValidator({ -// maxSizeMB: 5, // 限制最大 5MB -// allowedExtensions: ['jpg', 'png', 'pdf', 'docx'] // 仅允许这些格式 -// }); - -// // 2. 获取文件对象 (通常来自 input[type="file"]) -// const fileInput = document.getElementById('file-upload'); - -// fileInput.addEventListener('change', async (event) => { -// const file = event.target.files[0]; - -// if (!file) return; - -// try { -// // 3. 执行校验 -// await validator.validate(file); -// console.log('✅ 文件校验通过,可以上传'); - -// // 在这里执行你的上传逻辑... - -// } catch (errorMessage) { -// console.error('❌ 校验失败:', errorMessage); -// alert(errorMessage); -// } -// }); - -// 配置项: -// maxSizeMB: 允许的最大文件大小,单位 MB。 -// allowedExtensions: 允许的文件后缀列表(不区分大小写)。如果不传,则允许所有内置支持的格式。 - -// // 允许所有支持的格式,限制 20MB -// const v1 = new FileValidator({ maxSizeMB: 20 }); - -// // 仅允许图片 -// const v2 = new FileValidator({ -// allowedExtensions: ['jpg', 'jpeg', 'png', 'gif', 'webp'] -// }); - -// 设置 CSV 最大允许 1000 行 -// validator.csvMaxRows = 1000; - - -// 分类,扩展名,检测方式 -// 图片,"png, jpg, jpeg, gif, webp",二进制头签名 (Magic Number) -// 文档,"pdf, docx, xlsx, pptx, doc, xls, ppt",二进制头签名 (Magic Number) -// 文本,"txt, csv, md",UTF-8 编码检测 + 无 Null 字节检测 -// 数据,json,UTF-8 编码检测 + JSON.parse 语法校验 \ No newline at end of file