fix: create post

This commit is contained in:
yanghao05
2025-04-09 20:32:31 +08:00
parent 05d2ecc2b9
commit 2346983d67
10 changed files with 192 additions and 95 deletions

View File

@@ -27,7 +27,7 @@ httpClient.interceptors.request.use(
// Response interceptor
httpClient.interceptors.response.use(
response => {
return response.data;
return response
},
error => {
// Handle HTTP errors here

View File

@@ -13,4 +13,14 @@ export const postService = {
getPost(id) {
return httpClient.get(`/admin/posts/${id}`);
},
};
createPost(post) {
return httpClient.post('/admin/posts', post);
},
updatePost(id, post) {
return httpClient.put(`/admin/posts/${id}`, post);
},
deletePost(id) {
return httpClient.delete(`/admin/posts/${id}`);
},
}

View File

@@ -1,18 +1,18 @@
<script setup>
import { mediaService } from '@/api/mediaService';
import { InputText } from 'primevue';
import Badge from 'primevue/badge';
import Button from 'primevue/button';
import Column from 'primevue/column';
import ConfirmDialog from 'primevue/confirmdialog';
import DataTable from 'primevue/datatable';
import Dropdown from 'primevue/dropdown';
import ProgressSpinner from 'primevue/progressspinner';
import Toast from 'primevue/toast';
import { useConfirm } from 'primevue/useconfirm';
import { useToast } from 'primevue/usetoast';
import { computed, onMounted, ref } from 'vue';
import { useRouter } from 'vue-router';
import { mediaService } from "@/api/mediaService";
import { InputText } from "primevue";
import Badge from "primevue/badge";
import Button from "primevue/button";
import Column from "primevue/column";
import ConfirmDialog from "primevue/confirmdialog";
import DataTable from "primevue/datatable";
import Dropdown from "primevue/dropdown";
import ProgressSpinner from "primevue/progressspinner";
import Toast from "primevue/toast";
import { useConfirm } from "primevue/useconfirm";
import { useToast } from "primevue/usetoast";
import { computed, onMounted, ref } from "vue";
import { useRouter } from "vue-router";
// Import useConfirm dynamically to avoid the error when the service is not provided
const confirm = useConfirm();
@@ -22,24 +22,24 @@ const router = useRouter();
// Remove upload related refs and methods
const openUploadDialog = () => {
router.push('/medias/uploads');
router.push("/medias/uploads");
};
// Media types for filtering
const mediaTypes = ref([
{ name: '所有媒体', value: null },
{ name: '图片', value: 'Image' },
{ name: '视频', value: 'Video' },
{ name: '文档', value: 'Document' },
{ name: '音频', value: 'Audio' },
{ name: 'PDF', value: 'PDF' }
{ name: "所有媒体", value: null },
{ name: "图片", value: "Image" },
{ name: "视频", value: "Video" },
{ name: "文档", value: "Document" },
{ name: "音频", value: "Audio" },
{ name: "PDF", value: "PDF" },
]);
const selectedMediaType = ref(mediaTypes.value[0]);
const globalFilterValue = ref('');
const globalFilterValue = ref("");
const loading = ref(false);
const filters = ref({
global: { value: null, matchMode: 'contains' }
global: { value: null, matchMode: "contains" },
});
// Add pagination state
@@ -62,13 +62,18 @@ const fetchMediaFiles = async () => {
try {
const response = await mediaService.getMedias({
page: currentPage.value,
limit: rows.value
limit: rows.value,
});
mediaFiles.value = response.items;
totalRecords.value = response.total;
mediaFiles.value = response.data.items;
totalRecords.value = response.data.total;
console.log(totalRecords.value);
} catch (error) {
toast.add({ severity: 'error', summary: '错误', detail: '加载媒体文件失败', life: 3000 });
toast.add({
severity: "error",
summary: "错误",
detail: "加载媒体文件失败",
life: 3000,
});
} finally {
loading.value = false;
}
@@ -80,7 +85,7 @@ const onPage = (event) => {
rows.value = event.rows;
currentPage.value = Math.floor(event.first / event.rows) + 1;
console.log(event)
console.log(event);
fetchMediaFiles();
};
@@ -91,13 +96,13 @@ onMounted(() => {
// File type badge severity mapping
const getBadgeSeverity = (fileType) => {
const map = {
'Image': 'info',
'PDF': 'danger',
'Video': 'warning',
'Document': 'primary',
'Audio': 'success'
Image: "info",
PDF: "danger",
Video: "warning",
Document: "primary",
Audio: "success",
};
return map[fileType] || 'info';
return map[fileType] || "info";
};
// File type icon mapping
@@ -108,9 +113,9 @@ const getFileIcon = (file) => {
// Add helper function to format file size
const formatFileSize = (bytes) => {
if (bytes === 0) return '0 B';
if (bytes === 0) return "0 B";
const k = BigInt(1024);
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const sizes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
// Convert input to BigInt
const bytesValue = BigInt(bytes);
@@ -124,7 +129,7 @@ const formatFileSize = (bytes) => {
}
// Convert back to number and format with commas
return `${Number(size).toLocaleString('en-US')} ${sizes[i]}`;
return `${Number(size).toLocaleString("en-US")} ${sizes[i]}`;
};
</script>
@@ -153,9 +158,7 @@ const formatFileSize = (bytes) => {
current-page-report-template=" {first} {last} {totalRecords} " :lazy="true"
:show-current-page-report="true">
<template #paginatorLeft>
<div class="flex items-center">
每页: {{ rows }}
</div>
<div class="flex items-center">每页: {{ rows }}</div>
</template>
<template #paginatorRight>
<div class="flex items-center">
@@ -167,7 +170,7 @@ const formatFileSize = (bytes) => {
</template>
<template #loading>
<div class="flex flex-col items-center justify-center p-4">
<ProgressSpinner style="width:50px;height:50px" />
<ProgressSpinner style="width: 50px; height: 50px" />
<span class="mt-2">加载中...</span>
</div>
</template>
@@ -176,7 +179,8 @@ const formatFileSize = (bytes) => {
<template #body="{ data }">
<div class="flex items-center">
<div v-if="data.thumbnail_url" class="flex-shrink-0 h-10 w-10 mr-3">
<img class="h-10 w-10 object-cover rounded" :src="data.thumbnail_url" :alt="data.name">
<img class="h-10 w-10 object-cover rounded" :src="data.thumbnail_url"
:alt="data.name" />
</div>
<div v-else
class="flex-shrink-0 h-10 w-10 mr-3 bg-gray-100 rounded flex items-center justify-center">
@@ -205,7 +209,7 @@ const formatFileSize = (bytes) => {
</template>
</Column>
<Column header="" :exportable="false" style="min-width:8rem">
<Column header="" :exportable="false" style="min-width: 8rem">
<template #body="{ data }">
<div class="flex justify-center space-x-2">
<Button icon="pi pi-eye" rounded text severity="info" @click="previewFile(data)"

View File

@@ -1,5 +1,6 @@
<script setup>
import { mediaService } from '@/api/mediaService';
import { postService } from '@/api/postService';
import { useToast } from 'primevue/usetoast';
import { computed, reactive, ref } from 'vue';
import { useRouter } from 'vue-router';
@@ -24,16 +25,19 @@ const toast = useToast();
const post = reactive({
title: '',
price: 0,
discount: 100, // Add discount field with default value
introduction: '',
selectedMedia: [],
status: 'draft' // Add status field with default value
medias: [],
status: 0,
});
// Validation state
const errors = reactive({
title: '',
introduction: '',
selectedMedia: ''
selectedMedia: '',
discount: ''
});
// Media selection dialog state
@@ -55,8 +59,8 @@ const mediaTotalPages = computed(() => {
// Status options
const statusOptions = [
{ label: '发布', value: 'published' },
{ label: '草稿', value: 'draft' }
{ label: '发布', value: 1 },
{ label: '草稿', value: 0 }
];
// Open media selection dialog
@@ -75,8 +79,9 @@ const loadMediaItems = async () => {
page: mediaCurrentPage.value,
limit: mediaRows.value
});
mediaItems.value = response.items;
mediaTotalRecords.value = response.total;
console.log(response.data.items)
mediaItems.value = response.data.items;
mediaTotalRecords.value = response.data.total;
} catch (error) {
toast.add({ severity: 'error', summary: '错误', detail: '加载媒体文件失败', life: 3000 });
} finally {
@@ -118,6 +123,7 @@ const removeMedia = (media) => {
const savePost = async () => {
// Reset errors
Object.keys(errors).forEach(key => errors[key] = '');
console.log(post.value)
// Validate form
let valid = true;
@@ -137,18 +143,26 @@ const savePost = async () => {
valid = false;
}
// check discount
if (post.discount < 0 || post.discount > 100) {
errors.discount = '折扣必须在0到100之间';
valid = false;
}
if (!valid) {
toast.add({ severity: 'error', summary: '表单错误', detail: '请检查表单中的错误并修正', life: 3000 });
return;
}
try {
// In a real app, you would call an API to save the post
// await postApi.createPost(post);
// Simulate API delay
await new Promise(resolve => setTimeout(resolve, 1000));
post.medias = post.selectedMedia.map(media => media.id)
const resp = await postService.createPost(post);
console.log(resp)
if (resp.status !== 200) {
toast.add({ severity: 'error', summary: '错误', detail: resp.message, life: 3000 });
return;
}
toast.add({ severity: 'success', summary: '成功', detail: '文章已成功创建', life: 3000 });
// Navigate back to the post list
@@ -219,16 +233,23 @@ const formatFileSize = (bytes) => {
<div class="col-span-2">
<label for="title" class="block text-sm font-medium text-gray-700 mb-1">标题</label>
<InputText id="title" v-model="post.title" class="w-full p-inputtext-lg" placeholder="输入文章标题" />
<small v-if="errors.title" class="p-error">{{ errors.title }}</small>
<small v-if="errors.title" class="text-red-500">{{ errors.title }}</small>
</div>
<!-- Price -->
<div class="col-span-2">
<div class="col-span-1">
<label for="price" class="block text-sm font-medium text-gray-700 mb-1">价格 (¥)</label>
<InputNumber id="price" v-model="post.price" mode="currency" currency="CNY" :minFractionDigits="2"
class="w-full" />
</div>
<!-- Discount -->
<div class="col-span-1">
<label for="discount" class="block text-sm font-medium text-gray-700 mb-1">折扣 (%)</label>
<InputNumber id="discount" v-model="post.discount" :min="0" :max="100" :minFractionDigits="0"
:maxFractionDigits="0" class="w-full" placeholder="输入折扣百分比" />
</div>
<!-- Status -->
<div class="col-span-2">
<label class="block text-sm font-medium text-gray-700 mb-1">状态</label>
@@ -247,7 +268,7 @@ const formatFileSize = (bytes) => {
<label for="introduction" class="block text-sm font-medium text-gray-700 mb-1">文章介绍</label>
<Textarea id="introduction" v-model="post.introduction" rows="5" class="w-full"
placeholder="输入文章介绍内容" />
<small v-if="errors.introduction" class="p-error">{{ errors.introduction }}</small>
<small v-if="errors.introduction" class="text-red-500">{{ errors.introduction }}</small>
</div>
<!-- Media Selection -->
@@ -259,7 +280,7 @@ const formatFileSize = (bytes) => {
<i class="pi pi-image text-gray-400 text-5xl!"></i>
<p class="text-gray-500">尚未选择任何媒体文件</p>
<Button label="选择媒体" icon="pi pi-plus" @click="openMediaDialog" outlined />
<small v-if="errors.selectedMedia" class="p-error">{{ errors.selectedMedia }}</small>
<small v-if="errors.selectedMedia" class="text-red-500">{{ errors.selectedMedia }}</small>
</div>
<div v-else>
<div class="mb-4">

View File

@@ -131,7 +131,7 @@ const fetchPosts = async () => {
keyword: globalFilterValue.value
});
posts.value = response.items.map(post => ({
posts.value = response.data.items.map(post => ({
...post,
status: statusMap[post.status] || '未知',
mediaTypes: getMediaTypes(post.assets),
@@ -140,7 +140,7 @@ const fetchPosts = async () => {
viewCount: post.views,
likes: post.likes
}));
total.value = response.total;
total.value = response.data.total;
} catch (error) {
console.error('Fetch error:', error); // Debug log
toast.add({ severity: 'error', summary: '错误', detail: '加载文章失败', life: 3000 });