diff --git a/frontend/admin/bun.lock b/frontend/admin/bun.lock index e5857d7..357be13 100644 --- a/frontend/admin/bun.lock +++ b/frontend/admin/bun.lock @@ -13,6 +13,7 @@ "install": "^0.13.0", "primeicons": "^7.0.0", "primevue": "^4.3.3", + "spark-md5": "^3.0.2", "tailwindcss": "^4.0.17", "tailwindcss-primeui": "^0.6.1", "vue": "^3.5.13", @@ -299,6 +300,8 @@ "source-map-js": ["source-map-js@1.2.1", "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], + "spark-md5": ["spark-md5@3.0.2", "https://registry.npmmirror.com/spark-md5/-/spark-md5-3.0.2.tgz", {}, "sha512-wcFzz9cDfbuqe0FZzfi2or1sgyIrsDwmPwfZC4hiNidPdPINjeUwNfv5kldczoEAcjl9Y1L3SM7Uz2PUEQzxQw=="], + "tailwindcss": ["tailwindcss@4.0.17", "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-4.0.17.tgz", {}, "sha512-OErSiGzRa6rLiOvaipsDZvLMSpsBZ4ysB4f0VKGXUrjw2jfkJRd6kjRKV2+ZmTCNvwtvgdDam5D7w6WXsdLJZw=="], "tailwindcss-primeui": ["tailwindcss-primeui@0.6.1", "https://registry.npmmirror.com/tailwindcss-primeui/-/tailwindcss-primeui-0.6.1.tgz", { "peerDependencies": { "tailwindcss": ">=3.1.0" } }, "sha512-T69Rylcrmnt8zy9ik+qZvsLuRIrS9/k6rYJSIgZ1trnbEzGDDQSCIdmfyZknevqiHwpSJHSmQ9XT2C+S/hJY4A=="], diff --git a/frontend/admin/package.json b/frontend/admin/package.json index 24820a9..0f19055 100644 --- a/frontend/admin/package.json +++ b/frontend/admin/package.json @@ -18,6 +18,7 @@ "install": "^0.13.0", "primeicons": "^7.0.0", "primevue": "^4.3.3", + "spark-md5": "^3.0.2", "tailwindcss": "^4.0.17", "tailwindcss-primeui": "^0.6.1", "vue": "^3.5.13", diff --git a/frontend/admin/src/api/mediaService.js b/frontend/admin/src/api/mediaService.js index 461d75e..6f55f84 100644 --- a/frontend/admin/src/api/mediaService.js +++ b/frontend/admin/src/api/mediaService.js @@ -11,6 +11,10 @@ export const mediaService = { return httpClient.get('/admin/uploads/token'); }, + uploadedSuccess(data) { + return httpClient.post('/admin/uploads/post-uploaded-action', data); + }, + createMedia(mediaInfo) { return httpClient.post('/admin/medias', mediaInfo); } diff --git a/frontend/admin/src/pages/MediaUploadPage.vue b/frontend/admin/src/pages/MediaUploadPage.vue index 8230c07..b8968f2 100644 --- a/frontend/admin/src/pages/MediaUploadPage.vue +++ b/frontend/admin/src/pages/MediaUploadPage.vue @@ -4,6 +4,7 @@ import Badge from 'primevue/badge'; import Button from 'primevue/button'; import Card from 'primevue/card'; import { useToast } from 'primevue/usetoast'; +import SparkMD5 from 'spark-md5'; import { computed, onMounted, ref } from 'vue'; import { useRouter } from 'vue-router'; @@ -53,15 +54,6 @@ const addToHistory = (file, status = 'uploading') => { return historyItem; }; -// Update history item -const updateHistoryItem = (id, updates) => { - const index = uploadHistory.value.findIndex(item => item.id === id); - if (index !== -1) { - uploadHistory.value[index] = { ...uploadHistory.value[index], ...updates }; - saveUploadHistory(); - } -}; - // Clear completed uploads const clearCompleted = () => { uploadHistory.value = uploadHistory.value.filter(item => item.status !== 'completed'); @@ -71,14 +63,13 @@ const clearCompleted = () => { const getOssToken = async () => { try { const response = await mediaService.getUploadToken(); - ossConfig.value = response; + ossConfig.value = response.data; } catch (error) { toast.add({ severity: 'error', summary: '错误', detail: '获取上传凭证失败', life: 3000 }); } }; // Add pending uploads state -const pendingFiles = ref([]); const uploadQueue = ref([]); const currentUploadIndex = ref(-1); @@ -133,6 +124,40 @@ const updateQueueItemProgress = (index, progress) => { } }; +// Add MD5 calculation function +const calculateMD5 = (file) => { + return new Promise((resolve, reject) => { + const chunkSize = 2097152; // 2MB + const chunks = Math.ceil(file.size / chunkSize); + let currentChunk = 0; + const spark = new SparkMD5.ArrayBuffer(); + const fileReader = new FileReader(); + + fileReader.onload = (e) => { + spark.append(e.target.result); + currentChunk++; + + if (currentChunk < chunks) { + loadNext(); + } else { + resolve(spark.end()); + } + }; + + fileReader.onerror = (e) => { + reject(e); + }; + + function loadNext() { + const start = currentChunk * chunkSize; + const end = start + chunkSize >= file.size ? file.size : start + chunkSize; + fileReader.readAsArrayBuffer(file.slice(start, end)); + } + + loadNext(); + }); +}; + // Modify uploadFile function const uploadFile = async (file) => { try { @@ -144,6 +169,11 @@ const uploadFile = async (file) => { await getOssToken(); } + // Calculate MD5 before upload + const md5Hash = await calculateMD5(file); + const fileExt = file.name.split('.').pop(); + const newFileName = `${md5Hash}.${fileExt}`; + await new Promise((resolve, reject) => { const xhr = new XMLHttpRequest(); const formData = new FormData(); @@ -154,7 +184,7 @@ const uploadFile = async (file) => { formData.append('x-oss-signature-version', 'OSS4-HMAC-SHA256'); formData.append('x-oss-credential', ossConfig.value.x_oss_credential); formData.append('x-oss-date', ossConfig.value.x_oss_date); - formData.append('key', ossConfig.value.dir + file.name); + formData.append('key', ossConfig.value.dir + newFileName); formData.append('x-oss-security-token', ossConfig.value.security_token); formData.append('callback', ossConfig.value.callback); formData.append('file', file); @@ -170,7 +200,15 @@ const uploadFile = async (file) => { xhr.onreadystatechange = () => { if (xhr.readyState === 4) { if (xhr.status >= 200 && xhr.status < 300) { - resolve(xhr.response); + // Send additional info to backend after successful upload + mediaService.uploadedSuccess({ + originalName: file.name, + md5: md5Hash, + mimeType: file.type, + size: file.size + }).then(() => { + resolve(xhr.response); + }).catch(reject); } else { reject(new Error(`Upload failed with status: ${xhr.status}`)); } diff --git a/frontend/admin/src/pages/PostPage.vue b/frontend/admin/src/pages/PostPage.vue index c331aae..8576464 100644 --- a/frontend/admin/src/pages/PostPage.vue +++ b/frontend/admin/src/pages/PostPage.vue @@ -101,10 +101,6 @@ const confirmDelete = (post) => { icon: 'pi pi-exclamation-triangle', acceptClass: 'p-button-danger', accept: () => { - // // In a real app, you would call an API to delete the post - // posts.value = posts.value.filter(p => p.id !== post.id); - // toast.add({ severity: 'success', summary: '成功', detail: '文章已删除', life: 3000 }); - // call remote delete postService.deletePost(post.id) .then(() => { @@ -126,9 +122,13 @@ const formatDate = (date) => { return dayjs.tz(date, 'Asia/Shanghai').format('YYYY-MM-DD HH:mm:ss'); }; -// Calculate price after discount -const calculateDiscountPrice = (price, discount) => { - if (!discount || discount >= 100) return price; + +// Add these helper functions next to existing price-related functions +const getDiscountAmount = (price, discount) => { + return price * (100 - discount) / 100; +}; + +const getFinalPrice = (price, discount) => { return price * discount / 100; }; @@ -257,16 +257,12 @@ const formatMediaTypes = (mediaTypes) => {