feat: add post upload action
This commit is contained in:
@@ -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=="],
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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}`));
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
<Column field="price" header="价格" sortable>
|
||||
<template #body="{ data }">
|
||||
<div class="text-sm text-gray-900">
|
||||
<span class="line-through text-gray-500" v-if="data.discount != 100">
|
||||
{{ formatPrice(data.price) }}
|
||||
</span>
|
||||
<span :class="{ 'ml-2': data.discount != 100 }">
|
||||
{{ formatPrice(calculateDiscountPrice(data.price, data.discount)) }}
|
||||
</span>
|
||||
<span v-if="data.discount != 100" class="ml-2 text-red-500">
|
||||
({{ data.discount }}%)
|
||||
</span>
|
||||
<div class="flex flex-col">
|
||||
<span class="text-gray-500">原价: {{ formatPrice(data.price) }}</span>
|
||||
<span class="text-orange-500">优惠: -{{ formatPrice(getDiscountAmount(data.price,
|
||||
data.discount)) }}</span>
|
||||
<span class="font-bold">实付: {{ formatPrice(getFinalPrice(data.price, data.discount))
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
</Column>
|
||||
|
||||
Reference in New Issue
Block a user