flat: ok
This commit is contained in:
16
App.vue
16
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');
|
||||
});
|
||||
|
||||
@@ -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,7 +279,15 @@ function selectAvatar() {
|
||||
sizeType: ['original', 'compressed'],
|
||||
sourceType: ['album', 'camera'],
|
||||
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)
|
||||
.then((res) => {
|
||||
res = JSON.parse(res);
|
||||
@@ -287,6 +296,9 @@ function selectAvatar() {
|
||||
.catch((err) => {
|
||||
$api.msg('上传失败');
|
||||
});
|
||||
} catch (error) {
|
||||
$api.msg(error);
|
||||
}
|
||||
},
|
||||
fail: (error) => {},
|
||||
});
|
||||
|
||||
@@ -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,10 +538,14 @@ 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];
|
||||
// 继续上传
|
||||
|
||||
const imageValidator = new FileValidator()
|
||||
try {
|
||||
await imageValidator.validate(file)
|
||||
|
||||
$api.uploadFile(tempFilePaths[0], true).then((resData) => {
|
||||
resData = JSON.parse(resData);
|
||||
console.log(file.type,'++')
|
||||
@@ -552,6 +558,9 @@ function uploadCamera(type = 'camera') {
|
||||
textInput.value = state.uploadFileTips;
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
$api.msg(error)
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -560,15 +569,17 @@ 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 格式类型');
|
||||
}
|
||||
// 继续上传
|
||||
|
||||
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({
|
||||
@@ -579,6 +590,9 @@ function getUploadFile(type = 'camera') {
|
||||
});
|
||||
textInput.value = state.uploadFileTips;
|
||||
});
|
||||
}catch(error){
|
||||
$api.msg(error)
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -470,7 +470,6 @@ const { columnCount, columnSpace } = useColumnCount(() => {
|
||||
getJobRecommend('refresh');
|
||||
nextTick(() => {
|
||||
waterfallsFlowRef.value?.refresh?.();
|
||||
useLocationStore().getLocation();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -129,7 +129,6 @@ const { columnCount, columnSpace } = useColumnCount(() => {
|
||||
pageSize.value = 10 * (columnCount.value - 1);
|
||||
nextTick(() => {
|
||||
waterfallsFlowRef.value?.refresh?.();
|
||||
useLocationStore().getLocation();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -151,7 +151,6 @@ const { columnCount, columnSpace } = useColumnCount(() => {
|
||||
pageSize.value = 10 * (columnCount.value - 1);
|
||||
nextTick(() => {
|
||||
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">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
|
||||
/>
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||
<title>文件上传</title>
|
||||
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
@@ -172,6 +171,7 @@
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
@@ -341,6 +341,7 @@
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
@@ -352,30 +353,61 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 状态提示框容器 */
|
||||
.status-messages-container {
|
||||
pointer-events: none;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 状态提示框 */
|
||||
.status-message {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 20px;
|
||||
right: 20px;
|
||||
transform: translateY(-50%);
|
||||
background: rgb(238, 238, 238);
|
||||
border-radius: 16px;
|
||||
padding: 30px 25px;
|
||||
text-align: center;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.2);
|
||||
z-index: 1001;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
|
||||
margin: 0 auto 20px;
|
||||
display: none;
|
||||
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 {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-40%) scale(0.9);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(-50%) scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -583,13 +615,8 @@
|
||||
<div class="upload-icon">📁</div>
|
||||
<div class="upload-text" id="uploadText">点击选择文件</div>
|
||||
<div class="upload-hint" id="uploadHint">支持图片、文档、文本等格式</div>
|
||||
<input
|
||||
type="file"
|
||||
id="fileInput"
|
||||
class="file-input"
|
||||
multiple
|
||||
accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.csv,.ppt,.pptx,.txt,.md"
|
||||
/>
|
||||
<input type="file" id="fileInput" class="file-input" multiple
|
||||
accept="image/*,.pdf,.doc,.docx,.xls,.xlsx,.csv,.ppt,.pptx,.txt,.md" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -615,13 +642,8 @@
|
||||
<div class="loading-text" id="loadingText">上传中,请稍候...</div>
|
||||
</div>
|
||||
|
||||
<!-- 状态提示框 -->
|
||||
<div class="status-message" id="statusMessage">
|
||||
<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="status-messages-container" id="statusMessagesContainer"></div>
|
||||
|
||||
<!-- 图片预览模态框 -->
|
||||
<div class="image-preview-modal" id="imagePreviewModal">
|
||||
@@ -629,7 +651,13 @@
|
||||
<button class="close-preview" id="closePreview">×</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
<script type="module">
|
||||
import {
|
||||
FileValidator
|
||||
} from './js/FileValidator.js'; //文件校验JS
|
||||
// 创建文件校验器实例
|
||||
const fileValidator = new FileValidator();
|
||||
|
||||
// 获取URL中的参数
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const sessionId = urlParams.get('sessionId');
|
||||
@@ -655,11 +683,7 @@
|
||||
const uploadBtn = document.getElementById('uploadBtn');
|
||||
const loadingOverlay = document.getElementById('loadingOverlay');
|
||||
const loadingText = document.getElementById('loadingText');
|
||||
const statusMessage = document.getElementById('statusMessage');
|
||||
const statusIcon = document.getElementById('statusIcon');
|
||||
const statusTitle = document.getElementById('statusTitle');
|
||||
const statusDesc = document.getElementById('statusDesc');
|
||||
const statusBtn = document.getElementById('statusBtn');
|
||||
const statusMessagesContainer = document.getElementById('statusMessagesContainer');
|
||||
const imagePreviewModal = document.getElementById('imagePreviewModal');
|
||||
const previewImage = document.getElementById('previewImage');
|
||||
const closePreview = document.getElementById('closePreview');
|
||||
@@ -673,6 +697,8 @@
|
||||
let isUploading = false;
|
||||
let uploadedCount = 0; // 已上传的总文件数量
|
||||
|
||||
let statusMessageCount = 0; // 弹窗计数器
|
||||
|
||||
// 初始化
|
||||
function init() {
|
||||
// 更新限制提示文本
|
||||
@@ -688,12 +714,28 @@
|
||||
fileInput.addEventListener('change', handleFileSelect);
|
||||
clearBtn.addEventListener('click', clearAllFiles);
|
||||
uploadBtn.addEventListener('click', startUpload);
|
||||
statusBtn.addEventListener('click', hideStatus);
|
||||
closePreview.addEventListener('click', () => {
|
||||
imagePreviewModal.classList.remove('show');
|
||||
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) => {
|
||||
if (e.target === imagePreviewModal) {
|
||||
@@ -706,7 +748,9 @@
|
||||
setupDragAndDrop();
|
||||
|
||||
// 防止页面滚动
|
||||
document.body.addEventListener('touchmove', preventScroll, { passive: false });
|
||||
document.body.addEventListener('touchmove', preventScroll, {
|
||||
passive: false
|
||||
});
|
||||
|
||||
// 更新UI
|
||||
updateUI();
|
||||
@@ -795,7 +839,7 @@
|
||||
}
|
||||
|
||||
// 处理文件
|
||||
function handleFiles(files) {
|
||||
async function handleFiles(files) {
|
||||
// 检查是否已达到总文件数量上限
|
||||
if (MAX_FILE_COUNT > 0 && uploadedCount >= MAX_FILE_COUNT) {
|
||||
showError(`已超过文件上传总数限制(${MAX_FILE_COUNT}个)`);
|
||||
@@ -816,6 +860,7 @@
|
||||
}
|
||||
|
||||
for (let file of files) {
|
||||
console.log(file);
|
||||
// 检查是否已达到本次选择的文件数量上限
|
||||
if (selectedFiles.length >= MAX_FILE_COUNT) {
|
||||
showError(`最多只能选择 ${MAX_FILE_COUNT} 个文件,已忽略多余文件`);
|
||||
@@ -839,12 +884,18 @@
|
||||
continue;
|
||||
}
|
||||
|
||||
// 添加到文件列表
|
||||
fileValidator
|
||||
.validate(file)
|
||||
.then(() => {
|
||||
console.log(1111);
|
||||
selectedFiles.push(file);
|
||||
addFileToList(file);
|
||||
}
|
||||
|
||||
updateUI();
|
||||
})
|
||||
.catch((err) => {
|
||||
showError(file.name + err);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否为图片文件
|
||||
@@ -888,12 +939,26 @@
|
||||
};
|
||||
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) {
|
||||
const li = document.createElement('li');
|
||||
li.className = 'file-item';
|
||||
li.dataset.name = file.name;
|
||||
li.dataset.size = file.size; // 添加 size 到 dataset
|
||||
|
||||
// 检查是否为图片
|
||||
const isImage = isImageType(file) && isImageFile(file.name);
|
||||
@@ -901,7 +966,7 @@
|
||||
// 获取文件图标
|
||||
const fileIcon = getFileIcon(file.name);
|
||||
|
||||
// 创建HTML
|
||||
// 创建HTML(移除 onclick,改为 data 属性)
|
||||
li.innerHTML = `
|
||||
<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>
|
||||
</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);
|
||||
@@ -934,18 +999,17 @@
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 生成缩略图
|
||||
function generateThumbnail(file, containerId) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (e) {
|
||||
reader.onload = function(e) {
|
||||
const container = document.getElementById(containerId);
|
||||
if (container) {
|
||||
const img = container.querySelector('.thumbnail');
|
||||
img.src = e.target.result;
|
||||
|
||||
// 添加加载完成后的淡入效果
|
||||
img.onload = function () {
|
||||
img.onload = function() {
|
||||
img.style.opacity = '0';
|
||||
setTimeout(() => {
|
||||
img.style.transition = 'opacity 0.3s ease';
|
||||
@@ -960,7 +1024,7 @@
|
||||
// 预览完整图片
|
||||
function previewFullImage(file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function (e) {
|
||||
reader.onload = function(e) {
|
||||
previewImage.src = e.target.result;
|
||||
imagePreviewModal.classList.add('show');
|
||||
document.body.style.overflow = 'hidden';
|
||||
@@ -977,20 +1041,6 @@
|
||||
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() {
|
||||
selectedFiles = [];
|
||||
@@ -1037,7 +1087,8 @@
|
||||
fileInput.disabled = true;
|
||||
} else {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1197,43 +1248,129 @@
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
// 显示成功提示
|
||||
function showSuccess(message) {
|
||||
statusMessage.className = 'status-message status-success';
|
||||
statusIcon.textContent = '✅';
|
||||
statusTitle.textContent = '上传成功!';
|
||||
statusDesc.textContent = message;
|
||||
statusMessage.style.display = 'block';
|
||||
document.body.style.overflow = 'hidden';
|
||||
// 创建状态提示框
|
||||
function createStatusMessage(type, title, desc, autoClose = false, closeTime = 3000) {
|
||||
// 创建唯一的ID
|
||||
const messageId = 'status-' + Date.now() + '-' + Math.random().toString(36).substr(2, 9);
|
||||
|
||||
// 计算当前弹窗的偏移量(每个弹窗下移20px)
|
||||
const offset = statusMessageCount * 40;
|
||||
|
||||
// 创建弹窗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);
|
||||
}
|
||||
|
||||
// 显示错误提示
|
||||
function showError(message) {
|
||||
statusMessage.className = 'status-message status-error';
|
||||
statusIcon.textContent = '❌';
|
||||
statusTitle.textContent = '出错了';
|
||||
statusDesc.textContent = message;
|
||||
statusMessage.style.display = 'block';
|
||||
document.body.style.overflow = 'hidden';
|
||||
// 限制最大显示数量(最多显示5个)
|
||||
const messages = statusMessagesContainer.querySelectorAll('.status-message');
|
||||
if (messages.length > 5) {
|
||||
// 移除最早的消息
|
||||
const oldestMessage = messages[0];
|
||||
closeStatusMessage(oldestMessage.id);
|
||||
}
|
||||
|
||||
// 隐藏状态提示
|
||||
function hideStatus() {
|
||||
statusMessage.style.display = 'none';
|
||||
return messageId;
|
||||
}
|
||||
|
||||
// 关闭状态提示框
|
||||
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 = '';
|
||||
}
|
||||
|
||||
// 防止页面滚动
|
||||
// 显示成功提示(创建新弹窗)
|
||||
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) {
|
||||
const messages = statusMessagesContainer.querySelectorAll('.status-message');
|
||||
const hasMessages = messages.length > 0;
|
||||
|
||||
if (
|
||||
loadingOverlay.classList.contains('show') ||
|
||||
statusMessage.style.display === 'block' ||
|
||||
hasMessages ||
|
||||
imagePreviewModal.classList.contains('show')
|
||||
) {
|
||||
e.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
// HTML转义
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 语法校验
|
||||
Reference in New Issue
Block a user