fix: create post
This commit is contained in:
@@ -3,9 +3,11 @@ package admin
|
|||||||
import (
|
import (
|
||||||
"quyun/app/models"
|
"quyun/app/models"
|
||||||
"quyun/app/requests"
|
"quyun/app/requests"
|
||||||
|
"quyun/database/fields"
|
||||||
"quyun/database/schemas/public/model"
|
"quyun/database/schemas/public/model"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v3"
|
"github.com/gofiber/fiber/v3"
|
||||||
|
"github.com/samber/lo"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ListQuery struct {
|
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)
|
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
|
// Create
|
||||||
// @Router /v1/admin/posts [post]
|
// @Router /v1/admin/posts [post]
|
||||||
// @Bind form body
|
// @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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ func (r *Routes) Register(router fiber.Router) {
|
|||||||
|
|
||||||
router.Post("/v1/admin/posts", Func1(
|
router.Post("/v1/admin/posts", Func1(
|
||||||
r.posts.Create,
|
r.posts.Create,
|
||||||
Body[model.Posts]("form"),
|
Body[PostForm]("form"),
|
||||||
))
|
))
|
||||||
|
|
||||||
router.Put("/v1/admin/posts/:id", Func2(
|
router.Put("/v1/admin/posts/:id", Func2(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
"quyun/app/requests"
|
"quyun/app/requests"
|
||||||
|
"quyun/database/fields"
|
||||||
"quyun/database/schemas/public/model"
|
"quyun/database/schemas/public/model"
|
||||||
"quyun/database/schemas/public/table"
|
"quyun/database/schemas/public/table"
|
||||||
|
|
||||||
@@ -12,26 +13,14 @@ import (
|
|||||||
"github.com/sirupsen/logrus"
|
"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 {
|
type MediaItem struct {
|
||||||
ID int64 `json:"id"`
|
ID int64 `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
UploadTime string `json:"upload_time"`
|
UploadTime string `json:"upload_time"`
|
||||||
FileSize int64 `json:"file_size"`
|
FileSize int64 `json:"file_size"`
|
||||||
MimeType string `json:"media_type"`
|
MimeType string `json:"media_type"`
|
||||||
FileType MediaType `json:"file_type"`
|
FileType fields.MediaAssetType `json:"file_type"`
|
||||||
ThumbnailUrl string `json:"thumbnail_url"`
|
ThumbnailUrl string `json:"thumbnail_url"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// @provider
|
// @provider
|
||||||
@@ -150,30 +139,57 @@ func (m *mediasModel) Create(ctx context.Context, model *model.Medias) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mediasModel) ConvertFileTypeByMimeType(mimeType string) MediaType {
|
func (m *mediasModel) ConvertFileTypeByMimeType(mimeType string) fields.MediaAssetType {
|
||||||
switch mimeType {
|
switch mimeType {
|
||||||
case "image/jpeg", "image/jpg", "image/png":
|
case "image/jpeg", "image/jpg", "image/png", "image/gif":
|
||||||
return MediaTypeImage
|
return fields.MediaAssetTypeImage
|
||||||
case "video/mp4":
|
case "video/mp4", "video/x-m4v":
|
||||||
return MediaTypeVideo
|
return fields.MediaAssetTypeVideo
|
||||||
case "audio/mpeg":
|
case "audio/mpeg":
|
||||||
return MediaTypeAudio
|
return fields.MediaAssetTypeAudio
|
||||||
case "application/pdf":
|
case "application/pdf",
|
||||||
return MediaTypePDF
|
"application/msword",
|
||||||
case "application/msword",
|
|
||||||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||||||
"application/vnd.ms-excel",
|
"application/vnd.ms-excel",
|
||||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
"application/vnd.ms-powerpoint",
|
"application/vnd.ms-powerpoint",
|
||||||
"application/vnd.openxmlformats-officedocument.presentationml.presentation":
|
"application/vnd.openxmlformats-officedocument.presentationml.presentation":
|
||||||
return MediaTypeDocument
|
return fields.MediaAssetTypeDocument
|
||||||
case "application/rar",
|
case "application/rar",
|
||||||
"application/x-rar-compressed",
|
"application/x-rar-compressed",
|
||||||
"application/x-zip-compressed",
|
"application/x-zip-compressed",
|
||||||
"application/x-zip",
|
"application/x-zip",
|
||||||
"application/zip",
|
"application/zip",
|
||||||
"application/x-7z-compressed":
|
"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"
|
MediaAssetTypeAudio MediaAssetType = "audio"
|
||||||
// MediaAssetTypeDocument is a MediaAssetType of type Document.
|
// MediaAssetTypeDocument is a MediaAssetType of type Document.
|
||||||
MediaAssetTypeDocument MediaAssetType = "document"
|
MediaAssetTypeDocument MediaAssetType = "document"
|
||||||
|
// MediaAssetTypeArchive is a MediaAssetType of type Archive.
|
||||||
|
MediaAssetTypeArchive MediaAssetType = "archive"
|
||||||
// MediaAssetTypeOther is a MediaAssetType of type Other.
|
// MediaAssetTypeOther is a MediaAssetType of type Other.
|
||||||
MediaAssetTypeOther MediaAssetType = "other"
|
MediaAssetTypeOther MediaAssetType = "other"
|
||||||
)
|
)
|
||||||
@@ -40,6 +42,7 @@ var _MediaAssetTypeNames = []string{
|
|||||||
string(MediaAssetTypeVideo),
|
string(MediaAssetTypeVideo),
|
||||||
string(MediaAssetTypeAudio),
|
string(MediaAssetTypeAudio),
|
||||||
string(MediaAssetTypeDocument),
|
string(MediaAssetTypeDocument),
|
||||||
|
string(MediaAssetTypeArchive),
|
||||||
string(MediaAssetTypeOther),
|
string(MediaAssetTypeOther),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +62,7 @@ func MediaAssetTypeValues() []MediaAssetType {
|
|||||||
MediaAssetTypeVideo,
|
MediaAssetTypeVideo,
|
||||||
MediaAssetTypeAudio,
|
MediaAssetTypeAudio,
|
||||||
MediaAssetTypeDocument,
|
MediaAssetTypeDocument,
|
||||||
|
MediaAssetTypeArchive,
|
||||||
MediaAssetTypeOther,
|
MediaAssetTypeOther,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,6 +86,7 @@ var _MediaAssetTypeValue = map[string]MediaAssetType{
|
|||||||
"video": MediaAssetTypeVideo,
|
"video": MediaAssetTypeVideo,
|
||||||
"audio": MediaAssetTypeAudio,
|
"audio": MediaAssetTypeAudio,
|
||||||
"document": MediaAssetTypeDocument,
|
"document": MediaAssetTypeDocument,
|
||||||
|
"archive": MediaAssetTypeArchive,
|
||||||
"other": MediaAssetTypeOther,
|
"other": MediaAssetTypeOther,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ type MediaAsset struct {
|
|||||||
// Video = "video",
|
// Video = "video",
|
||||||
// Audio = "audio",
|
// Audio = "audio",
|
||||||
// Document = "document",
|
// Document = "document",
|
||||||
|
// Archive = "archive",
|
||||||
// Other = "other"
|
// Other = "other"
|
||||||
// )
|
// )
|
||||||
type MediaAssetType string
|
type MediaAssetType string
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ httpClient.interceptors.request.use(
|
|||||||
// Response interceptor
|
// Response interceptor
|
||||||
httpClient.interceptors.response.use(
|
httpClient.interceptors.response.use(
|
||||||
response => {
|
response => {
|
||||||
return response.data;
|
return response
|
||||||
},
|
},
|
||||||
error => {
|
error => {
|
||||||
// Handle HTTP errors here
|
// Handle HTTP errors here
|
||||||
|
|||||||
@@ -13,4 +13,14 @@ export const postService = {
|
|||||||
getPost(id) {
|
getPost(id) {
|
||||||
return httpClient.get(`/admin/posts/${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>
|
<script setup>
|
||||||
import { mediaService } from '@/api/mediaService';
|
import { mediaService } from "@/api/mediaService";
|
||||||
import { InputText } from 'primevue';
|
import { InputText } from "primevue";
|
||||||
import Badge from 'primevue/badge';
|
import Badge from "primevue/badge";
|
||||||
import Button from 'primevue/button';
|
import Button from "primevue/button";
|
||||||
import Column from 'primevue/column';
|
import Column from "primevue/column";
|
||||||
import ConfirmDialog from 'primevue/confirmdialog';
|
import ConfirmDialog from "primevue/confirmdialog";
|
||||||
import DataTable from 'primevue/datatable';
|
import DataTable from "primevue/datatable";
|
||||||
import Dropdown from 'primevue/dropdown';
|
import Dropdown from "primevue/dropdown";
|
||||||
import ProgressSpinner from 'primevue/progressspinner';
|
import ProgressSpinner from "primevue/progressspinner";
|
||||||
import Toast from 'primevue/toast';
|
import Toast from "primevue/toast";
|
||||||
import { useConfirm } from 'primevue/useconfirm';
|
import { useConfirm } from "primevue/useconfirm";
|
||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from "primevue/usetoast";
|
||||||
import { computed, onMounted, ref } from 'vue';
|
import { computed, onMounted, ref } from "vue";
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
// Import useConfirm dynamically to avoid the error when the service is not provided
|
// Import useConfirm dynamically to avoid the error when the service is not provided
|
||||||
const confirm = useConfirm();
|
const confirm = useConfirm();
|
||||||
@@ -22,24 +22,24 @@ const router = useRouter();
|
|||||||
|
|
||||||
// Remove upload related refs and methods
|
// Remove upload related refs and methods
|
||||||
const openUploadDialog = () => {
|
const openUploadDialog = () => {
|
||||||
router.push('/medias/uploads');
|
router.push("/medias/uploads");
|
||||||
};
|
};
|
||||||
|
|
||||||
// Media types for filtering
|
// Media types for filtering
|
||||||
const mediaTypes = ref([
|
const mediaTypes = ref([
|
||||||
{ name: '所有媒体', value: null },
|
{ name: "所有媒体", value: null },
|
||||||
{ name: '图片', value: 'Image' },
|
{ name: "图片", value: "Image" },
|
||||||
{ name: '视频', value: 'Video' },
|
{ name: "视频", value: "Video" },
|
||||||
{ name: '文档', value: 'Document' },
|
{ name: "文档", value: "Document" },
|
||||||
{ name: '音频', value: 'Audio' },
|
{ name: "音频", value: "Audio" },
|
||||||
{ name: 'PDF', value: 'PDF' }
|
{ name: "PDF", value: "PDF" },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const selectedMediaType = ref(mediaTypes.value[0]);
|
const selectedMediaType = ref(mediaTypes.value[0]);
|
||||||
const globalFilterValue = ref('');
|
const globalFilterValue = ref("");
|
||||||
const loading = ref(false);
|
const loading = ref(false);
|
||||||
const filters = ref({
|
const filters = ref({
|
||||||
global: { value: null, matchMode: 'contains' }
|
global: { value: null, matchMode: "contains" },
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add pagination state
|
// Add pagination state
|
||||||
@@ -62,13 +62,18 @@ const fetchMediaFiles = async () => {
|
|||||||
try {
|
try {
|
||||||
const response = await mediaService.getMedias({
|
const response = await mediaService.getMedias({
|
||||||
page: currentPage.value,
|
page: currentPage.value,
|
||||||
limit: rows.value
|
limit: rows.value,
|
||||||
});
|
});
|
||||||
mediaFiles.value = response.items;
|
mediaFiles.value = response.data.items;
|
||||||
totalRecords.value = response.total;
|
totalRecords.value = response.data.total;
|
||||||
console.log(totalRecords.value);
|
console.log(totalRecords.value);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.add({ severity: 'error', summary: '错误', detail: '加载媒体文件失败', life: 3000 });
|
toast.add({
|
||||||
|
severity: "error",
|
||||||
|
summary: "错误",
|
||||||
|
detail: "加载媒体文件失败",
|
||||||
|
life: 3000,
|
||||||
|
});
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
}
|
}
|
||||||
@@ -80,7 +85,7 @@ const onPage = (event) => {
|
|||||||
rows.value = event.rows;
|
rows.value = event.rows;
|
||||||
currentPage.value = Math.floor(event.first / event.rows) + 1;
|
currentPage.value = Math.floor(event.first / event.rows) + 1;
|
||||||
|
|
||||||
console.log(event)
|
console.log(event);
|
||||||
fetchMediaFiles();
|
fetchMediaFiles();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -91,13 +96,13 @@ onMounted(() => {
|
|||||||
// File type badge severity mapping
|
// File type badge severity mapping
|
||||||
const getBadgeSeverity = (fileType) => {
|
const getBadgeSeverity = (fileType) => {
|
||||||
const map = {
|
const map = {
|
||||||
'Image': 'info',
|
Image: "info",
|
||||||
'PDF': 'danger',
|
PDF: "danger",
|
||||||
'Video': 'warning',
|
Video: "warning",
|
||||||
'Document': 'primary',
|
Document: "primary",
|
||||||
'Audio': 'success'
|
Audio: "success",
|
||||||
};
|
};
|
||||||
return map[fileType] || 'info';
|
return map[fileType] || "info";
|
||||||
};
|
};
|
||||||
|
|
||||||
// File type icon mapping
|
// File type icon mapping
|
||||||
@@ -108,9 +113,9 @@ const getFileIcon = (file) => {
|
|||||||
|
|
||||||
// Add helper function to format file size
|
// Add helper function to format file size
|
||||||
const formatFileSize = (bytes) => {
|
const formatFileSize = (bytes) => {
|
||||||
if (bytes === 0) return '0 B';
|
if (bytes === 0) return "0 B";
|
||||||
const k = BigInt(1024);
|
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
|
// Convert input to BigInt
|
||||||
const bytesValue = BigInt(bytes);
|
const bytesValue = BigInt(bytes);
|
||||||
@@ -124,7 +129,7 @@ const formatFileSize = (bytes) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convert back to number and format with commas
|
// 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>
|
</script>
|
||||||
|
|
||||||
@@ -153,9 +158,7 @@ const formatFileSize = (bytes) => {
|
|||||||
current-page-report-template="第 {first} 到 {last} 条,共 {totalRecords} 条" :lazy="true"
|
current-page-report-template="第 {first} 到 {last} 条,共 {totalRecords} 条" :lazy="true"
|
||||||
:show-current-page-report="true">
|
:show-current-page-report="true">
|
||||||
<template #paginatorLeft>
|
<template #paginatorLeft>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">每页: {{ rows }}</div>
|
||||||
每页: {{ rows }}
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
<template #paginatorRight>
|
<template #paginatorRight>
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
@@ -167,7 +170,7 @@ const formatFileSize = (bytes) => {
|
|||||||
</template>
|
</template>
|
||||||
<template #loading>
|
<template #loading>
|
||||||
<div class="flex flex-col items-center justify-center p-4">
|
<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>
|
<span class="mt-2">加载中...</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -176,7 +179,8 @@ const formatFileSize = (bytes) => {
|
|||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div v-if="data.thumbnail_url" class="flex-shrink-0 h-10 w-10 mr-3">
|
<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>
|
||||||
<div v-else
|
<div v-else
|
||||||
class="flex-shrink-0 h-10 w-10 mr-3 bg-gray-100 rounded flex items-center justify-center">
|
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>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
||||||
<Column header="" :exportable="false" style="min-width:8rem">
|
<Column header="" :exportable="false" style="min-width: 8rem">
|
||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
<div class="flex justify-center space-x-2">
|
<div class="flex justify-center space-x-2">
|
||||||
<Button icon="pi pi-eye" rounded text severity="info" @click="previewFile(data)"
|
<Button icon="pi pi-eye" rounded text severity="info" @click="previewFile(data)"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { mediaService } from '@/api/mediaService';
|
import { mediaService } from '@/api/mediaService';
|
||||||
|
import { postService } from '@/api/postService';
|
||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import { computed, reactive, ref } from 'vue';
|
import { computed, reactive, ref } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
@@ -24,16 +25,19 @@ const toast = useToast();
|
|||||||
const post = reactive({
|
const post = reactive({
|
||||||
title: '',
|
title: '',
|
||||||
price: 0,
|
price: 0,
|
||||||
|
discount: 100, // Add discount field with default value
|
||||||
introduction: '',
|
introduction: '',
|
||||||
selectedMedia: [],
|
selectedMedia: [],
|
||||||
status: 'draft' // Add status field with default value
|
medias: [],
|
||||||
|
status: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Validation state
|
// Validation state
|
||||||
const errors = reactive({
|
const errors = reactive({
|
||||||
title: '',
|
title: '',
|
||||||
introduction: '',
|
introduction: '',
|
||||||
selectedMedia: ''
|
selectedMedia: '',
|
||||||
|
discount: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
// Media selection dialog state
|
// Media selection dialog state
|
||||||
@@ -55,8 +59,8 @@ const mediaTotalPages = computed(() => {
|
|||||||
|
|
||||||
// Status options
|
// Status options
|
||||||
const statusOptions = [
|
const statusOptions = [
|
||||||
{ label: '发布', value: 'published' },
|
{ label: '发布', value: 1 },
|
||||||
{ label: '草稿', value: 'draft' }
|
{ label: '草稿', value: 0 }
|
||||||
];
|
];
|
||||||
|
|
||||||
// Open media selection dialog
|
// Open media selection dialog
|
||||||
@@ -75,8 +79,9 @@ const loadMediaItems = async () => {
|
|||||||
page: mediaCurrentPage.value,
|
page: mediaCurrentPage.value,
|
||||||
limit: mediaRows.value
|
limit: mediaRows.value
|
||||||
});
|
});
|
||||||
mediaItems.value = response.items;
|
console.log(response.data.items)
|
||||||
mediaTotalRecords.value = response.total;
|
mediaItems.value = response.data.items;
|
||||||
|
mediaTotalRecords.value = response.data.total;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.add({ severity: 'error', summary: '错误', detail: '加载媒体文件失败', life: 3000 });
|
toast.add({ severity: 'error', summary: '错误', detail: '加载媒体文件失败', life: 3000 });
|
||||||
} finally {
|
} finally {
|
||||||
@@ -118,6 +123,7 @@ const removeMedia = (media) => {
|
|||||||
const savePost = async () => {
|
const savePost = async () => {
|
||||||
// Reset errors
|
// Reset errors
|
||||||
Object.keys(errors).forEach(key => errors[key] = '');
|
Object.keys(errors).forEach(key => errors[key] = '');
|
||||||
|
console.log(post.value)
|
||||||
|
|
||||||
// Validate form
|
// Validate form
|
||||||
let valid = true;
|
let valid = true;
|
||||||
@@ -137,18 +143,26 @@ const savePost = async () => {
|
|||||||
valid = false;
|
valid = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check discount
|
||||||
|
if (post.discount < 0 || post.discount > 100) {
|
||||||
|
errors.discount = '折扣必须在0到100之间';
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
toast.add({ severity: 'error', summary: '表单错误', detail: '请检查表单中的错误并修正', life: 3000 });
|
toast.add({ severity: 'error', summary: '表单错误', detail: '请检查表单中的错误并修正', life: 3000 });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// In a real app, you would call an API to save the post
|
post.medias = post.selectedMedia.map(media => media.id)
|
||||||
// await postApi.createPost(post);
|
const resp = await postService.createPost(post);
|
||||||
|
console.log(resp)
|
||||||
// Simulate API delay
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
||||||
|
|
||||||
|
if (resp.status !== 200) {
|
||||||
|
toast.add({ severity: 'error', summary: '错误', detail: resp.message, life: 3000 });
|
||||||
|
return;
|
||||||
|
}
|
||||||
toast.add({ severity: 'success', summary: '成功', detail: '文章已成功创建', life: 3000 });
|
toast.add({ severity: 'success', summary: '成功', detail: '文章已成功创建', life: 3000 });
|
||||||
|
|
||||||
// Navigate back to the post list
|
// Navigate back to the post list
|
||||||
@@ -219,16 +233,23 @@ const formatFileSize = (bytes) => {
|
|||||||
<div class="col-span-2">
|
<div class="col-span-2">
|
||||||
<label for="title" class="block text-sm font-medium text-gray-700 mb-1">标题</label>
|
<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="输入文章标题" />
|
<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>
|
</div>
|
||||||
|
|
||||||
<!-- Price -->
|
<!-- 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>
|
<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"
|
<InputNumber id="price" v-model="post.price" mode="currency" currency="CNY" :minFractionDigits="2"
|
||||||
class="w-full" />
|
class="w-full" />
|
||||||
</div>
|
</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 -->
|
<!-- Status -->
|
||||||
<div class="col-span-2">
|
<div class="col-span-2">
|
||||||
<label class="block text-sm font-medium text-gray-700 mb-1">状态</label>
|
<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>
|
<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"
|
<Textarea id="introduction" v-model="post.introduction" rows="5" class="w-full"
|
||||||
placeholder="输入文章介绍内容" />
|
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>
|
</div>
|
||||||
|
|
||||||
<!-- Media Selection -->
|
<!-- Media Selection -->
|
||||||
@@ -259,7 +280,7 @@ const formatFileSize = (bytes) => {
|
|||||||
<i class="pi pi-image text-gray-400 text-5xl!"></i>
|
<i class="pi pi-image text-gray-400 text-5xl!"></i>
|
||||||
<p class="text-gray-500">尚未选择任何媒体文件</p>
|
<p class="text-gray-500">尚未选择任何媒体文件</p>
|
||||||
<Button label="选择媒体" icon="pi pi-plus" @click="openMediaDialog" outlined />
|
<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>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ const fetchPosts = async () => {
|
|||||||
keyword: globalFilterValue.value
|
keyword: globalFilterValue.value
|
||||||
});
|
});
|
||||||
|
|
||||||
posts.value = response.items.map(post => ({
|
posts.value = response.data.items.map(post => ({
|
||||||
...post,
|
...post,
|
||||||
status: statusMap[post.status] || '未知',
|
status: statusMap[post.status] || '未知',
|
||||||
mediaTypes: getMediaTypes(post.assets),
|
mediaTypes: getMediaTypes(post.assets),
|
||||||
@@ -140,7 +140,7 @@ const fetchPosts = async () => {
|
|||||||
viewCount: post.views,
|
viewCount: post.views,
|
||||||
likes: post.likes
|
likes: post.likes
|
||||||
}));
|
}));
|
||||||
total.value = response.total;
|
total.value = response.data.total;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Fetch error:', error); // Debug log
|
console.error('Fetch error:', error); // Debug log
|
||||||
toast.add({ severity: 'error', summary: '错误', detail: '加载文章失败', life: 3000 });
|
toast.add({ severity: 'error', summary: '错误', detail: '加载文章失败', life: 3000 });
|
||||||
|
|||||||
Reference in New Issue
Block a user