feat: add post upload action

This commit is contained in:
yanghao05
2025-04-11 09:40:13 +08:00
parent d940c182a4
commit 736991e3ea
5 changed files with 72 additions and 30 deletions

View File

@@ -13,6 +13,7 @@
"install": "^0.13.0", "install": "^0.13.0",
"primeicons": "^7.0.0", "primeicons": "^7.0.0",
"primevue": "^4.3.3", "primevue": "^4.3.3",
"spark-md5": "^3.0.2",
"tailwindcss": "^4.0.17", "tailwindcss": "^4.0.17",
"tailwindcss-primeui": "^0.6.1", "tailwindcss-primeui": "^0.6.1",
"vue": "^3.5.13", "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=="], "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": ["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=="], "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=="],

View File

@@ -18,6 +18,7 @@
"install": "^0.13.0", "install": "^0.13.0",
"primeicons": "^7.0.0", "primeicons": "^7.0.0",
"primevue": "^4.3.3", "primevue": "^4.3.3",
"spark-md5": "^3.0.2",
"tailwindcss": "^4.0.17", "tailwindcss": "^4.0.17",
"tailwindcss-primeui": "^0.6.1", "tailwindcss-primeui": "^0.6.1",
"vue": "^3.5.13", "vue": "^3.5.13",

View File

@@ -11,6 +11,10 @@ export const mediaService = {
return httpClient.get('/admin/uploads/token'); return httpClient.get('/admin/uploads/token');
}, },
uploadedSuccess(data) {
return httpClient.post('/admin/uploads/post-uploaded-action', data);
},
createMedia(mediaInfo) { createMedia(mediaInfo) {
return httpClient.post('/admin/medias', mediaInfo); return httpClient.post('/admin/medias', mediaInfo);
} }

View File

@@ -4,6 +4,7 @@ import Badge from 'primevue/badge';
import Button from 'primevue/button'; import Button from 'primevue/button';
import Card from 'primevue/card'; import Card from 'primevue/card';
import { useToast } from 'primevue/usetoast'; import { useToast } from 'primevue/usetoast';
import SparkMD5 from 'spark-md5';
import { computed, onMounted, ref } from 'vue'; import { computed, onMounted, ref } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
@@ -53,15 +54,6 @@ const addToHistory = (file, status = 'uploading') => {
return historyItem; 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 // Clear completed uploads
const clearCompleted = () => { const clearCompleted = () => {
uploadHistory.value = uploadHistory.value.filter(item => item.status !== 'completed'); uploadHistory.value = uploadHistory.value.filter(item => item.status !== 'completed');
@@ -71,14 +63,13 @@ const clearCompleted = () => {
const getOssToken = async () => { const getOssToken = async () => {
try { try {
const response = await mediaService.getUploadToken(); const response = await mediaService.getUploadToken();
ossConfig.value = response; ossConfig.value = response.data;
} catch (error) { } catch (error) {
toast.add({ severity: 'error', summary: '错误', detail: '获取上传凭证失败', life: 3000 }); toast.add({ severity: 'error', summary: '错误', detail: '获取上传凭证失败', life: 3000 });
} }
}; };
// Add pending uploads state // Add pending uploads state
const pendingFiles = ref([]);
const uploadQueue = ref([]); const uploadQueue = ref([]);
const currentUploadIndex = ref(-1); 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 // Modify uploadFile function
const uploadFile = async (file) => { const uploadFile = async (file) => {
try { try {
@@ -144,6 +169,11 @@ const uploadFile = async (file) => {
await getOssToken(); 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) => { await new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest(); const xhr = new XMLHttpRequest();
const formData = new FormData(); 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-signature-version', 'OSS4-HMAC-SHA256');
formData.append('x-oss-credential', ossConfig.value.x_oss_credential); formData.append('x-oss-credential', ossConfig.value.x_oss_credential);
formData.append('x-oss-date', ossConfig.value.x_oss_date); 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('x-oss-security-token', ossConfig.value.security_token);
formData.append('callback', ossConfig.value.callback); formData.append('callback', ossConfig.value.callback);
formData.append('file', file); formData.append('file', file);
@@ -170,7 +200,15 @@ const uploadFile = async (file) => {
xhr.onreadystatechange = () => { xhr.onreadystatechange = () => {
if (xhr.readyState === 4) { if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) { 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 { } else {
reject(new Error(`Upload failed with status: ${xhr.status}`)); reject(new Error(`Upload failed with status: ${xhr.status}`));
} }

View File

@@ -101,10 +101,6 @@ const confirmDelete = (post) => {
icon: 'pi pi-exclamation-triangle', icon: 'pi pi-exclamation-triangle',
acceptClass: 'p-button-danger', acceptClass: 'p-button-danger',
accept: () => { 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 // call remote delete
postService.deletePost(post.id) postService.deletePost(post.id)
.then(() => { .then(() => {
@@ -126,9 +122,13 @@ const formatDate = (date) => {
return dayjs.tz(date, 'Asia/Shanghai').format('YYYY-MM-DD HH:mm:ss'); return dayjs.tz(date, 'Asia/Shanghai').format('YYYY-MM-DD HH:mm:ss');
}; };
// Calculate price after discount
const calculateDiscountPrice = (price, discount) => { // Add these helper functions next to existing price-related functions
if (!discount || discount >= 100) return price; const getDiscountAmount = (price, discount) => {
return price * (100 - discount) / 100;
};
const getFinalPrice = (price, discount) => {
return price * discount / 100; return price * discount / 100;
}; };
@@ -257,16 +257,12 @@ const formatMediaTypes = (mediaTypes) => {
<Column field="price" header="价格" sortable> <Column field="price" header="价格" sortable>
<template #body="{ data }"> <template #body="{ data }">
<div class="text-sm text-gray-900"> <div class="flex flex-col">
<span class="line-through text-gray-500" v-if="data.discount != 100"> <span class="text-gray-500">原价: {{ formatPrice(data.price) }}</span>
{{ formatPrice(data.price) }} <span class="text-orange-500">优惠: -{{ formatPrice(getDiscountAmount(data.price,
</span> data.discount)) }}</span>
<span :class="{ 'ml-2': data.discount != 100 }"> <span class="font-bold">实付: {{ formatPrice(getFinalPrice(data.price, data.discount))
{{ formatPrice(calculateDiscountPrice(data.price, data.discount)) }} }}</span>
</span>
<span v-if="data.discount != 100" class="ml-2 text-red-500">
({{ data.discount }}%)
</span>
</div> </div>
</template> </template>
</Column> </Column>