Compare commits
10 Commits
4dfc7bdfd8
...
4ae11e31f4
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ae11e31f4 | |||
| bca0d997c6 | |||
|
|
b43eb98a1c | ||
|
|
44c297aac2 | ||
| f64c9e5dae | |||
| 975835baa5 | |||
| 1ac524e1f1 | |||
| 7e8bef0cb9 | |||
|
|
ce597b182d | ||
|
|
fdd5577c85 |
16
App.vue
16
App.vue
@@ -24,6 +24,7 @@ onLaunch((options) => {
|
|||||||
getUserInfo();
|
getUserInfo();
|
||||||
useUserStore().changMiniProgramAppStatus(false);
|
useUserStore().changMiniProgramAppStatus(false);
|
||||||
useUserStore().changMachineEnv(false);
|
useUserStore().changMachineEnv(false);
|
||||||
|
useLocationStore().getLocationLoop()//循环获取定位
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (isY9MachineType()) {
|
if (isY9MachineType()) {
|
||||||
@@ -32,7 +33,18 @@ onLaunch((options) => {
|
|||||||
useUserStore().logOutApp();
|
useUserStore().logOutApp();
|
||||||
useUserStore().changMiniProgramAppStatus(true);
|
useUserStore().changMiniProgramAppStatus(true);
|
||||||
useUserStore().changMachineEnv(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();
|
uQRListen = new IncreaseRevie();
|
||||||
inactivityManager = new GlobalInactivityManager(handleInactivity, 60 * 1000);
|
inactivityManager = new GlobalInactivityManager(handleInactivity, 60 * 1000);
|
||||||
inactivityManager.start();
|
inactivityManager.start();
|
||||||
@@ -40,6 +52,7 @@ onLaunch((options) => {
|
|||||||
}
|
}
|
||||||
// 正式上线去除此方法
|
// 正式上线去除此方法
|
||||||
console.warn('浏览器环境');
|
console.warn('浏览器环境');
|
||||||
|
useLocationStore().getLocationLoop()//循环获取定位
|
||||||
useUserStore().changMiniProgramAppStatus(true);
|
useUserStore().changMiniProgramAppStatus(true);
|
||||||
useUserStore().changMachineEnv(false);
|
useUserStore().changMachineEnv(false);
|
||||||
useUserStore().initSeesionId(); //更新
|
useUserStore().initSeesionId(); //更新
|
||||||
@@ -57,6 +70,7 @@ onLaunch((options) => {
|
|||||||
|
|
||||||
onMounted(() => {});
|
onMounted(() => {});
|
||||||
|
|
||||||
|
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
console.log('App Show');
|
console.log('App Show');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -630,6 +630,10 @@ export function sm4Encrypt(key, value, mode = "hex") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function reloadBrowser() {
|
||||||
|
window.location.reload()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
export const $api = {
|
export const $api = {
|
||||||
msg,
|
msg,
|
||||||
@@ -679,5 +683,6 @@ export default {
|
|||||||
aes_Decrypt,
|
aes_Decrypt,
|
||||||
sm2_Decrypt,
|
sm2_Decrypt,
|
||||||
sm2_Encrypt,
|
sm2_Encrypt,
|
||||||
safeReLaunch
|
safeReLaunch,
|
||||||
|
reloadBrowser
|
||||||
}
|
}
|
||||||
@@ -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');
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name" : "qingdao-employment-service",
|
"name" : "qingdao-employment-service",
|
||||||
"appid" : "__UNI__2496162",
|
"appid" : "__UNI__C939371",
|
||||||
"description" : "招聘",
|
"description" : "招聘",
|
||||||
"versionName" : "1.0.0",
|
"versionName" : "1.0.0",
|
||||||
"versionCode" : "100",
|
"versionCode" : "100",
|
||||||
|
|||||||
@@ -82,6 +82,7 @@ const { userInfo } = storeToRefs(useUserStore());
|
|||||||
const { getUserResume } = useUserStore();
|
const { getUserResume } = useUserStore();
|
||||||
const { dictLabel, oneDictData } = useDictStore();
|
const { dictLabel, oneDictData } = useDictStore();
|
||||||
const openSelectPopup = inject('openSelectPopup');
|
const openSelectPopup = inject('openSelectPopup');
|
||||||
|
import { FileValidator } from '@/utils/fileValidator.js'; //文件校验
|
||||||
|
|
||||||
const percent = ref('0%');
|
const percent = ref('0%');
|
||||||
const state = reactive({
|
const state = reactive({
|
||||||
@@ -278,7 +279,15 @@ function selectAvatar() {
|
|||||||
sizeType: ['original', 'compressed'],
|
sizeType: ['original', 'compressed'],
|
||||||
sourceType: ['album', 'camera'],
|
sourceType: ['album', 'camera'],
|
||||||
count: 1,
|
count: 1,
|
||||||
success: ({ tempFilePaths, tempFiles }) => {
|
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)
|
$api.uploadFile(tempFilePaths[0], true)
|
||||||
.then((res) => {
|
.then((res) => {
|
||||||
res = JSON.parse(res);
|
res = JSON.parse(res);
|
||||||
@@ -287,6 +296,9 @@ function selectAvatar() {
|
|||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
$api.msg('上传失败');
|
$api.msg('上传失败');
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
$api.msg(error);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
fail: (error) => {},
|
fail: (error) => {},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -273,9 +273,7 @@ import useScreenStore from '@/stores/useScreenStore'
|
|||||||
const screenStore = useScreenStore();
|
const screenStore = useScreenStore();
|
||||||
// 系统功能hook和阿里云hook
|
// 系统功能hook和阿里云hook
|
||||||
import { useAudioRecorder } from '@/hook/useRealtimeRecorder.js';
|
import { useAudioRecorder } from '@/hook/useRealtimeRecorder.js';
|
||||||
// import { useAudioRecorder } from '@/hook/useSystemSpeechReader.js';
|
|
||||||
import { useTTSPlayer } from '@/hook/useTTSPlayer.js';
|
import { useTTSPlayer } from '@/hook/useTTSPlayer.js';
|
||||||
// import { useTTSPlayer } from '@/hook/useSystemPlayer.js';
|
|
||||||
// 全局
|
// 全局
|
||||||
const { $api, navTo, throttle } = inject('globalFunction');
|
const { $api, navTo, throttle } = inject('globalFunction');
|
||||||
const emit = defineEmits(['onConfirm']);
|
const emit = defineEmits(['onConfirm']);
|
||||||
@@ -283,6 +281,8 @@ const { messages, isTyping, textInput, chatSessionID } = storeToRefs(useChatGrou
|
|||||||
import successIcon from '@/static/icon/success.png';
|
import successIcon from '@/static/icon/success.png';
|
||||||
import useUserStore from '@/stores/useUserStore';
|
import useUserStore from '@/stores/useUserStore';
|
||||||
const { isMachineEnv } = storeToRefs(useUserStore());
|
const { isMachineEnv } = storeToRefs(useUserStore());
|
||||||
|
|
||||||
|
import { FileValidator } from '@/utils/fileValidator.js'; //文件校验
|
||||||
// hook
|
// hook
|
||||||
// 语音识别
|
// 语音识别
|
||||||
const {
|
const {
|
||||||
@@ -538,10 +538,14 @@ function uploadCamera(type = 'camera') {
|
|||||||
count: 1, //默认9
|
count: 1, //默认9
|
||||||
sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有
|
sizeType: ['original', 'compressed'], //可以指定是原图还是压缩图,默认二者都有
|
||||||
sourceType: [type], //从相册选择
|
sourceType: [type], //从相册选择
|
||||||
success: function (res) {
|
success: async (res)=> {
|
||||||
const tempFilePaths = res.tempFilePaths;
|
const tempFilePaths = res.tempFilePaths;
|
||||||
const file = res.tempFiles[0];
|
const file = res.tempFiles[0];
|
||||||
// 继续上传
|
|
||||||
|
const imageValidator = new FileValidator()
|
||||||
|
try {
|
||||||
|
await imageValidator.validate(file)
|
||||||
|
|
||||||
$api.uploadFile(tempFilePaths[0], true).then((resData) => {
|
$api.uploadFile(tempFilePaths[0], true).then((resData) => {
|
||||||
resData = JSON.parse(resData);
|
resData = JSON.parse(resData);
|
||||||
console.log(file.type,'++')
|
console.log(file.type,'++')
|
||||||
@@ -554,6 +558,9 @@ function uploadCamera(type = 'camera') {
|
|||||||
textInput.value = state.uploadFileTips;
|
textInput.value = state.uploadFileTips;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} catch (error) {
|
||||||
|
$api.msg(error)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -562,15 +569,17 @@ function getUploadFile(type = 'camera') {
|
|||||||
if (VerifyNumberFiles()) return;
|
if (VerifyNumberFiles()) return;
|
||||||
uni.chooseFile({
|
uni.chooseFile({
|
||||||
count: 1,
|
count: 1,
|
||||||
success: (res) => {
|
success: async(res) => {
|
||||||
const tempFilePaths = res.tempFilePaths;
|
const tempFilePaths = res.tempFilePaths;
|
||||||
const file = res.tempFiles[0];
|
const file = res.tempFiles[0];
|
||||||
const allowedTypes = config.allowedFileTypes || [];
|
const allowedTypes = config.allowedFileTypes || [];
|
||||||
const size = $api.formatFileSize(file.size);
|
const size = $api.formatFileSize(file.size);
|
||||||
if (!allowedTypes.includes(file.type)) {
|
|
||||||
return $api.msg('仅支持 txt md word pdf ppt csv excel 格式类型');
|
const imageValidator = new FileValidator({allowedExtensions:config.allowedFileTypes})
|
||||||
}
|
|
||||||
// 继续上传
|
try{
|
||||||
|
await imageValidator.validate(file)
|
||||||
|
|
||||||
$api.uploadFile(tempFilePaths[0], true).then((resData) => {
|
$api.uploadFile(tempFilePaths[0], true).then((resData) => {
|
||||||
resData = JSON.parse(resData);
|
resData = JSON.parse(resData);
|
||||||
filesList.value.push({
|
filesList.value.push({
|
||||||
@@ -581,6 +590,9 @@ function getUploadFile(type = 'camera') {
|
|||||||
});
|
});
|
||||||
textInput.value = state.uploadFileTips;
|
textInput.value = state.uploadFileTips;
|
||||||
});
|
});
|
||||||
|
}catch(error){
|
||||||
|
$api.msg(error)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,12 @@
|
|||||||
<image class="bg-text" mode="widthFix" src="@/static/icon/index-text-bg.png"></image>
|
<image class="bg-text" mode="widthFix" src="@/static/icon/index-text-bg.png"></image>
|
||||||
<view class="search-inner">
|
<view class="search-inner">
|
||||||
<view class="inner-left">
|
<view class="inner-left">
|
||||||
<image class="bg-text2" mode="widthFix" src="@/static/icon/index-text-bg2.png"></image>
|
<image
|
||||||
|
class="bg-text2"
|
||||||
|
mode="widthFix"
|
||||||
|
@click="reloadBrowser()"
|
||||||
|
src="@/static/icon/index-text-bg2.png"
|
||||||
|
></image>
|
||||||
<view class="search-input button-click" @click="navTo('/pages/search/search')">
|
<view class="search-input button-click" @click="navTo('/pages/search/search')">
|
||||||
<image class="icon" src="@/static/icon/index-search.png"></image>
|
<image class="icon" src="@/static/icon/index-search.png"></image>
|
||||||
<text class="inpute">请告诉我想找什么工作</text>
|
<text class="inpute">请告诉我想找什么工作</text>
|
||||||
@@ -20,7 +25,7 @@
|
|||||||
<image class="bg-robot button-click" mode="widthFix" src="@/static/icon/index-robot.png"></image>
|
<image class="bg-robot button-click" mode="widthFix" src="@/static/icon/index-robot.png"></image>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view v-if="!isMachineEnv" class="ai-card-out" >
|
<view v-if="!isMachineEnv" class="ai-card-out">
|
||||||
<view class="ai-card">
|
<view class="ai-card">
|
||||||
<image class="ai-card-bg" src="@/static/icon/ai-card-bg.png" />
|
<image class="ai-card-bg" src="@/static/icon/ai-card-bg.png" />
|
||||||
<view class="ai-card-inner">
|
<view class="ai-card-inner">
|
||||||
@@ -56,7 +61,7 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view v-if="hasLogin" :class="{'match-move-top':isMachineEnv}" class="match-card-out">
|
<view v-if="hasLogin" :class="{ 'match-move-top': isMachineEnv }" class="match-card-out">
|
||||||
<view class="match-card">
|
<view class="match-card">
|
||||||
<image class="match-card-bg" src="@/static/icon/match-card-bg.png" />
|
<image class="match-card-bg" src="@/static/icon/match-card-bg.png" />
|
||||||
<view class="title">简历匹配职位</view>
|
<view class="title">简历匹配职位</view>
|
||||||
@@ -65,7 +70,7 @@
|
|||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
<view :class="{'cards-move-top':isMachineEnv && !hasLogin}" class="cards">
|
<view :class="{ 'cards-move-top': isMachineEnv && !hasLogin }" class="cards">
|
||||||
<view class="card card1 press-button" @click="navTo('/pages/nearby/nearby')">
|
<view class="card card1 press-button" @click="navTo('/pages/nearby/nearby')">
|
||||||
<view class="card-title">附近工作</view>
|
<view class="card-title">附近工作</view>
|
||||||
<view class="card-text">好岗职等你来</view>
|
<view class="card-text">好岗职等你来</view>
|
||||||
@@ -254,11 +259,11 @@
|
|||||||
import { reactive, inject, watch, ref, onMounted, watchEffect, nextTick, getCurrentInstance } from 'vue';
|
import { reactive, inject, watch, ref, onMounted, watchEffect, nextTick, getCurrentInstance } from 'vue';
|
||||||
import img from '@/static/icon/filter.png';
|
import img from '@/static/icon/filter.png';
|
||||||
import dictLabel from '@/components/dict-Label/dict-Label.vue';
|
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 { onLoad, onShow } from '@dcloudio/uni-app';
|
||||||
import { storeToRefs } from 'pinia';
|
import { storeToRefs } from 'pinia';
|
||||||
import useUserStore from '@/stores/useUserStore';
|
import useUserStore from '@/stores/useUserStore';
|
||||||
const { userInfo, hasLogin ,isMachineEnv} = storeToRefs(useUserStore());
|
const { userInfo, hasLogin, isMachineEnv } = storeToRefs(useUserStore());
|
||||||
import useDictStore from '@/stores/useDictStore';
|
import useDictStore from '@/stores/useDictStore';
|
||||||
const { getTransformChildren, oneDictData } = useDictStore();
|
const { getTransformChildren, oneDictData } = useDictStore();
|
||||||
import useLocationStore from '@/stores/useLocationStore';
|
import useLocationStore from '@/stores/useLocationStore';
|
||||||
@@ -271,7 +276,6 @@ const recommedIndexDb = useRecommedIndexedDBStore();
|
|||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
import AIMatch from './AIMatch.vue';
|
import AIMatch from './AIMatch.vue';
|
||||||
|
|
||||||
|
|
||||||
const { proxy } = getCurrentInstance();
|
const { proxy } = getCurrentInstance();
|
||||||
|
|
||||||
const maskFirstEntry = ref(true);
|
const maskFirstEntry = ref(true);
|
||||||
@@ -363,7 +367,7 @@ onMounted(() => {
|
|||||||
let firstEntry = uni.getStorageSync('firstEntry') === false ? false : true; // 默认未读
|
let firstEntry = uni.getStorageSync('firstEntry') === false ? false : true; // 默认未读
|
||||||
maskFirstEntry.value = firstEntry;
|
maskFirstEntry.value = firstEntry;
|
||||||
getMatchTags();
|
getMatchTags();
|
||||||
console.log(isMachineEnv.value,'+++++++++')
|
// console.log(isMachineEnv.value, '+++++++++');
|
||||||
});
|
});
|
||||||
|
|
||||||
async function getMatchTags() {
|
async function getMatchTags() {
|
||||||
@@ -466,7 +470,6 @@ const { columnCount, columnSpace } = useColumnCount(() => {
|
|||||||
getJobRecommend('refresh');
|
getJobRecommend('refresh');
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
waterfallsFlowRef.value?.refresh?.();
|
waterfallsFlowRef.value?.refresh?.();
|
||||||
useLocationStore().getLocation();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -129,7 +129,6 @@ const { columnCount, columnSpace } = useColumnCount(() => {
|
|||||||
pageSize.value = 10 * (columnCount.value - 1);
|
pageSize.value = 10 * (columnCount.value - 1);
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
waterfallsFlowRef.value?.refresh?.();
|
waterfallsFlowRef.value?.refresh?.();
|
||||||
useLocationStore().getLocation();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,13 +2,14 @@
|
|||||||
<scroll-view :scroll-y="true" class="nearby-scroll" @scrolltolower="scrollBottom">
|
<scroll-view :scroll-y="true" class="nearby-scroll" @scrolltolower="scrollBottom">
|
||||||
<view class="nearby-map" @touchmove.stop.prevent>
|
<view class="nearby-map" @touchmove.stop.prevent>
|
||||||
<map
|
<map
|
||||||
style="width: 100%; height: 410rpx"
|
style="width: 100%; height: 690rpx"
|
||||||
:latitude="latitudeVal"
|
:latitude="latitudeVal"
|
||||||
:longitude="longitudeVal"
|
:longitude="longitudeVal"
|
||||||
:markers="mapCovers"
|
:markers="mapCovers"
|
||||||
:circles="mapCircles"
|
:circles="mapCircles"
|
||||||
:controls="mapControls"
|
:controls="mapControls"
|
||||||
@controltap="handleControl"
|
@controltap="handleControl"
|
||||||
|
:scale="mapScale"
|
||||||
></map>
|
></map>
|
||||||
<view class="nearby-select">
|
<view class="nearby-select">
|
||||||
<view class="select-view" @click="changeRangeShow">
|
<view class="select-view" @click="changeRangeShow">
|
||||||
@@ -106,16 +107,18 @@ const tMap = ref();
|
|||||||
const progress = ref();
|
const progress = ref();
|
||||||
const mapCovers = ref([]);
|
const mapCovers = ref([]);
|
||||||
const mapCircles = ref([]);
|
const mapCircles = ref([]);
|
||||||
|
const mapScale = ref(14.5)
|
||||||
const mapControls = ref([
|
const mapControls = ref([
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
position: {
|
position: {
|
||||||
// 控件位置
|
// 控件位置
|
||||||
left: customSystem.systemInfo.screenWidth - 48 - 14,
|
left: customSystem.systemInfo.screenWidth - uni.upx2px(75 + 30),
|
||||||
top: 320,
|
top: uni.upx2px(655 - 75 - 30),
|
||||||
width: 48,
|
width: uni.upx2px(75),
|
||||||
height: 48,
|
height: uni.upx2px(75),
|
||||||
},
|
},
|
||||||
|
width:100,
|
||||||
iconPath: LocationPng, // 控件图标
|
iconPath: LocationPng, // 控件图标
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
@@ -148,6 +151,8 @@ function changeRangeShow() {
|
|||||||
|
|
||||||
function changeRadius(item) {
|
function changeRadius(item) {
|
||||||
console.log(item);
|
console.log(item);
|
||||||
|
if(item > 1) mapScale.value = 14.5 - item * 0.3
|
||||||
|
else mapScale.value = 14.5
|
||||||
pageState.search.radius = item;
|
pageState.search.radius = item;
|
||||||
rangeShow.value = false;
|
rangeShow.value = false;
|
||||||
progressChange(item);
|
progressChange(item);
|
||||||
@@ -221,27 +226,23 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function getInit() {
|
function getInit() {
|
||||||
useLocationStore()
|
|
||||||
.getLocation()
|
|
||||||
.then((res) => {
|
|
||||||
mapCovers.value = [
|
mapCovers.value = [
|
||||||
{
|
{
|
||||||
latitude: res.latitude,
|
latitude: latitudeVal.value,
|
||||||
longitude: res.longitude,
|
longitude: longitudeVal.value,
|
||||||
iconPath: point2,
|
iconPath: point2,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
mapCircles.value = [
|
mapCircles.value = [
|
||||||
{
|
{
|
||||||
latitude: res.latitude,
|
latitude: latitudeVal.value,
|
||||||
longitude: res.longitude,
|
longitude:longitudeVal.value,
|
||||||
radius: 1000,
|
radius: 1000,
|
||||||
fillColor: '#1c52fa25',
|
fillColor: '#1c52fa25',
|
||||||
color: '#256BFA',
|
color: '#256BFA',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
getJobList('refresh');
|
getJobList('refresh');
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function progressChange(value) {
|
function progressChange(value) {
|
||||||
@@ -363,7 +364,7 @@ defineExpose({ loadData, handleFilterConfirm });
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
background: #f4f4f4;
|
background: #f4f4f4;
|
||||||
.nearby-map
|
.nearby-map
|
||||||
height: 400rpx;
|
height: 655rpx;
|
||||||
background: #e8e8e8;
|
background: #e8e8e8;
|
||||||
overflow: hidden
|
overflow: hidden
|
||||||
.nearby-list
|
.nearby-list
|
||||||
|
|||||||
@@ -151,7 +151,6 @@ const { columnCount, columnSpace } = useColumnCount(() => {
|
|||||||
pageSize.value = 10 * (columnCount.value - 1);
|
pageSize.value = 10 * (columnCount.value - 1);
|
||||||
nextTick(() => {
|
nextTick(() => {
|
||||||
waterfallsFlowRef.value?.refresh?.();
|
waterfallsFlowRef.value?.refresh?.();
|
||||||
useLocationStore().getLocation();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
170
static/js/fileValidator.js
Normal file
170
static/js/fileValidator.js
Normal file
@@ -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';
|
||||||
|
// });
|
||||||
@@ -2,11 +2,10 @@
|
|||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta
|
<meta name="viewport"
|
||||||
name="viewport"
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
|
|
||||||
/>
|
|
||||||
<title>文件上传</title>
|
<title>文件上传</title>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
* {
|
* {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -172,6 +171,7 @@
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(10px);
|
transform: translateY(10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
@@ -341,6 +341,7 @@
|
|||||||
0% {
|
0% {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
@@ -352,30 +353,61 @@
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 状态提示框容器 */
|
||||||
|
.status-messages-container {
|
||||||
|
pointer-events: none;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 状态提示框 */
|
||||||
.status-message {
|
.status-message {
|
||||||
position: fixed;
|
|
||||||
top: 50%;
|
|
||||||
left: 20px;
|
|
||||||
right: 20px;
|
|
||||||
transform: translateY(-50%);
|
|
||||||
background: rgb(238, 238, 238);
|
background: rgb(238, 238, 238);
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
padding: 30px 25px;
|
padding: 30px 25px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
|
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
|
||||||
z-index: 1001;
|
margin: 0 auto 20px;
|
||||||
display: none;
|
display: none;
|
||||||
animation: popUp 0.3s ease;
|
animation: popUp 0.3s ease;
|
||||||
|
max-width: 85%;
|
||||||
|
pointer-events: auto;
|
||||||
|
position: fixed;
|
||||||
|
top: 45%;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 1001;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 点击关闭按钮 */
|
||||||
|
.status-message .close-btn {
|
||||||
|
position: absolute;
|
||||||
|
top: 10px;
|
||||||
|
right: 10px;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: #666;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-message .close-btn:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes popUp {
|
@keyframes popUp {
|
||||||
from {
|
from {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(-40%) scale(0.9);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translateY(-50%) scale(1);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -583,13 +615,8 @@
|
|||||||
<div class="upload-icon">📁</div>
|
<div class="upload-icon">📁</div>
|
||||||
<div class="upload-text" id="uploadText">点击选择文件</div>
|
<div class="upload-text" id="uploadText">点击选择文件</div>
|
||||||
<div class="upload-hint" id="uploadHint">支持图片、文档、文本等格式</div>
|
<div class="upload-hint" id="uploadHint">支持图片、文档、文本等格式</div>
|
||||||
<input
|
<input type="file" id="fileInput" class="file-input" multiple
|
||||||
type="file"
|
accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.csv,.ppt,.pptx,.txt,.md" />
|
||||||
id="fileInput"
|
|
||||||
class="file-input"
|
|
||||||
multiple
|
|
||||||
accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.csv,.ppt,.pptx,.txt,.md"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -615,13 +642,8 @@
|
|||||||
<div class="loading-text" id="loadingText">上传中,请稍候...</div>
|
<div class="loading-text" id="loadingText">上传中,请稍候...</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 状态提示框 -->
|
<!-- 状态提示框容器 -->
|
||||||
<div class="status-message" id="statusMessage">
|
<div class="status-messages-container" id="statusMessagesContainer"></div>
|
||||||
<span class="status-icon" id="statusIcon">✅</span>
|
|
||||||
<h3 class="status-title" id="statusTitle">上传成功!</h3>
|
|
||||||
<p class="status-desc" id="statusDesc">文件已成功上传到电脑端</p>
|
|
||||||
<button class="status-btn" id="statusBtn">完成</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 图片预览模态框 -->
|
<!-- 图片预览模态框 -->
|
||||||
<div class="image-preview-modal" id="imagePreviewModal">
|
<div class="image-preview-modal" id="imagePreviewModal">
|
||||||
@@ -629,7 +651,13 @@
|
|||||||
<button class="close-preview" id="closePreview">×</button>
|
<button class="close-preview" id="closePreview">×</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script type="module">
|
||||||
|
import {
|
||||||
|
FileValidator
|
||||||
|
} from './js/FileValidator.js'; //文件校验JS
|
||||||
|
// 创建文件校验器实例
|
||||||
|
const fileValidator = new FileValidator();
|
||||||
|
|
||||||
// 获取URL中的参数
|
// 获取URL中的参数
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const sessionId = urlParams.get('sessionId');
|
const sessionId = urlParams.get('sessionId');
|
||||||
@@ -655,11 +683,7 @@
|
|||||||
const uploadBtn = document.getElementById('uploadBtn');
|
const uploadBtn = document.getElementById('uploadBtn');
|
||||||
const loadingOverlay = document.getElementById('loadingOverlay');
|
const loadingOverlay = document.getElementById('loadingOverlay');
|
||||||
const loadingText = document.getElementById('loadingText');
|
const loadingText = document.getElementById('loadingText');
|
||||||
const statusMessage = document.getElementById('statusMessage');
|
const statusMessagesContainer = document.getElementById('statusMessagesContainer');
|
||||||
const statusIcon = document.getElementById('statusIcon');
|
|
||||||
const statusTitle = document.getElementById('statusTitle');
|
|
||||||
const statusDesc = document.getElementById('statusDesc');
|
|
||||||
const statusBtn = document.getElementById('statusBtn');
|
|
||||||
const imagePreviewModal = document.getElementById('imagePreviewModal');
|
const imagePreviewModal = document.getElementById('imagePreviewModal');
|
||||||
const previewImage = document.getElementById('previewImage');
|
const previewImage = document.getElementById('previewImage');
|
||||||
const closePreview = document.getElementById('closePreview');
|
const closePreview = document.getElementById('closePreview');
|
||||||
@@ -673,6 +697,8 @@
|
|||||||
let isUploading = false;
|
let isUploading = false;
|
||||||
let uploadedCount = 0; // 已上传的总文件数量
|
let uploadedCount = 0; // 已上传的总文件数量
|
||||||
|
|
||||||
|
let statusMessageCount = 0; // 弹窗计数器
|
||||||
|
|
||||||
// 初始化
|
// 初始化
|
||||||
function init() {
|
function init() {
|
||||||
// 更新限制提示文本
|
// 更新限制提示文本
|
||||||
@@ -688,12 +714,28 @@
|
|||||||
fileInput.addEventListener('change', handleFileSelect);
|
fileInput.addEventListener('change', handleFileSelect);
|
||||||
clearBtn.addEventListener('click', clearAllFiles);
|
clearBtn.addEventListener('click', clearAllFiles);
|
||||||
uploadBtn.addEventListener('click', startUpload);
|
uploadBtn.addEventListener('click', startUpload);
|
||||||
statusBtn.addEventListener('click', hideStatus);
|
|
||||||
closePreview.addEventListener('click', () => {
|
closePreview.addEventListener('click', () => {
|
||||||
imagePreviewModal.classList.remove('show');
|
imagePreviewModal.classList.remove('show');
|
||||||
document.body.style.overflow = '';
|
document.body.style.overflow = '';
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 添加事件委托来处理状态消息的关闭按钮
|
||||||
|
statusMessagesContainer.addEventListener('click', (e) => {
|
||||||
|
if (e.target.matches('[data-action="close"]')) {
|
||||||
|
const messageId = e.target.dataset.messageId;
|
||||||
|
closeStatusMessage(messageId);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 添加事件委托来处理删除按钮
|
||||||
|
fileList.addEventListener('click', (e) => {
|
||||||
|
if (e.target.classList.contains('remove-btn')) {
|
||||||
|
const name = e.target.dataset.name;
|
||||||
|
const size = parseInt(e.target.dataset.size);
|
||||||
|
removeFile(name, size);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 图片预览模态框点击外部关闭
|
// 图片预览模态框点击外部关闭
|
||||||
imagePreviewModal.addEventListener('click', (e) => {
|
imagePreviewModal.addEventListener('click', (e) => {
|
||||||
if (e.target === imagePreviewModal) {
|
if (e.target === imagePreviewModal) {
|
||||||
@@ -706,7 +748,9 @@
|
|||||||
setupDragAndDrop();
|
setupDragAndDrop();
|
||||||
|
|
||||||
// 防止页面滚动
|
// 防止页面滚动
|
||||||
document.body.addEventListener('touchmove', preventScroll, { passive: false });
|
document.body.addEventListener('touchmove', preventScroll, {
|
||||||
|
passive: false
|
||||||
|
});
|
||||||
|
|
||||||
// 更新UI
|
// 更新UI
|
||||||
updateUI();
|
updateUI();
|
||||||
@@ -795,7 +839,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 处理文件
|
// 处理文件
|
||||||
function handleFiles(files) {
|
async function handleFiles(files) {
|
||||||
// 检查是否已达到总文件数量上限
|
// 检查是否已达到总文件数量上限
|
||||||
if (MAX_FILE_COUNT > 0 && uploadedCount >= MAX_FILE_COUNT) {
|
if (MAX_FILE_COUNT > 0 && uploadedCount >= MAX_FILE_COUNT) {
|
||||||
showError(`已超过文件上传总数限制(${MAX_FILE_COUNT}个)`);
|
showError(`已超过文件上传总数限制(${MAX_FILE_COUNT}个)`);
|
||||||
@@ -816,6 +860,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (let file of files) {
|
for (let file of files) {
|
||||||
|
console.log(file);
|
||||||
// 检查是否已达到本次选择的文件数量上限
|
// 检查是否已达到本次选择的文件数量上限
|
||||||
if (selectedFiles.length >= MAX_FILE_COUNT) {
|
if (selectedFiles.length >= MAX_FILE_COUNT) {
|
||||||
showError(`最多只能选择 ${MAX_FILE_COUNT} 个文件,已忽略多余文件`);
|
showError(`最多只能选择 ${MAX_FILE_COUNT} 个文件,已忽略多余文件`);
|
||||||
@@ -839,12 +884,18 @@
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 添加到文件列表
|
fileValidator
|
||||||
|
.validate(file)
|
||||||
|
.then(() => {
|
||||||
|
console.log(1111);
|
||||||
selectedFiles.push(file);
|
selectedFiles.push(file);
|
||||||
addFileToList(file);
|
addFileToList(file);
|
||||||
}
|
|
||||||
|
|
||||||
updateUI();
|
updateUI();
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
showError(file.name + err);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否为图片文件
|
// 检查是否为图片文件
|
||||||
@@ -888,12 +939,26 @@
|
|||||||
};
|
};
|
||||||
return icons[ext] || icons.default;
|
return icons[ext] || icons.default;
|
||||||
}
|
}
|
||||||
|
// 移除文件
|
||||||
|
function removeFile(name, size) {
|
||||||
|
selectedFiles = selectedFiles.filter((f) => !(f.name === name && f.size === size));
|
||||||
|
|
||||||
|
const fileItem = document.querySelector(`.file-item[data-name="${name}"]`);
|
||||||
|
if (fileItem) {
|
||||||
|
fileItem.style.animation = 'slideIn 0.3s ease reverse';
|
||||||
|
setTimeout(() => {
|
||||||
|
fileItem.remove();
|
||||||
|
updateUI();
|
||||||
|
}, 150);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 添加文件到列表
|
// 添加文件到列表
|
||||||
function addFileToList(file) {
|
function addFileToList(file) {
|
||||||
const li = document.createElement('li');
|
const li = document.createElement('li');
|
||||||
li.className = 'file-item';
|
li.className = 'file-item';
|
||||||
li.dataset.name = file.name;
|
li.dataset.name = file.name;
|
||||||
|
li.dataset.size = file.size; // 添加 size 到 dataset
|
||||||
|
|
||||||
// 检查是否为图片
|
// 检查是否为图片
|
||||||
const isImage = isImageType(file) && isImageFile(file.name);
|
const isImage = isImageType(file) && isImageFile(file.name);
|
||||||
@@ -901,7 +966,7 @@
|
|||||||
// 获取文件图标
|
// 获取文件图标
|
||||||
const fileIcon = getFileIcon(file.name);
|
const fileIcon = getFileIcon(file.name);
|
||||||
|
|
||||||
// 创建HTML
|
// 创建HTML(移除 onclick,改为 data 属性)
|
||||||
li.innerHTML = `
|
li.innerHTML = `
|
||||||
<div class="file-icon-container" id="icon-${escapeHtml(file.name)}">
|
<div class="file-icon-container" id="icon-${escapeHtml(file.name)}">
|
||||||
${
|
${
|
||||||
@@ -917,7 +982,7 @@
|
|||||||
<div class="file-progress-bar" id="progress-${escapeHtml(file.name)}"></div>
|
<div class="file-progress-bar" id="progress-${escapeHtml(file.name)}"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="remove-btn" onclick="removeFile('${escapeHtml(file.name)}', ${file.size})">×</button>
|
<button class="remove-btn" data-name="${escapeHtml(file.name)}" data-size="${file.size}">×</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
fileList.appendChild(li);
|
fileList.appendChild(li);
|
||||||
@@ -934,18 +999,17 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成缩略图
|
// 生成缩略图
|
||||||
function generateThumbnail(file, containerId) {
|
function generateThumbnail(file, containerId) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = function (e) {
|
reader.onload = function(e) {
|
||||||
const container = document.getElementById(containerId);
|
const container = document.getElementById(containerId);
|
||||||
if (container) {
|
if (container) {
|
||||||
const img = container.querySelector('.thumbnail');
|
const img = container.querySelector('.thumbnail');
|
||||||
img.src = e.target.result;
|
img.src = e.target.result;
|
||||||
|
|
||||||
// 添加加载完成后的淡入效果
|
// 添加加载完成后的淡入效果
|
||||||
img.onload = function () {
|
img.onload = function() {
|
||||||
img.style.opacity = '0';
|
img.style.opacity = '0';
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
img.style.transition = 'opacity 0.3s ease';
|
img.style.transition = 'opacity 0.3s ease';
|
||||||
@@ -960,7 +1024,7 @@
|
|||||||
// 预览完整图片
|
// 预览完整图片
|
||||||
function previewFullImage(file) {
|
function previewFullImage(file) {
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
reader.onload = function (e) {
|
reader.onload = function(e) {
|
||||||
previewImage.src = e.target.result;
|
previewImage.src = e.target.result;
|
||||||
imagePreviewModal.classList.add('show');
|
imagePreviewModal.classList.add('show');
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden';
|
||||||
@@ -977,20 +1041,6 @@
|
|||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移除文件
|
|
||||||
function removeFile(name, size) {
|
|
||||||
selectedFiles = selectedFiles.filter((f) => !(f.name === name && f.size === size));
|
|
||||||
|
|
||||||
const fileItem = document.querySelector(`.file-item[data-name="${name}"]`);
|
|
||||||
if (fileItem) {
|
|
||||||
fileItem.style.animation = 'slideIn 0.3s ease reverse';
|
|
||||||
setTimeout(() => {
|
|
||||||
fileItem.remove();
|
|
||||||
updateUI();
|
|
||||||
}, 150);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清空所有文件
|
// 清空所有文件
|
||||||
function clearAllFiles() {
|
function clearAllFiles() {
|
||||||
selectedFiles = [];
|
selectedFiles = [];
|
||||||
@@ -1037,7 +1087,8 @@
|
|||||||
fileInput.disabled = true;
|
fileInput.disabled = true;
|
||||||
} else {
|
} else {
|
||||||
uploadText.textContent = '点击选择文件';
|
uploadText.textContent = '点击选择文件';
|
||||||
uploadHint.innerHTML = `支持图片、文档、文本等格式<br />最多${MAX_FILE_COUNT}个文件,每个不超过10MB<br />已上传: ${uploadedCount}/${MAX_FILE_COUNT}`;
|
uploadHint.innerHTML =
|
||||||
|
`支持图片、文档、文本等格式<br />最多${MAX_FILE_COUNT}个文件,每个不超过10MB<br />已上传: ${uploadedCount}/${MAX_FILE_COUNT}`;
|
||||||
fileInput.disabled = false;
|
fileInput.disabled = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1197,43 +1248,129 @@
|
|||||||
document.body.style.overflow = '';
|
document.body.style.overflow = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示成功提示
|
// 创建状态提示框
|
||||||
function showSuccess(message) {
|
function createStatusMessage(type, title, desc, autoClose = false, closeTime = 3000) {
|
||||||
statusMessage.className = 'status-message status-success';
|
// 创建唯一的ID
|
||||||
statusIcon.textContent = '✅';
|
const messageId = 'status-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
|
||||||
statusTitle.textContent = '上传成功!';
|
|
||||||
statusDesc.textContent = message;
|
// 计算当前弹窗的偏移量(每个弹窗下移20px)
|
||||||
statusMessage.style.display = 'block';
|
const offset = statusMessageCount * 40;
|
||||||
document.body.style.overflow = 'hidden';
|
|
||||||
|
// 创建弹窗HTML,添加自定义的偏移样式
|
||||||
|
const messageHtml = `
|
||||||
|
<div class="status-message status-${type}" id="${messageId}"
|
||||||
|
data-message-id="${messageId}"
|
||||||
|
style="margin-top: ${offset}px;">
|
||||||
|
<button class="close-btn" data-action="close" data-message-id="${messageId}">×</button>
|
||||||
|
<span class="status-icon">${type === 'success' ? '✅' : '❌'}</span>
|
||||||
|
<h3 class="status-title">${escapeHtml(title)}</h3>
|
||||||
|
<p class="status-desc">${escapeHtml(desc)}</p>
|
||||||
|
<button class="status-btn" data-action="close" data-message-id="${messageId}">完成</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// 添加到容器
|
||||||
|
statusMessagesContainer.innerHTML += messageHtml;
|
||||||
|
|
||||||
|
// 显示弹窗
|
||||||
|
const messageElement = document.getElementById(messageId);
|
||||||
|
messageElement.style.display = 'block';
|
||||||
|
|
||||||
|
// 增加弹窗计数
|
||||||
|
statusMessageCount++;
|
||||||
|
|
||||||
|
// 自动关闭
|
||||||
|
if (autoClose) {
|
||||||
|
setTimeout(() => {
|
||||||
|
closeStatusMessage(messageId);
|
||||||
|
}, closeTime);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 显示错误提示
|
// 限制最大显示数量(最多显示5个)
|
||||||
function showError(message) {
|
const messages = statusMessagesContainer.querySelectorAll('.status-message');
|
||||||
statusMessage.className = 'status-message status-error';
|
if (messages.length > 5) {
|
||||||
statusIcon.textContent = '❌';
|
// 移除最早的消息
|
||||||
statusTitle.textContent = '出错了';
|
const oldestMessage = messages[0];
|
||||||
statusDesc.textContent = message;
|
closeStatusMessage(oldestMessage.id);
|
||||||
statusMessage.style.display = 'block';
|
|
||||||
document.body.style.overflow = 'hidden';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 隐藏状态提示
|
return messageId;
|
||||||
function hideStatus() {
|
}
|
||||||
statusMessage.style.display = 'none';
|
|
||||||
|
// 关闭状态提示框
|
||||||
|
function closeStatusMessage(messageId) {
|
||||||
|
const messageElement = document.getElementById(messageId);
|
||||||
|
if (messageElement) {
|
||||||
|
// 添加淡出动画
|
||||||
|
messageElement.style.animation = 'popUp 0.3s ease reverse';
|
||||||
|
messageElement.style.opacity = '0';
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
messageElement.remove();
|
||||||
|
|
||||||
|
// 减少弹窗计数
|
||||||
|
statusMessageCount--;
|
||||||
|
|
||||||
|
// 重新计算并更新剩余弹窗的位置
|
||||||
|
updateRemainingStatusMessages();
|
||||||
|
|
||||||
|
// 检查是否还有消息显示
|
||||||
|
const remainingMessages = statusMessagesContainer.querySelectorAll('.status-message');
|
||||||
|
if (remainingMessages.length === 0) {
|
||||||
|
document.body.style.overflow = '';
|
||||||
|
}
|
||||||
|
}, 300);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新剩余弹窗的位置
|
||||||
|
function updateRemainingStatusMessages() {
|
||||||
|
const messages = statusMessagesContainer.querySelectorAll('.status-message');
|
||||||
|
|
||||||
|
messages.forEach((message, index) => {
|
||||||
|
const offset = index * 20; // 每个弹窗下移20px
|
||||||
|
message.style.marginTop = `${offset}px`;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 隐藏所有状态提示
|
||||||
|
function hideAllStatus() {
|
||||||
|
const messages = statusMessagesContainer.querySelectorAll('.status-message');
|
||||||
|
messages.forEach((message) => {
|
||||||
|
closeStatusMessage(message.id);
|
||||||
|
});
|
||||||
document.body.style.overflow = '';
|
document.body.style.overflow = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 防止页面滚动
|
// 显示成功提示(创建新弹窗)
|
||||||
|
function showSuccess(message, options = {}) {
|
||||||
|
const {
|
||||||
|
autoClose = true, closeTime = 3000
|
||||||
|
} = options;
|
||||||
|
return createStatusMessage('success', '上传成功!', message, autoClose, closeTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示错误提示(创建新弹窗)
|
||||||
|
function showError(message, options = {}) {
|
||||||
|
const {
|
||||||
|
autoClose = false, closeTime = 5000
|
||||||
|
} = options;
|
||||||
|
return createStatusMessage('error', '出错了', message, autoClose, closeTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 修改防止页面滚动函数
|
||||||
function preventScroll(e) {
|
function preventScroll(e) {
|
||||||
|
const messages = statusMessagesContainer.querySelectorAll('.status-message');
|
||||||
|
const hasMessages = messages.length > 0;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
loadingOverlay.classList.contains('show') ||
|
loadingOverlay.classList.contains('show') ||
|
||||||
statusMessage.style.display === 'block' ||
|
hasMessages ||
|
||||||
imagePreviewModal.classList.contains('show')
|
imagePreviewModal.classList.contains('show')
|
||||||
) {
|
) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HTML转义
|
// HTML转义
|
||||||
function escapeHtml(text) {
|
function escapeHtml(text) {
|
||||||
const div = document.createElement('div');
|
const div = document.createElement('div');
|
||||||
|
|||||||
@@ -9,10 +9,17 @@ import {
|
|||||||
} from '@/common/globalFunction.js'
|
} from '@/common/globalFunction.js'
|
||||||
import config from '../config';
|
import config from '../config';
|
||||||
|
|
||||||
|
const defalutLongLat = {
|
||||||
|
longitude: 120.382665,
|
||||||
|
latitude: 36.066938,
|
||||||
|
}
|
||||||
|
|
||||||
const useLocationStore = defineStore("location", () => {
|
const useLocationStore = defineStore("location", () => {
|
||||||
// 定义状态
|
// 定义状态
|
||||||
const longitudeVal = ref(null) // 经度
|
const longitudeVal = ref(null) // 经度
|
||||||
const latitudeVal = ref(null) //纬度
|
const latitudeVal = ref(null) //纬度
|
||||||
|
const timer = ref(null)
|
||||||
|
const count = ref(0)
|
||||||
|
|
||||||
function getLocation() { // 获取经纬度两个平台
|
function getLocation() { // 获取经纬度两个平台
|
||||||
return new Promise((resole, reject) => {
|
return new Promise((resole, reject) => {
|
||||||
@@ -25,49 +32,57 @@ const useLocationStore = defineStore("location", () => {
|
|||||||
resole(data)
|
resole(data)
|
||||||
},
|
},
|
||||||
fail: function(data) {
|
fail: function(data) {
|
||||||
longitudeVal.value = 120.382665
|
longitudeVal.value = defalutLongLat.longitude
|
||||||
latitudeVal.value = 36.066938
|
latitudeVal.value = defalutLongLat.latitude
|
||||||
resole({
|
resole(defalutLongLat)
|
||||||
longitude: 120.382665,
|
|
||||||
latitude: 36.066938
|
|
||||||
})
|
|
||||||
msg('用户位置获取失败')
|
msg('用户位置获取失败')
|
||||||
|
console.log('失败3', data)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
uni.getLocation({
|
uni.getLocation({
|
||||||
type: 'gcj02',
|
type: 'gcj02',
|
||||||
highAccuracyExpireTime: 3000,
|
// highAccuracyExpireTime: 3000,
|
||||||
isHighAccuracy: true,
|
// isHighAccuracy: true,
|
||||||
timeout: 2000,
|
// timeout: 2000,
|
||||||
success: function(data) {
|
success: function(data) {
|
||||||
longitudeVal.value = Number(data.longitude)
|
longitudeVal.value = Number(data.longitude)
|
||||||
latitudeVal.value = Number(data.latitude)
|
latitudeVal.value = Number(data.latitude)
|
||||||
resole(data)
|
resole(data)
|
||||||
},
|
},
|
||||||
fail: function(data) {
|
fail: function(data) {
|
||||||
longitudeVal.value = 120.382665
|
longitudeVal.value = defalutLongLat.longitude
|
||||||
latitudeVal.value = 36.066938
|
latitudeVal.value = defalutLongLat.latitude
|
||||||
resole({
|
resole(defalutLongLat)
|
||||||
longitude: 120.382665,
|
|
||||||
latitude: 36.066938
|
|
||||||
})
|
|
||||||
msg('用户位置获取失败')
|
msg('用户位置获取失败')
|
||||||
|
console.log('失败2', data)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
longitudeVal.value = 120.382665
|
longitudeVal.value = defalutLongLat.longitude
|
||||||
latitudeVal.value = 36.066938
|
latitudeVal.value = defalutLongLat.latitude
|
||||||
resole({
|
resole(defalutLongLat)
|
||||||
longitude: 120.382665,
|
|
||||||
latitude: 36.066938
|
|
||||||
})
|
|
||||||
msg('测试环境,使用模拟定位')
|
msg('测试环境,使用模拟定位')
|
||||||
console.log('失败', data)
|
console.log('失败1', e)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
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() {
|
function longitude() {
|
||||||
return longitudeVal.value
|
return longitudeVal.value
|
||||||
@@ -80,11 +95,12 @@ const useLocationStore = defineStore("location", () => {
|
|||||||
// 导入
|
// 导入
|
||||||
return {
|
return {
|
||||||
getLocation,
|
getLocation,
|
||||||
|
getLocationLoop,
|
||||||
|
clearGetLocationLoop,
|
||||||
longitudeVal,
|
longitudeVal,
|
||||||
latitudeVal
|
latitudeVal,
|
||||||
|
|
||||||
}
|
}
|
||||||
},{
|
}, {
|
||||||
unistorage: true,
|
unistorage: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,56 +1,26 @@
|
|||||||
/**
|
|
||||||
* FileValidator.js
|
|
||||||
* 封装好的文件安全校验类
|
|
||||||
*/
|
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// 1. 预定义:已知文件类型的魔数 (Signature Database)
|
|
||||||
// ==========================================
|
|
||||||
const KNOWN_SIGNATURES = {
|
const KNOWN_SIGNATURES = {
|
||||||
// === 图片 ===
|
|
||||||
png: '89504E470D0A1A0A',
|
png: '89504E470D0A1A0A',
|
||||||
jpg: 'FFD8FF',
|
jpg: 'FFD8FF',
|
||||||
jpeg: 'FFD8FF',
|
jpeg: 'FFD8FF',
|
||||||
gif: '47494638',
|
gif: '47494638',
|
||||||
webp: '52494646', // RIFF Header
|
webp: '52494646',
|
||||||
|
|
||||||
// === 文档 (Office 新版 - ZIP 格式) ===
|
|
||||||
docx: '504B0304',
|
docx: '504B0304',
|
||||||
xlsx: '504B0304',
|
xlsx: '504B0304',
|
||||||
pptx: '504B0304',
|
pptx: '504B0304',
|
||||||
|
|
||||||
// === 文档 (Office 旧版 - OLECF 格式) ===
|
|
||||||
doc: 'D0CF11E0',
|
doc: 'D0CF11E0',
|
||||||
xls: 'D0CF11E0',
|
xls: 'D0CF11E0',
|
||||||
ppt: 'D0CF11E0',
|
ppt: 'D0CF11E0',
|
||||||
|
|
||||||
// === 其他 ===
|
|
||||||
pdf: '25504446',
|
pdf: '25504446',
|
||||||
|
|
||||||
// === 纯文本 (无固定魔数,需特殊算法检测) ===
|
|
||||||
txt: 'TYPE_TEXT',
|
txt: 'TYPE_TEXT',
|
||||||
csv: 'TYPE_TEXT',
|
csv: 'TYPE_TEXT',
|
||||||
md: 'TYPE_TEXT',
|
md: 'TYPE_TEXT',
|
||||||
json: 'TYPE_TEXT',
|
json: 'TYPE_TEXT',
|
||||||
};
|
};
|
||||||
|
|
||||||
// ==========================================
|
|
||||||
// 2. 核心类定义
|
|
||||||
// ==========================================
|
|
||||||
export class FileValidator {
|
export class FileValidator {
|
||||||
/**
|
|
||||||
* 构造函数
|
|
||||||
* @param {Object} options 配置项
|
|
||||||
* @param {number} [options.maxSizeMB=10] 最大文件大小 (MB)
|
|
||||||
* @param {string[]} [options.allowedExtensions] 允许的扩展名列表 (如 ['jpg', 'png']),默认允许全部已知类型
|
|
||||||
*/
|
|
||||||
version = '1.0.0';
|
version = '1.0.0';
|
||||||
|
signs = Object.keys(KNOWN_SIGNATURES);
|
||||||
constructor(options = {}) {
|
constructor(options = {}) {
|
||||||
// 配置大小 (默认 10MB)
|
|
||||||
this.maxSizeMB = options.maxSizeMB || 10;
|
this.maxSizeMB = options.maxSizeMB || 10;
|
||||||
|
|
||||||
// 配置允许的类型
|
|
||||||
// 如果传入了 allowedExtensions,则只使用传入的;否则使用全部 KNOWN_SIGNATURES
|
|
||||||
if (options.allowedExtensions && Array.isArray(options.allowedExtensions)) {
|
if (options.allowedExtensions && Array.isArray(options.allowedExtensions)) {
|
||||||
this.allowedConfig = {};
|
this.allowedConfig = {};
|
||||||
options.allowedExtensions.forEach((ext) => {
|
options.allowedExtensions.forEach((ext) => {
|
||||||
@@ -63,103 +33,122 @@ export class FileValidator {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.allowedConfig = {
|
this.allowedConfig = {
|
||||||
...KNOWN_SIGNATURES
|
...KNOWN_SIGNATURES,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
_isValidUTF8(buffer) {
|
||||||
/**
|
try {
|
||||||
* 辅助:ArrayBuffer 转 Hex 字符串
|
const decoder = new TextDecoder('utf-8', {
|
||||||
*/
|
fatal: true,
|
||||||
|
});
|
||||||
|
decoder.decode(buffer);
|
||||||
|
return true;
|
||||||
|
} catch (e) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
_bufferToHex(buffer) {
|
_bufferToHex(buffer) {
|
||||||
return Array.prototype.map
|
return Array.prototype.map
|
||||||
.call(new Uint8Array(buffer), (x) => ('00' + x.toString(16)).slice(-2))
|
.call(new Uint8Array(buffer), (x) => ('00' + x.toString(16)).slice(-2))
|
||||||
.join('')
|
.join('')
|
||||||
.toUpperCase();
|
.toUpperCase();
|
||||||
}
|
}
|
||||||
|
_countCSVRows(buffer) {
|
||||||
/**
|
const decoder = new TextDecoder('utf-8');
|
||||||
* 辅助:纯文本抽样检测
|
const text = decoder.decode(buffer);
|
||||||
*/
|
let rowCount = 0;
|
||||||
_isCleanText(buffer) {
|
let inQuote = false;
|
||||||
const bytes = new Uint8Array(buffer);
|
let len = text.length;
|
||||||
const checkLen = Math.min(bytes.length, 1000);
|
for (let i = 0; i < len; i++) {
|
||||||
let suspiciousCount = 0;
|
const char = text[i];
|
||||||
|
if (char === '"') {
|
||||||
for (let i = 0; i < checkLen; i++) {
|
inQuote = !inQuote;
|
||||||
const byte = bytes[i];
|
} else if (char === '\n' && !inQuote) {
|
||||||
// 允许常见控制符: 9(Tab), 10(LF), 13(CR)
|
rowCount++;
|
||||||
// 0-31 范围内其他的通常是二进制控制符
|
|
||||||
if (byte < 32 && ![9, 10, 13].includes(byte)) {
|
|
||||||
suspiciousCount++;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 如果可疑字符占比 < 5%,认为是纯文本
|
if (len > 0 && text[len - 1] !== '\n') {
|
||||||
return suspiciousCount / checkLen < 0.05;
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 执行校验
|
|
||||||
* @param {File} file 文件对象
|
|
||||||
* @returns {Promise<boolean>}
|
|
||||||
*/
|
|
||||||
validate(file) {
|
validate(file) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// 1. 基础对象检查
|
|
||||||
if (!file || !file.name) return reject('无效的文件对象');
|
if (!file || !file.name) return reject('无效的文件对象');
|
||||||
|
|
||||||
// 2. 大小检查
|
|
||||||
if (file.size > this.maxSizeMB * 1024 * 1024) {
|
if (file.size > this.maxSizeMB * 1024 * 1024) {
|
||||||
return reject(`文件大小超出限制 (最大 ${this.maxSizeMB}MB)`);
|
return reject(`文件大小超出限制 (最大 ${this.maxSizeMB}MB)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 后缀名检查
|
|
||||||
const fileName = file.name.toLowerCase();
|
const fileName = file.name.toLowerCase();
|
||||||
const extension = fileName.substring(fileName.lastIndexOf('.') + 1);
|
const extension = fileName.substring(fileName.lastIndexOf('.') + 1);
|
||||||
|
|
||||||
// 检查是否在配置的白名单中
|
|
||||||
const expectedMagic = this.allowedConfig[extension];
|
const expectedMagic = this.allowedConfig[extension];
|
||||||
if (!expectedMagic) {
|
if (!expectedMagic) {
|
||||||
return reject(`不支持的文件格式: .${extension}`);
|
return reject(`不支持的文件格式: .${extension}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. 读取二进制头进行魔数校验
|
|
||||||
const reader = new FileReader();
|
const reader = new FileReader();
|
||||||
|
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
const buffer = e.target.result;
|
const buffer = e.target.result;
|
||||||
let isSafe = false;
|
let isSafe = false;
|
||||||
|
|
||||||
// 分支处理:纯文本 vs 二进制
|
|
||||||
if (expectedMagic === 'TYPE_TEXT') {
|
if (expectedMagic === 'TYPE_TEXT') {
|
||||||
if (this._isCleanText(buffer)) {
|
if (this._validateTextContent(buffer, extension)) {
|
||||||
isSafe = true;
|
isSafe = true;
|
||||||
} else {
|
} else {
|
||||||
return reject(`文件异常:.${extension} 文件包含非法二进制内容`);
|
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 {
|
} else {
|
||||||
// 获取文件头 Hex (读取足够长的字节以覆盖最长的魔数,PNG需8字节)
|
|
||||||
const fileHeader = this._bufferToHex(buffer.slice(0, 8));
|
const fileHeader = this._bufferToHex(buffer.slice(0, 8));
|
||||||
|
|
||||||
// 使用 startsWith 匹配
|
|
||||||
if (fileHeader.startsWith(expectedMagic)) {
|
if (fileHeader.startsWith(expectedMagic)) {
|
||||||
isSafe = true;
|
isSafe = true;
|
||||||
} else {
|
} else {
|
||||||
return reject(`文件可能已被篡改 (真实类型与 .${extension} 不符)`);
|
return reject(`文件可能已被篡改 (真实类型与 .${extension} 不符)`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSafe) resolve(true);
|
if (isSafe) resolve(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
reader.onerror = () => reject('文件读取失败,无法校验');
|
reader.onerror = () => reject('文件读取失败,无法校验');
|
||||||
|
if (expectedMagic === 'TYPE_TEXT' && extension === 'json') {
|
||||||
// 读取前 1KB 进行判断
|
reader.readAsArrayBuffer(file);
|
||||||
reader.readAsArrayBuffer(file.slice(0, 1024));
|
} else {
|
||||||
|
reader.readAsArrayBuffer(file.slice(0, 2048));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 【demo】
|
// 【demo】
|
||||||
// 如果传入了 allowedExtensions,则只使用传入的;否则使用全部 KNOWN_SIGNATURES
|
// 如果传入了 allowedExtensions,则只使用传入的;否则使用全部 KNOWN_SIGNATURES
|
||||||
// const imageValidator = new FileValidator({
|
// const imageValidator = new FileValidator({
|
||||||
|
|||||||
Reference in New Issue
Block a user