feat: 更新内容管理功能,支持价格为可选字段,添加状态管理及媒体计数显示
This commit is contained in:
@@ -38,7 +38,8 @@ type ContentUpdateForm struct {
|
|||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Genre string `json:"genre"`
|
Genre string `json:"genre"`
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
Price float64 `json:"price"`
|
Price *float64 `json:"price"`
|
||||||
|
Status string `json:"status"`
|
||||||
CoverIDs []string `json:"cover_ids"`
|
CoverIDs []string `json:"cover_ids"`
|
||||||
MediaIDs []string `json:"media_ids"`
|
MediaIDs []string `json:"media_ids"`
|
||||||
}
|
}
|
||||||
@@ -64,6 +65,11 @@ type CreatorContentItem struct {
|
|||||||
Price float64 `json:"price"`
|
Price float64 `json:"price"`
|
||||||
Views int `json:"views"`
|
Views int `json:"views"`
|
||||||
Likes int `json:"likes"`
|
Likes int `json:"likes"`
|
||||||
|
Cover string `json:"cover"`
|
||||||
|
ImageCount int `json:"image_count"`
|
||||||
|
VideoCount int `json:"video_count"`
|
||||||
|
AudioCount int `json:"audio_count"`
|
||||||
|
Status string `json:"status"`
|
||||||
IsPurchased bool `json:"is_purchased"`
|
IsPurchased bool `json:"is_purchased"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -117,7 +117,12 @@ func (s *creator) ListContents(
|
|||||||
q = q.Where(tbl.Title.Like("%" + *filter.Keyword + "%"))
|
q = q.Where(tbl.Title.Like("%" + *filter.Keyword + "%"))
|
||||||
}
|
}
|
||||||
|
|
||||||
list, err := q.Order(tbl.CreatedAt.Desc()).Find()
|
var list []*models.Content
|
||||||
|
err = q.Order(tbl.CreatedAt.Desc()).
|
||||||
|
UnderlyingDB().
|
||||||
|
Preload("ContentAssets").
|
||||||
|
Preload("ContentAssets.Asset").
|
||||||
|
Find(&list).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||||
}
|
}
|
||||||
@@ -137,6 +142,38 @@ func (s *creator) ListContents(
|
|||||||
|
|
||||||
var data []creator_dto.CreatorContentItem
|
var data []creator_dto.CreatorContentItem
|
||||||
for _, item := range list {
|
for _, item := range list {
|
||||||
|
var imageCount, videoCount, audioCount int
|
||||||
|
var cover string
|
||||||
|
var firstImage string
|
||||||
|
|
||||||
|
for _, ca := range item.ContentAssets {
|
||||||
|
if ca.Asset == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count logic
|
||||||
|
switch ca.Asset.Type {
|
||||||
|
case consts.MediaAssetTypeImage:
|
||||||
|
imageCount++
|
||||||
|
if firstImage == "" {
|
||||||
|
firstImage = Common.GetAssetURL(ca.Asset.ObjectKey)
|
||||||
|
}
|
||||||
|
case consts.MediaAssetTypeVideo:
|
||||||
|
videoCount++
|
||||||
|
case consts.MediaAssetTypeAudio:
|
||||||
|
audioCount++
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cover logic
|
||||||
|
if ca.Role == consts.ContentAssetRoleCover && cover == "" {
|
||||||
|
cover = Common.GetAssetURL(ca.Asset.ObjectKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if cover == "" {
|
||||||
|
cover = firstImage
|
||||||
|
}
|
||||||
|
|
||||||
data = append(data, creator_dto.CreatorContentItem{
|
data = append(data, creator_dto.CreatorContentItem{
|
||||||
ID: cast.ToString(item.ID),
|
ID: cast.ToString(item.ID),
|
||||||
Title: item.Title,
|
Title: item.Title,
|
||||||
@@ -145,6 +182,11 @@ func (s *creator) ListContents(
|
|||||||
Price: priceMap[item.ID],
|
Price: priceMap[item.ID],
|
||||||
Views: int(item.Views),
|
Views: int(item.Views),
|
||||||
Likes: int(item.Likes),
|
Likes: int(item.Likes),
|
||||||
|
Cover: cover,
|
||||||
|
ImageCount: imageCount,
|
||||||
|
VideoCount: videoCount,
|
||||||
|
AudioCount: audioCount,
|
||||||
|
Status: string(item.Status),
|
||||||
IsPurchased: false,
|
IsPurchased: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -239,34 +281,40 @@ func (s *creator) UpdateContent(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. Update Content
|
// 2. Update Content
|
||||||
_, err = tx.Content.WithContext(ctx).Where(tx.Content.ID.Eq(cid)).Updates(&models.Content{
|
contentUpdates := &models.Content{
|
||||||
Title: form.Title,
|
Title: form.Title,
|
||||||
Genre: form.Genre,
|
Genre: form.Genre,
|
||||||
Key: form.Key,
|
Key: form.Key,
|
||||||
})
|
}
|
||||||
|
if form.Status != "" {
|
||||||
|
contentUpdates.Status = consts.ContentStatus(form.Status)
|
||||||
|
}
|
||||||
|
_, err = tx.Content.WithContext(ctx).Where(tx.Content.ID.Eq(cid)).Updates(contentUpdates)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Update Price
|
// 3. Update Price
|
||||||
// Check if price exists
|
// Check if price exists
|
||||||
count, _ := tx.ContentPrice.WithContext(ctx).Where(tx.ContentPrice.ContentID.Eq(cid)).Count()
|
if form.Price != nil {
|
||||||
newPrice := int64(form.Price * 100)
|
count, _ := tx.ContentPrice.WithContext(ctx).Where(tx.ContentPrice.ContentID.Eq(cid)).Count()
|
||||||
if count > 0 {
|
newPrice := int64(*form.Price * 100)
|
||||||
_, err = tx.ContentPrice.WithContext(ctx).
|
if count > 0 {
|
||||||
Where(tx.ContentPrice.ContentID.Eq(cid)).
|
_, err = tx.ContentPrice.WithContext(ctx).
|
||||||
UpdateSimple(tx.ContentPrice.PriceAmount.Value(newPrice))
|
Where(tx.ContentPrice.ContentID.Eq(cid)).
|
||||||
} else {
|
UpdateSimple(tx.ContentPrice.PriceAmount.Value(newPrice))
|
||||||
err = tx.ContentPrice.WithContext(ctx).Create(&models.ContentPrice{
|
} else {
|
||||||
TenantID: tid,
|
err = tx.ContentPrice.WithContext(ctx).Create(&models.ContentPrice{
|
||||||
UserID: c.UserID,
|
TenantID: tid,
|
||||||
ContentID: cid,
|
UserID: c.UserID,
|
||||||
PriceAmount: newPrice,
|
ContentID: cid,
|
||||||
Currency: consts.CurrencyCNY,
|
PriceAmount: newPrice,
|
||||||
})
|
Currency: consts.CurrencyCNY,
|
||||||
}
|
})
|
||||||
if err != nil {
|
}
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Update Assets (Full replacement strategy)
|
// 4. Update Assets (Full replacement strategy)
|
||||||
|
|||||||
@@ -77,10 +77,21 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex items-center gap-6 text-sm text-slate-500">
|
<div class="flex items-center gap-6 text-sm text-slate-500">
|
||||||
<span v-if="item.price > 0" class="text-red-600 font-bold">¥ {{ item.price }}</span>
|
<span v-if="item.price > 0" class="text-red-600 font-bold">¥ {{ item.price.toFixed(2) }}</span>
|
||||||
<span v-else class="text-green-600 font-bold">免费</span>
|
<span v-else class="text-green-600 font-bold">免费</span>
|
||||||
<span><i class="pi pi-eye mr-1"></i> {{ item.views }}</span>
|
|
||||||
<span><i class="pi pi-thumbs-up mr-1"></i> {{ item.likes }}</span>
|
<span class="flex items-center gap-1" title="图片" v-if="item.image_count > 0">
|
||||||
|
<i class="pi pi-image"></i> {{ item.image_count }}
|
||||||
|
</span>
|
||||||
|
<span class="flex items-center gap-1" title="视频" v-if="item.video_count > 0">
|
||||||
|
<i class="pi pi-video"></i> {{ item.video_count }}
|
||||||
|
</span>
|
||||||
|
<span class="flex items-center gap-1" title="音频" v-if="item.audio_count > 0">
|
||||||
|
<i class="pi pi-microphone"></i> {{ item.audio_count }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span title="浏览量"><i class="pi pi-eye mr-1"></i> {{ item.views }}</span>
|
||||||
|
<span title="点赞数"><i class="pi pi-thumbs-up mr-1"></i> {{ item.likes }}</span>
|
||||||
<!-- Date field missing in DTO, using hardcoded or omitting -->
|
<!-- Date field missing in DTO, using hardcoded or omitting -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -91,10 +102,12 @@
|
|||||||
@click="$router.push(`/creator/contents/${item.id}`)"><i class="pi pi-file-edit mr-1"></i>
|
@click="$router.push(`/creator/contents/${item.id}`)"><i class="pi pi-file-edit mr-1"></i>
|
||||||
编辑</button>
|
编辑</button>
|
||||||
<button v-if="item.status === 'published'"
|
<button v-if="item.status === 'published'"
|
||||||
class="text-sm text-slate-500 hover:text-orange-600 font-medium cursor-pointer"><i
|
class="text-sm text-slate-500 hover:text-orange-600 font-medium cursor-pointer"
|
||||||
|
@click="handleStatusChange(item.id, 'unpublished')"><i
|
||||||
class="pi pi-arrow-down mr-1"></i> 下架</button>
|
class="pi pi-arrow-down mr-1"></i> 下架</button>
|
||||||
<button v-if="item.status === 'unpublished'"
|
<button v-if="item.status === 'unpublished'"
|
||||||
class="text-sm text-slate-500 hover:text-green-600 font-medium cursor-pointer"><i
|
class="text-sm text-slate-500 hover:text-green-600 font-medium cursor-pointer"
|
||||||
|
@click="handleStatusChange(item.id, 'published')"><i
|
||||||
class="pi pi-arrow-up mr-1"></i> 上架</button>
|
class="pi pi-arrow-up mr-1"></i> 上架</button>
|
||||||
<button class="text-sm text-slate-500 hover:text-red-600 font-medium ml-auto cursor-pointer"
|
<button class="text-sm text-slate-500 hover:text-red-600 font-medium ml-auto cursor-pointer"
|
||||||
@click="handleDelete(item.id)"><i class="pi pi-trash mr-1"></i> 删除</button>
|
@click="handleDelete(item.id)"><i class="pi pi-trash mr-1"></i> 删除</button>
|
||||||
@@ -108,10 +121,12 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, ref, watch } from 'vue';
|
import { onMounted, ref, watch } from 'vue';
|
||||||
import { useRouter } from 'vue-router';
|
import { useRouter } from 'vue-router';
|
||||||
|
import { useToast } from 'primevue/usetoast';
|
||||||
import { commonApi } from '../../api/common';
|
import { commonApi } from '../../api/common';
|
||||||
import { creatorApi } from '../../api/creator';
|
import { creatorApi } from '../../api/creator';
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const toast = useToast();
|
||||||
const contents = ref([]);
|
const contents = ref([]);
|
||||||
const filterStatus = ref('all');
|
const filterStatus = ref('all');
|
||||||
const filterGenre = ref('all');
|
const filterGenre = ref('all');
|
||||||
@@ -176,6 +191,17 @@ const statusStyle = (status) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleStatusChange = async (id, status) => {
|
||||||
|
try {
|
||||||
|
await creatorApi.updateContent(id, { status });
|
||||||
|
toast.add({ severity: 'success', summary: '更新成功', life: 2000 });
|
||||||
|
fetchContents();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
toast.add({ severity: 'error', summary: '更新失败', detail: e.message, life: 3000 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleDelete = async (id) => {
|
const handleDelete = async (id) => {
|
||||||
if (!confirm('确定要删除吗?')) return;
|
if (!confirm('确定要删除吗?')) return;
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user