Compare commits
2 Commits
454f6809b0
...
31d6192816
| Author | SHA1 | Date | |
|---|---|---|---|
| 31d6192816 | |||
| 1d53d9560e |
@@ -72,6 +72,8 @@ type CreatorContentItem struct {
|
||||
VideoCount int `json:"video_count"`
|
||||
AudioCount int `json:"audio_count"`
|
||||
Status string `json:"status"`
|
||||
Visibility string `json:"visibility"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
IsPinned bool `json:"is_pinned"`
|
||||
IsPurchased bool `json:"is_purchased"`
|
||||
}
|
||||
@@ -88,8 +90,9 @@ type AssetDTO struct {
|
||||
|
||||
type CreatorContentListFilter struct {
|
||||
requests.Pagination
|
||||
Status *string `query:"status"`
|
||||
Genre *string `query:"genre"`
|
||||
Status *string `query:"status"`
|
||||
Visibility *string `query:"visibility"`
|
||||
Genre *string `query:"genre"`
|
||||
Key *string `query:"key"`
|
||||
Keyword *string `query:"keyword"`
|
||||
}
|
||||
|
||||
@@ -71,10 +71,24 @@ func (s *content) List(ctx context.Context, filter *content_dto.ContentListFilte
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
|
||||
// Fetch Prices
|
||||
priceMap := make(map[int64]float64)
|
||||
if len(list) > 0 {
|
||||
ids := make([]int64, len(list))
|
||||
for i, item := range list {
|
||||
ids[i] = item.ID
|
||||
}
|
||||
pTbl, pQ := models.ContentPriceQuery.QueryContext(ctx)
|
||||
prices, _ := pQ.Where(pTbl.ContentID.In(ids...)).Find()
|
||||
for _, p := range prices {
|
||||
priceMap[p.ContentID] = float64(p.PriceAmount) / 100.0
|
||||
}
|
||||
}
|
||||
|
||||
// Convert to DTO
|
||||
data := make([]content_dto.ContentItem, len(list))
|
||||
for i, item := range list {
|
||||
data[i] = s.toContentItemDTO(item)
|
||||
data[i] = s.toContentItemDTO(item, priceMap[item.ID])
|
||||
}
|
||||
|
||||
return &requests.Pager{
|
||||
@@ -110,6 +124,13 @@ func (s *content) Get(ctx context.Context, userID int64, id string) (*content_dt
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
|
||||
// Fetch Price
|
||||
var price float64
|
||||
cp, err := models.ContentPriceQuery.WithContext(ctx).Where(models.ContentPriceQuery.ContentID.Eq(cid)).First()
|
||||
if err == nil {
|
||||
price = float64(cp.PriceAmount) / 100.0
|
||||
}
|
||||
|
||||
// Interaction & Access status
|
||||
isLiked := false
|
||||
isFavorited := false
|
||||
@@ -159,7 +180,7 @@ func (s *content) Get(ctx context.Context, userID int64, id string) (*content_dt
|
||||
}
|
||||
|
||||
detail := &content_dto.ContentDetail{
|
||||
ContentItem: s.toContentItemDTO(&item),
|
||||
ContentItem: s.toContentItemDTO(&item, price),
|
||||
Description: item.Description,
|
||||
Body: item.Body,
|
||||
MediaUrls: s.toMediaURLs(accessibleAssets),
|
||||
@@ -335,7 +356,7 @@ func (s *content) GetLibrary(ctx context.Context, userID int64) ([]user_dto.Cont
|
||||
|
||||
var data []user_dto.ContentItem
|
||||
for _, item := range list {
|
||||
dto := s.toContentItemDTO(item)
|
||||
dto := s.toContentItemDTO(item, 0)
|
||||
dto.IsPurchased = true
|
||||
data = append(data, dto)
|
||||
}
|
||||
@@ -420,7 +441,7 @@ func (s *content) ListTopics(ctx context.Context) ([]content_dto.Topic, error) {
|
||||
|
||||
// Helpers
|
||||
|
||||
func (s *content) toContentItemDTO(item *models.Content) content_dto.ContentItem {
|
||||
func (s *content) toContentItemDTO(item *models.Content, price float64) content_dto.ContentItem {
|
||||
dto := content_dto.ContentItem{
|
||||
ID: cast.ToString(item.ID),
|
||||
Title: item.Title,
|
||||
@@ -428,6 +449,7 @@ func (s *content) toContentItemDTO(item *models.Content) content_dto.ContentItem
|
||||
AuthorID: cast.ToString(item.UserID),
|
||||
Views: int(item.Views),
|
||||
Likes: int(item.Likes),
|
||||
Price: price,
|
||||
}
|
||||
if item.Author != nil {
|
||||
dto.AuthorName = item.Author.Nickname
|
||||
@@ -600,7 +622,7 @@ func (s *content) getInteractList(ctx context.Context, userID int64, typ string)
|
||||
|
||||
var data []user_dto.ContentItem
|
||||
for _, item := range list {
|
||||
data = append(data, s.toContentItemDTO(item))
|
||||
data = append(data, s.toContentItemDTO(item, 0))
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
@@ -121,6 +121,12 @@ func (s *creator) ListContents(
|
||||
if filter.Status != nil && *filter.Status != "" {
|
||||
q = q.Where(tbl.Status.Eq(consts.ContentStatus(*filter.Status)))
|
||||
}
|
||||
if filter.Visibility != nil && *filter.Visibility != "" {
|
||||
q = q.Where(tbl.Visibility.Eq(consts.ContentVisibility(*filter.Visibility)))
|
||||
}
|
||||
if filter.Visibility != nil && *filter.Visibility != "" {
|
||||
q = q.Where(tbl.Visibility.Eq(consts.ContentVisibility(*filter.Visibility)))
|
||||
}
|
||||
if filter.Genre != nil && *filter.Genre != "" {
|
||||
val := *filter.Genre
|
||||
if cn, ok := genreMap[val]; ok {
|
||||
@@ -138,7 +144,7 @@ func (s *creator) ListContents(
|
||||
}
|
||||
|
||||
var list []*models.Content
|
||||
err = q.Order(tbl.CreatedAt.Desc()).
|
||||
err = q.Order(tbl.ID.Desc()).
|
||||
UnderlyingDB().
|
||||
Preload("ContentAssets").
|
||||
Preload("ContentAssets.Asset").
|
||||
@@ -148,13 +154,14 @@ func (s *creator) ListContents(
|
||||
}
|
||||
|
||||
// Fetch Prices
|
||||
ids := make([]int64, len(list))
|
||||
for i, item := range list {
|
||||
ids[i] = item.ID
|
||||
}
|
||||
priceMap := make(map[int64]float64)
|
||||
if len(ids) > 0 {
|
||||
prices, _ := models.ContentPriceQuery.WithContext(ctx).Where(models.ContentPriceQuery.ContentID.In(ids...)).Find()
|
||||
if len(list) > 0 {
|
||||
ids := make([]int64, len(list))
|
||||
for i, item := range list {
|
||||
ids[i] = item.ID
|
||||
}
|
||||
pTbl, pQ := models.ContentPriceQuery.QueryContext(ctx)
|
||||
prices, _ := pQ.Where(pTbl.ContentID.In(ids...)).Find()
|
||||
for _, p := range prices {
|
||||
priceMap[p.ContentID] = float64(p.PriceAmount) / 100.0
|
||||
}
|
||||
@@ -207,6 +214,8 @@ func (s *creator) ListContents(
|
||||
VideoCount: videoCount,
|
||||
AudioCount: audioCount,
|
||||
Status: string(item.Status),
|
||||
Visibility: string(item.Visibility),
|
||||
CreatedAt: item.CreatedAt.Format("2006-01-02 15:04"),
|
||||
IsPinned: item.IsPinned,
|
||||
IsPurchased: false,
|
||||
})
|
||||
|
||||
@@ -18,6 +18,14 @@
|
||||
<option v-for="opt in statusOptions" :key="opt.key" :value="opt.key">{{ opt.value }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-bold text-slate-500">可见性:</span>
|
||||
<select v-model="filterVisibility"
|
||||
class="h-9 px-3 rounded border border-slate-200 text-sm focus:border-primary-500 outline-none bg-white cursor-pointer min-w-[100px]">
|
||||
<option value="all">全部</option>
|
||||
<option v-for="opt in visibilityOptions" :key="opt.key" :value="opt.key">{{ opt.value }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-bold text-slate-500">曲种:</span>
|
||||
<select v-model="filterGenre"
|
||||
@@ -61,73 +69,99 @@
|
||||
<!-- Info -->
|
||||
<div class="flex-1 min-w-0 flex flex-col justify-between">
|
||||
<div>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<span v-if="item.is_pinned" class="bg-red-600 text-white text-xs px-1.5 py-0.5 rounded font-bold">置顶</span>
|
||||
<span class="text-xs px-1.5 py-0.5 border rounded text-slate-500" v-if="item.genre">[{{ getGenreLabel(item.genre) }}]</span>
|
||||
<span class="text-xs px-1.5 py-0.5 border rounded text-slate-500" v-if="item.key">[{{ item.key }}]</span>
|
||||
<h3
|
||||
class="font-bold text-slate-900 text-lg truncate hover:text-primary-600 cursor-pointer transition-colors"
|
||||
@click="$router.push(`/creator/contents/${item.id}`)">
|
||||
{{ item.title }}</h3>
|
||||
</div>
|
||||
<!-- Status Badge -->
|
||||
<div class="flex items-center gap-2">
|
||||
<span v-if="item.status === 'blocked'"
|
||||
class="text-red-500 text-xs flex items-center gap-1 cursor-help" title="已被封禁">
|
||||
<i class="pi pi-info-circle"></i> 封禁
|
||||
</span>
|
||||
<span class="px-2.5 py-1 rounded text-xs font-bold"
|
||||
:class="statusStyle(item.status).bg + ' ' + statusStyle(item.status).text">
|
||||
{{ statusStyle(item.status).label }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<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.toFixed(2) }}</span>
|
||||
<span v-else class="text-green-600 font-bold">免费</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 -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center gap-4 pt-3 border-t border-slate-50 mt-2">
|
||||
<button class="text-sm text-slate-500 hover:text-primary-600 font-medium cursor-pointer" @click="$router.push(`/creator/contents/${item.id}`)"><i
|
||||
class="pi pi-file-edit mr-1"></i> 编辑</button>
|
||||
<button v-if="item.status === 'published'"
|
||||
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>
|
||||
<button v-if="item.status === 'unpublished'"
|
||||
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>
|
||||
<template v-if="item.status === 'published'">
|
||||
<button v-if="!item.is_pinned"
|
||||
class="text-sm text-slate-500 hover:text-blue-600 font-medium cursor-pointer"
|
||||
@click="handlePin(item.id, true)"><i
|
||||
class="pi pi-bookmark mr-1"></i> 置顶</button>
|
||||
<button v-else
|
||||
class="text-sm text-blue-600 font-medium cursor-pointer"
|
||||
@click="handlePin(item.id, false)"><i
|
||||
class="pi pi-bookmark-fill mr-1"></i> 取消置顶</button>
|
||||
</template> <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>
|
||||
</div>
|
||||
<div class="flex items-center justify-between mb-2">
|
||||
<div class="flex items-center gap-2 flex-1 min-w-0">
|
||||
<span v-if="item.is_pinned"
|
||||
class="bg-red-600 text-white text-[10px] px-1.5 py-0.5 rounded font-bold whitespace-nowrap">置顶</span>
|
||||
<span v-if="item.genre"
|
||||
class="bg-slate-100 text-slate-600 text-[11px] px-2 py-0.5 rounded-full font-bold whitespace-nowrap">{{
|
||||
getGenreLabel(item.genre) }}</span>
|
||||
<span v-if="item.key"
|
||||
class="bg-blue-50 text-blue-600 text-[11px] px-2 py-0.5 rounded-full font-bold whitespace-nowrap">{{
|
||||
item.key }}</span>
|
||||
<h3 class="font-bold text-slate-900 text-lg truncate hover:text-primary-600 cursor-pointer transition-colors"
|
||||
@click="$router.push(`/creator/contents/${item.id}`)">
|
||||
{{ item.title }}</h3>
|
||||
</div>
|
||||
<!-- Status Badge -->
|
||||
<div class="flex items-center gap-2 ml-4">
|
||||
<span class="text-[10px] px-1.5 py-0.5 rounded border border-slate-200 text-slate-500 bg-slate-50" v-if="item.visibility">
|
||||
{{ getVisibilityLabel(item.visibility) }}
|
||||
</span>
|
||||
<span v-if="item.status === 'blocked'" class="text-red-500 text-xs flex items-center gap-1 cursor-help"
|
||||
title="已被封禁">
|
||||
<i class="pi pi-info-circle"></i> 封禁
|
||||
</span>
|
||||
<span class="px-2.5 py-1 rounded text-xs font-bold whitespace-nowrap"
|
||||
:class="statusStyle(item.status).bg + ' ' + statusStyle(item.status).text">
|
||||
{{ statusStyle(item.status).label }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4 text-xs text-slate-500 mt-3">
|
||||
<span class="flex items-center gap-1" title="发布时间">
|
||||
<i class="pi pi-calendar text-[10px]"></i> {{ item.created_at }}
|
||||
</span>
|
||||
|
||||
<div class="flex items-center gap-3 border-l border-slate-200 pl-3">
|
||||
<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>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 border-l border-slate-200 pl-3">
|
||||
<span class="flex items-center gap-1" title="图片" v-if="item.image_count > 0">
|
||||
<i class="pi pi-image text-[10px]"></i> {{ item.image_count }}
|
||||
</span>
|
||||
<span class="flex items-center gap-1" title="视频" v-if="item.video_count > 0">
|
||||
<i class="pi pi-video text-[10px]"></i> {{ item.video_count }}
|
||||
</span>
|
||||
<span class="flex items-center gap-1" title="音频" v-if="item.audio_count > 0">
|
||||
<i class="pi pi-microphone text-[10px]"></i> {{ item.audio_count }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 border-l border-slate-200 pl-3">
|
||||
<span title="浏览量"><i class="pi pi-eye mr-1 text-[10px]"></i> {{ item.views }}</span>
|
||||
<span title="点赞数"><i class="pi pi-thumbs-up mr-1 text-[10px]"></i> {{ item.likes }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="flex items-center gap-4 pt-3 border-t border-slate-50 mt-3">
|
||||
<button class="text-sm text-slate-500 hover:text-primary-600 font-medium cursor-pointer flex items-center gap-1"
|
||||
@click="$router.push(`/creator/contents/${item.id}`)">
|
||||
<i class="pi pi-file-edit"></i> 编辑
|
||||
</button>
|
||||
<button v-if="item.status === 'published'"
|
||||
class="text-sm text-slate-500 hover:text-orange-600 font-medium cursor-pointer flex items-center gap-1"
|
||||
@click="handleStatusChange(item.id, 'unpublished')">
|
||||
<i class="pi pi-arrow-down"></i> 下架
|
||||
</button>
|
||||
<button v-if="item.status === 'unpublished'"
|
||||
class="text-sm text-slate-500 hover:text-green-600 font-medium cursor-pointer flex items-center gap-1"
|
||||
@click="handleStatusChange(item.id, 'published')">
|
||||
<i class="pi pi-arrow-up"></i> 上架
|
||||
</button>
|
||||
<template v-if="item.status === 'published'">
|
||||
<button v-if="!item.is_pinned"
|
||||
class="text-sm text-slate-500 hover:text-blue-600 font-medium cursor-pointer flex items-center gap-1"
|
||||
@click="handlePin(item.id, true)">
|
||||
<i class="pi pi-bookmark"></i> 置顶
|
||||
</button>
|
||||
<button v-else
|
||||
class="text-sm text-blue-600 font-medium cursor-pointer flex items-center gap-1"
|
||||
@click="handlePin(item.id, false)">
|
||||
<i class="pi pi-bookmark-fill"></i> 取消置顶
|
||||
</button>
|
||||
</template>
|
||||
<button
|
||||
class="text-sm text-slate-400 hover:text-red-600 font-medium ml-auto cursor-pointer flex items-center gap-1 transition-colors"
|
||||
@click="handleDelete(item.id)">
|
||||
<i class="pi pi-trash"></i> 删除
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -150,10 +184,16 @@ const toast = useToast();
|
||||
const confirm = useConfirm();
|
||||
const contents = ref([]);
|
||||
const filterStatus = ref('all');
|
||||
const filterVisibility = ref('all');
|
||||
const filterGenre = ref('all');
|
||||
const filterKey = ref('all');
|
||||
const searchKeyword = ref('');
|
||||
const statusOptions = ref([]);
|
||||
const visibilityOptions = [
|
||||
{ key: 'public', value: '公开' },
|
||||
{ key: 'tenant_only', value: '仅会员' },
|
||||
{ key: 'private', value: '私有' }
|
||||
];
|
||||
const genreOptions = ref([]);
|
||||
const keys = ['C大调', 'D大调', 'E大调', 'F大调', 'G大调', 'A大调', 'B大调', '降E大调'];
|
||||
|
||||
@@ -173,6 +213,7 @@ const fetchContents = async () => {
|
||||
try {
|
||||
const params = {};
|
||||
if (filterStatus.value !== 'all') params.status = filterStatus.value;
|
||||
if (filterVisibility.value !== 'all') params.visibility = filterVisibility.value;
|
||||
if (filterGenre.value !== 'all') params.genre = filterGenre.value;
|
||||
if (filterKey.value !== 'all') params.key = filterKey.value;
|
||||
if (searchKeyword.value) params.keyword = searchKeyword.value;
|
||||
@@ -189,7 +230,7 @@ onMounted(() => {
|
||||
fetchContents();
|
||||
});
|
||||
|
||||
watch([filterStatus, filterGenre, filterKey], () => {
|
||||
watch([filterStatus, filterVisibility, filterGenre, filterKey], () => {
|
||||
fetchContents();
|
||||
});
|
||||
|
||||
@@ -202,6 +243,15 @@ const getGenreLabel = (key) => {
|
||||
return opt ? opt.value : key;
|
||||
};
|
||||
|
||||
const getVisibilityLabel = (vis) => {
|
||||
const map = {
|
||||
'public': '公开',
|
||||
'tenant_only': '仅会员',
|
||||
'private': '私有'
|
||||
};
|
||||
return map[vis] || vis;
|
||||
};
|
||||
|
||||
const statusStyle = (status) => {
|
||||
// Map backend status to UI style. Labels should ideally come from backend option value/label map if needed,
|
||||
// but for style/color mapping we can keep it here or use a helper.
|
||||
|
||||
Reference in New Issue
Block a user