fix: create post
This commit is contained in:
@@ -3,9 +3,11 @@ package admin
|
||||
import (
|
||||
"quyun/app/models"
|
||||
"quyun/app/requests"
|
||||
"quyun/database/fields"
|
||||
"quyun/database/schemas/public/model"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
type ListQuery struct {
|
||||
@@ -24,10 +26,48 @@ func (ctl *posts) List(ctx fiber.Ctx, pagination *requests.Pagination, query *Li
|
||||
return models.Posts.List(ctx.Context(), pagination, cond)
|
||||
}
|
||||
|
||||
type PostForm struct {
|
||||
Title string `json:"title"`
|
||||
Price int64 `json:"price"`
|
||||
Discount int16 `json:"discount"`
|
||||
Introduction string `json:"introduction"`
|
||||
Medias []int64 `json:"medias"`
|
||||
Status fields.PostStatus `json:"status"`
|
||||
}
|
||||
|
||||
// Create
|
||||
// @Router /v1/admin/posts [post]
|
||||
// @Bind form body
|
||||
func (ctl *posts) Create(ctx fiber.Ctx, form *model.Posts) error {
|
||||
func (ctl *posts) Create(ctx fiber.Ctx, form *PostForm) error {
|
||||
post := model.Posts{
|
||||
Title: form.Title,
|
||||
Price: form.Price,
|
||||
Discount: form.Discount,
|
||||
Description: form.Introduction,
|
||||
Status: form.Status,
|
||||
Content: "",
|
||||
Tags: fields.Json[[]string]{},
|
||||
Assets: fields.Json[[]fields.MediaAsset]{},
|
||||
}
|
||||
|
||||
if form.Medias != nil {
|
||||
medias, err := models.Medias.GetByIds(ctx.Context(), form.Medias)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
assets := lo.Map(medias, func(media *model.Medias, _ int) fields.MediaAsset {
|
||||
return fields.MediaAsset{
|
||||
Type: models.Medias.ConvertFileTypeByMimeType(media.MimeType),
|
||||
Media: media.ID,
|
||||
Mark: nil,
|
||||
}
|
||||
})
|
||||
post.Assets = fields.ToJson(assets)
|
||||
}
|
||||
|
||||
if err := models.Posts.Create(ctx.Context(), &post); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ func (r *Routes) Register(router fiber.Router) {
|
||||
|
||||
router.Post("/v1/admin/posts", Func1(
|
||||
r.posts.Create,
|
||||
Body[model.Posts]("form"),
|
||||
Body[PostForm]("form"),
|
||||
))
|
||||
|
||||
router.Put("/v1/admin/posts/:id", Func2(
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
|
||||
"quyun/app/requests"
|
||||
"quyun/database/fields"
|
||||
"quyun/database/schemas/public/model"
|
||||
"quyun/database/schemas/public/table"
|
||||
|
||||
@@ -12,25 +13,13 @@ import (
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type MediaType string
|
||||
|
||||
const (
|
||||
MediaTypeUnknown MediaType = "unknown"
|
||||
MediaTypeArchive MediaType = "archive"
|
||||
MediaTypeImage MediaType = "image"
|
||||
MediaTypeVideo MediaType = "video"
|
||||
MediaTypeDocument MediaType = "document"
|
||||
MediaTypeAudio MediaType = "audio"
|
||||
MediaTypePDF MediaType = "pdf"
|
||||
)
|
||||
|
||||
type MediaItem struct {
|
||||
ID int64 `json:"id"`
|
||||
Name string `json:"name"`
|
||||
UploadTime string `json:"upload_time"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
MimeType string `json:"media_type"`
|
||||
FileType MediaType `json:"file_type"`
|
||||
FileType fields.MediaAssetType `json:"file_type"`
|
||||
ThumbnailUrl string `json:"thumbnail_url"`
|
||||
}
|
||||
|
||||
@@ -150,30 +139,57 @@ func (m *mediasModel) Create(ctx context.Context, model *model.Medias) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mediasModel) ConvertFileTypeByMimeType(mimeType string) MediaType {
|
||||
func (m *mediasModel) ConvertFileTypeByMimeType(mimeType string) fields.MediaAssetType {
|
||||
switch mimeType {
|
||||
case "image/jpeg", "image/jpg", "image/png":
|
||||
return MediaTypeImage
|
||||
case "video/mp4":
|
||||
return MediaTypeVideo
|
||||
case "image/jpeg", "image/jpg", "image/png", "image/gif":
|
||||
return fields.MediaAssetTypeImage
|
||||
case "video/mp4", "video/x-m4v":
|
||||
return fields.MediaAssetTypeVideo
|
||||
case "audio/mpeg":
|
||||
return MediaTypeAudio
|
||||
case "application/pdf":
|
||||
return MediaTypePDF
|
||||
case "application/msword",
|
||||
return fields.MediaAssetTypeAudio
|
||||
case "application/pdf",
|
||||
"application/msword",
|
||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||
"application/vnd.ms-excel",
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
"application/vnd.ms-powerpoint",
|
||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation":
|
||||
return MediaTypeDocument
|
||||
return fields.MediaAssetTypeDocument
|
||||
case "application/rar",
|
||||
"application/x-rar-compressed",
|
||||
"application/x-zip-compressed",
|
||||
"application/x-zip",
|
||||
"application/zip",
|
||||
"application/x-7z-compressed":
|
||||
return MediaTypeArchive
|
||||
return fields.MediaAssetTypeArchive
|
||||
}
|
||||
return MediaTypeUnknown
|
||||
return fields.MediaAssetTypeUnknown
|
||||
}
|
||||
|
||||
// GetByIds
|
||||
func (m *mediasModel) GetByIds(ctx context.Context, ids []int64) ([]*model.Medias, error) {
|
||||
if len(ids) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
condIds := lo.Map(ids, func(id int64, _ int) Expression {
|
||||
return Int64(id)
|
||||
})
|
||||
|
||||
tbl := table.Medias
|
||||
stmt := tbl.
|
||||
SELECT(tbl.AllColumns).
|
||||
WHERE(tbl.ID.IN(condIds...))
|
||||
m.log.Infof("sql: %s", stmt.DebugSql())
|
||||
|
||||
var medias []model.Medias
|
||||
err := stmt.QueryContext(ctx, db, &medias)
|
||||
if err != nil {
|
||||
m.log.Errorf("error querying media items: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return lo.Map(medias, func(media model.Medias, _ int) *model.Medias {
|
||||
return &media
|
||||
}), nil
|
||||
}
|
||||
|
||||
@@ -27,6 +27,8 @@ const (
|
||||
MediaAssetTypeAudio MediaAssetType = "audio"
|
||||
// MediaAssetTypeDocument is a MediaAssetType of type Document.
|
||||
MediaAssetTypeDocument MediaAssetType = "document"
|
||||
// MediaAssetTypeArchive is a MediaAssetType of type Archive.
|
||||
MediaAssetTypeArchive MediaAssetType = "archive"
|
||||
// MediaAssetTypeOther is a MediaAssetType of type Other.
|
||||
MediaAssetTypeOther MediaAssetType = "other"
|
||||
)
|
||||
@@ -40,6 +42,7 @@ var _MediaAssetTypeNames = []string{
|
||||
string(MediaAssetTypeVideo),
|
||||
string(MediaAssetTypeAudio),
|
||||
string(MediaAssetTypeDocument),
|
||||
string(MediaAssetTypeArchive),
|
||||
string(MediaAssetTypeOther),
|
||||
}
|
||||
|
||||
@@ -59,6 +62,7 @@ func MediaAssetTypeValues() []MediaAssetType {
|
||||
MediaAssetTypeVideo,
|
||||
MediaAssetTypeAudio,
|
||||
MediaAssetTypeDocument,
|
||||
MediaAssetTypeArchive,
|
||||
MediaAssetTypeOther,
|
||||
}
|
||||
}
|
||||
@@ -82,6 +86,7 @@ var _MediaAssetTypeValue = map[string]MediaAssetType{
|
||||
"video": MediaAssetTypeVideo,
|
||||
"audio": MediaAssetTypeAudio,
|
||||
"document": MediaAssetTypeDocument,
|
||||
"archive": MediaAssetTypeArchive,
|
||||
"other": MediaAssetTypeOther,
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ type MediaAsset struct {
|
||||
// Video = "video",
|
||||
// Audio = "audio",
|
||||
// Document = "document",
|
||||
// Archive = "archive",
|
||||
// Other = "other"
|
||||
// )
|
||||
type MediaAssetType string
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}`);
|
||||
},
|
||||
}
|
||||
@@ -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">
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 });
|
||||
|
||||
Reference in New Issue
Block a user