feat: update
This commit is contained in:
@@ -28,12 +28,13 @@ func (ctl *posts) List(ctx fiber.Ctx, pagination *requests.Pagination, query *Li
|
|||||||
|
|
||||||
type PostForm struct {
|
type PostForm struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
HeadImage int64 `json:"head_image"`
|
HeadImageIds []int64 `json:"head_image_ids"`
|
||||||
Price int64 `json:"price"`
|
Price int64 `json:"price"`
|
||||||
Discount int16 `json:"discount"`
|
Discount int16 `json:"discount"`
|
||||||
Introduction string `json:"introduction"`
|
Introduction string `json:"introduction"`
|
||||||
Medias []int64 `json:"medias"`
|
Medias []int64 `json:"medias"`
|
||||||
Status fields.PostStatus `json:"status"`
|
Status fields.PostStatus `json:"status"`
|
||||||
|
Content string `json:"content"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create
|
// Create
|
||||||
@@ -42,11 +43,12 @@ type PostForm struct {
|
|||||||
func (ctl *posts) Create(ctx fiber.Ctx, form *PostForm) error {
|
func (ctl *posts) Create(ctx fiber.Ctx, form *PostForm) error {
|
||||||
post := model.Posts{
|
post := model.Posts{
|
||||||
Title: form.Title,
|
Title: form.Title,
|
||||||
|
HeadImages: fields.ToJson(form.HeadImageIds),
|
||||||
Price: form.Price,
|
Price: form.Price,
|
||||||
Discount: form.Discount,
|
Discount: form.Discount,
|
||||||
Description: form.Introduction,
|
Description: form.Introduction,
|
||||||
Status: form.Status,
|
Status: form.Status,
|
||||||
Content: "",
|
Content: form.Content,
|
||||||
Tags: fields.Json[[]string]{},
|
Tags: fields.Json[[]string]{},
|
||||||
Assets: fields.Json[[]fields.MediaAsset]{},
|
Assets: fields.Json[[]fields.MediaAsset]{},
|
||||||
}
|
}
|
||||||
@@ -84,11 +86,12 @@ func (ctl *posts) Update(ctx fiber.Ctx, id int64, form *PostForm) error {
|
|||||||
|
|
||||||
post := &model.Posts{
|
post := &model.Posts{
|
||||||
Title: form.Title,
|
Title: form.Title,
|
||||||
|
HeadImages: fields.ToJson(form.HeadImageIds),
|
||||||
Price: form.Price,
|
Price: form.Price,
|
||||||
Discount: form.Discount,
|
Discount: form.Discount,
|
||||||
Description: form.Introduction,
|
Description: form.Introduction,
|
||||||
Status: form.Status,
|
Status: form.Status,
|
||||||
Content: "",
|
Content: form.Content,
|
||||||
Tags: fields.Json[[]string]{},
|
Tags: fields.Json[[]string]{},
|
||||||
Assets: fields.Json[[]fields.MediaAsset]{},
|
Assets: fields.Json[[]fields.MediaAsset]{},
|
||||||
CreatedAt: oldPost.CreatedAt,
|
CreatedAt: oldPost.CreatedAt,
|
||||||
|
|||||||
@@ -27,5 +27,5 @@ type Posts struct {
|
|||||||
Likes int64 `json:"likes"`
|
Likes int64 `json:"likes"`
|
||||||
Tags fields.Json[[]string] `json:"tags"`
|
Tags fields.Json[[]string] `json:"tags"`
|
||||||
Assets fields.Json[[]fields.MediaAsset] `json:"assets"`
|
Assets fields.Json[[]fields.MediaAsset] `json:"assets"`
|
||||||
HeadImages string `json:"head_images"`
|
HeadImages fields.Json[[]int64] `json:"head_images"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ types:
|
|||||||
assets: Json[[]MediaAsset]
|
assets: Json[[]MediaAsset]
|
||||||
tags: Json[[]string]
|
tags: Json[[]string]
|
||||||
meta: Json[PostMeta]
|
meta: Json[PostMeta]
|
||||||
|
head_images: Json[[]int64]
|
||||||
|
|
||||||
users:
|
users:
|
||||||
status: UserStatus
|
status: UserStatus
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { mediaService } from '@/api/mediaService';
|
import { mediaService } from '@/api/mediaService';
|
||||||
import { postService } from '@/api/postService';
|
import { postService } from '@/api/postService';
|
||||||
|
import { formatFileSize } from "@/utils/filesize";
|
||||||
|
import { getFileIcon, getFileTypeByMimeCN } from "@/utils/filetype";
|
||||||
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';
|
||||||
@@ -18,6 +20,33 @@ import RadioButton from 'primevue/radiobutton';
|
|||||||
import Textarea from 'primevue/textarea';
|
import Textarea from 'primevue/textarea';
|
||||||
import Toast from 'primevue/toast';
|
import Toast from 'primevue/toast';
|
||||||
|
|
||||||
|
import {
|
||||||
|
BsFileEarmark,
|
||||||
|
BsFileExcel,
|
||||||
|
BsFileImage,
|
||||||
|
BsFileMusic,
|
||||||
|
BsFilePdf,
|
||||||
|
BsFilePlayFill,
|
||||||
|
BsFilePpt,
|
||||||
|
BsFileText,
|
||||||
|
BsFileWord,
|
||||||
|
BsFileZip
|
||||||
|
} from 'vue-icons-plus/bs';
|
||||||
|
|
||||||
|
// Create icons object
|
||||||
|
const icons = {
|
||||||
|
BsFileEarmark,
|
||||||
|
BsFileExcel,
|
||||||
|
BsFileImage,
|
||||||
|
BsFileMusic,
|
||||||
|
BsFilePdf,
|
||||||
|
BsFilePlayFill,
|
||||||
|
BsFilePpt,
|
||||||
|
BsFileText,
|
||||||
|
BsFileWord,
|
||||||
|
BsFileZip
|
||||||
|
};
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
||||||
@@ -212,43 +241,6 @@ const cancelCreate = () => {
|
|||||||
router.push('/posts');
|
router.push('/posts');
|
||||||
};
|
};
|
||||||
|
|
||||||
// File type badge severity mapping
|
|
||||||
const getBadgeSeverity = (fileType) => {
|
|
||||||
const map = {
|
|
||||||
'image': 'info',
|
|
||||||
'pdf': 'danger',
|
|
||||||
'video': 'warning',
|
|
||||||
'document': 'primary',
|
|
||||||
'audio': 'success'
|
|
||||||
};
|
|
||||||
return map[fileType] || 'info';
|
|
||||||
};
|
|
||||||
|
|
||||||
// File type icon mapping
|
|
||||||
const getFileIcon = (file) => {
|
|
||||||
const map = {
|
|
||||||
'image': 'pi-image',
|
|
||||||
'pdf': 'pi-file-pdf',
|
|
||||||
'video': 'pi-video',
|
|
||||||
'document': 'pi-file',
|
|
||||||
'audio': 'pi-volume-up'
|
|
||||||
};
|
|
||||||
return `pi ${map[file.fileType] || 'pi-file'}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Format file size helper
|
|
||||||
const formatFileSize = (bytes) => {
|
|
||||||
if (!bytes && bytes !== 0) return '0 B';
|
|
||||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
|
||||||
const base = 1024;
|
|
||||||
let size = Number(bytes);
|
|
||||||
if (isNaN(size)) return '0 B';
|
|
||||||
|
|
||||||
const i = Math.floor(Math.log(size) / Math.log(base));
|
|
||||||
size = size / Math.pow(base, i);
|
|
||||||
return `${size.toFixed(2)} ${sizes[i]}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add head image preview loading
|
// Add head image preview loading
|
||||||
const loadHeadImagePreviews = async () => {
|
const loadHeadImagePreviews = async () => {
|
||||||
headImageUrls.value = [];
|
headImageUrls.value = [];
|
||||||
@@ -372,20 +364,16 @@ const loadHeadImagePreviews = async () => {
|
|||||||
<div class="grid grid-cols-1 md:grid-cols-1 gap-3">
|
<div class="grid grid-cols-1 md:grid-cols-1 gap-3">
|
||||||
<div v-for="media in post.selectedMedia" :key="media.id"
|
<div v-for="media in post.selectedMedia" :key="media.id"
|
||||||
class="relative border border-gray-200 rounded-md p-2 flex items-center">
|
class="relative border border-gray-200 rounded-md p-2 flex items-center">
|
||||||
<div v-if="media.thumbnailUrl" class="flex-shrink-0 h-10 w-10 mr-3">
|
<div
|
||||||
<img class="h-10 w-10 object-cover rounded" :src="media.thumbnailUrl"
|
|
||||||
:alt="media.file_name">
|
|
||||||
</div>
|
|
||||||
<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">
|
||||||
<i :class="getFileIcon(media)" class="text-2xl"></i>
|
<component :is="getFileIcon(media, icons)" class="text-xl text-gray-600" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 overflow-hidden">
|
<div class="flex-1 overflow-hidden">
|
||||||
<div class="text-sm font-medium text-gray-900 truncate">
|
<div class="text-sm font-medium text-gray-900 truncate">{{ media.name }}
|
||||||
{{ media.name }}
|
|
||||||
</div>
|
</div>
|
||||||
<Badge :value="media.file_type" :severity="getBadgeSeverity(media.file_type)"
|
|
||||||
class="text-xs" />
|
<Badge :value="getFileTypeByMimeCN(media.media_type)" />
|
||||||
</div>
|
</div>
|
||||||
<Button icon="pi pi-times" class="p-button-rounded p-button-text p-button-sm"
|
<Button icon="pi pi-times" class="p-button-rounded p-button-text p-button-sm"
|
||||||
@click="removeMedia(media)" aria-label="移除" />
|
@click="removeMedia(media)" aria-label="移除" />
|
||||||
@@ -444,7 +432,7 @@ const loadHeadImagePreviews = async () => {
|
|||||||
|
|
||||||
<Column field="fileType" header="文件类型">
|
<Column field="fileType" header="文件类型">
|
||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
<Badge :value="data.file_type" :severity="getBadgeSeverity(data.file_type)" />
|
<Badge :value="getFileTypeByMimeCN(data.media_type)" />
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { mediaService } from '@/api/mediaService';
|
import { mediaService } from '@/api/mediaService';
|
||||||
import { postService } from '@/api/postService';
|
import { postService } from '@/api/postService';
|
||||||
|
import { formatFileSize } from "@/utils/filesize";
|
||||||
|
import { getFileIcon, getFileTypeByMimeCN } from "@/utils/filetype";
|
||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import { computed, onMounted, reactive, ref } from 'vue';
|
import { computed, onMounted, reactive, ref } from 'vue';
|
||||||
import { useRoute, useRouter } from 'vue-router';
|
import { useRoute, useRouter } from 'vue-router';
|
||||||
@@ -18,11 +20,40 @@ import RadioButton from 'primevue/radiobutton';
|
|||||||
import Textarea from 'primevue/textarea';
|
import Textarea from 'primevue/textarea';
|
||||||
import Toast from 'primevue/toast';
|
import Toast from 'primevue/toast';
|
||||||
|
|
||||||
|
import {
|
||||||
|
BsFileEarmark,
|
||||||
|
BsFileExcel,
|
||||||
|
BsFileImage,
|
||||||
|
BsFileMusic,
|
||||||
|
BsFilePdf,
|
||||||
|
BsFilePlayFill,
|
||||||
|
BsFilePpt,
|
||||||
|
BsFileText,
|
||||||
|
BsFileWord,
|
||||||
|
BsFileZip
|
||||||
|
} from 'vue-icons-plus/bs';
|
||||||
|
|
||||||
|
// Create icons object
|
||||||
|
const icons = {
|
||||||
|
BsFileEarmark,
|
||||||
|
BsFileExcel,
|
||||||
|
BsFileImage,
|
||||||
|
BsFileMusic,
|
||||||
|
BsFilePdf,
|
||||||
|
BsFilePlayFill,
|
||||||
|
BsFilePpt,
|
||||||
|
BsFileText,
|
||||||
|
BsFileWord,
|
||||||
|
BsFileZip
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const loading = ref(true);
|
const loading = ref(true);
|
||||||
|
|
||||||
|
|
||||||
// Form state
|
// Form state
|
||||||
const post = reactive({
|
const post = reactive({
|
||||||
id: null,
|
id: null,
|
||||||
@@ -248,27 +279,8 @@ const cancelEdit = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// File type badge severity mapping
|
// File type badge severity mapping
|
||||||
const getBadgeSeverity = (fileType) => {
|
const getBadgeSeverity = () => {
|
||||||
const map = {
|
return 'info';
|
||||||
'image': 'info',
|
|
||||||
'pdf': 'danger',
|
|
||||||
'video': 'warning',
|
|
||||||
'document': 'primary',
|
|
||||||
'audio': 'success'
|
|
||||||
};
|
|
||||||
return map[fileType] || 'info';
|
|
||||||
};
|
|
||||||
|
|
||||||
// File type icon mapping
|
|
||||||
const getFileIcon = (file) => {
|
|
||||||
const map = {
|
|
||||||
'image': 'pi-image',
|
|
||||||
'pdf': 'pi-file-pdf',
|
|
||||||
'video': 'pi-video',
|
|
||||||
'document': 'pi-file',
|
|
||||||
'audio': 'pi-volume-up'
|
|
||||||
};
|
|
||||||
return `pi ${map[file.fileType] || 'pi-file'}`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add pagination handler
|
// Add pagination handler
|
||||||
@@ -279,19 +291,6 @@ const onMediaPage = (event) => {
|
|||||||
loadMediaItems();
|
loadMediaItems();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add format file size helper
|
|
||||||
const formatFileSize = (bytes) => {
|
|
||||||
if (!bytes && bytes !== 0) return '0 B';
|
|
||||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
|
|
||||||
const base = 1024;
|
|
||||||
let size = Number(bytes);
|
|
||||||
if (isNaN(size)) return '0 B';
|
|
||||||
|
|
||||||
const i = Math.floor(Math.log(size) / Math.log(base));
|
|
||||||
size = size / Math.pow(base, i);
|
|
||||||
return `${size.toFixed(2)} ${sizes[i]}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// Get post ID from route params
|
// Get post ID from route params
|
||||||
const postId = route.params.id;
|
const postId = route.params.id;
|
||||||
@@ -414,22 +413,19 @@ onMounted(() => {
|
|||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<Button label="更改媒体" icon="pi pi-sync" @click="openMediaDialog" outlined />
|
<Button label="更改媒体" icon="pi pi-sync" @click="openMediaDialog" outlined />
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-3">
|
<div class="grid grid-cols-1 md:grid-cols-1 gap-3">
|
||||||
<div v-for="media in post.selectedMedia" :key="media.id"
|
<div v-for="media in post.selectedMedia" :key="media.id"
|
||||||
class="relative border border-gray-200 rounded-md p-2 flex items-center">
|
class="relative border border-gray-200 rounded-md p-2 flex items-center">
|
||||||
<div v-if="media.thumbnailUrl" class="flex-shrink-0 h-10 w-10 mr-3">
|
<div
|
||||||
<img class="h-10 w-10 object-cover rounded" :src="media.thumbnailUrl"
|
|
||||||
:alt="media.name">
|
|
||||||
</div>
|
|
||||||
<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">
|
||||||
<i :class="getFileIcon(media)" class="text-2xl"></i>
|
<component :is="getFileIcon(media, icons)" class="text-xl text-gray-600" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 overflow-hidden">
|
<div class="flex-1 overflow-hidden">
|
||||||
<div class="text-sm font-medium text-gray-900 truncate">{{ media.name }}
|
<div class="text-sm font-medium text-gray-900 truncate">{{ media.name }}
|
||||||
</div>
|
</div>
|
||||||
<Badge :value="media.file_type" :severity="getBadgeSeverity(media.file_type)"
|
|
||||||
class="text-xs" />
|
<Badge :value="getFileTypeByMimeCN(media.media_type)" />
|
||||||
</div>
|
</div>
|
||||||
<Button icon="pi pi-times" class="p-button-rounded p-button-text p-button-sm"
|
<Button icon="pi pi-times" class="p-button-rounded p-button-text p-button-sm"
|
||||||
@click="removeMedia(media)" aria-label="移除" />
|
@click="removeMedia(media)" aria-label="移除" />
|
||||||
@@ -488,7 +484,7 @@ onMounted(() => {
|
|||||||
|
|
||||||
<Column field="fileType" header="文件类型">
|
<Column field="fileType" header="文件类型">
|
||||||
<template #body="{ data }">
|
<template #body="{ data }">
|
||||||
<Badge :value="data.file_type" :severity="getBadgeSeverity(data.file_type)" />
|
<Badge :value="getFileTypeByMimeCN(data.media_type)" />
|
||||||
</template>
|
</template>
|
||||||
</Column>
|
</Column>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user