feat: 添加内容排序功能,优化内容列表查询和加载状态处理
This commit is contained in:
@@ -2,6 +2,7 @@ package v1
|
||||
|
||||
import (
|
||||
"quyun/v2/app/http/v1/dto"
|
||||
"quyun/v2/app/requests"
|
||||
"quyun/v2/app/services"
|
||||
"quyun/v2/database/models"
|
||||
|
||||
@@ -75,7 +76,7 @@ func (c *Creator) ListContents(
|
||||
ctx fiber.Ctx,
|
||||
user *models.User,
|
||||
filter *dto.CreatorContentListFilter,
|
||||
) ([]dto.CreatorContentItem, error) {
|
||||
) (*requests.Pager, error) {
|
||||
return services.Creator.ListContents(ctx, user.ID, filter)
|
||||
}
|
||||
|
||||
|
||||
@@ -93,8 +93,9 @@ type CreatorContentListFilter struct {
|
||||
Status *string `query:"status"`
|
||||
Visibility *string `query:"visibility"`
|
||||
Genre *string `query:"genre"`
|
||||
Key *string `query:"key"`
|
||||
Keyword *string `query:"keyword"`
|
||||
Key *string `query:"key"`
|
||||
Keyword *string `query:"keyword"`
|
||||
Sort *string `query:"sort"`
|
||||
}
|
||||
|
||||
type CreatorOrderListFilter struct {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"quyun/v2/app/errorx"
|
||||
creator_dto "quyun/v2/app/http/v1/dto"
|
||||
"quyun/v2/app/requests"
|
||||
"quyun/v2/database/fields"
|
||||
"quyun/v2/database/models"
|
||||
"quyun/v2/pkg/consts"
|
||||
@@ -109,7 +110,7 @@ func (s *creator) ListContents(
|
||||
ctx context.Context,
|
||||
userID int64,
|
||||
filter *creator_dto.CreatorContentListFilter,
|
||||
) ([]creator_dto.CreatorContentItem, error) {
|
||||
) (*requests.Pager, error) {
|
||||
tid, err := s.getTenantID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -143,8 +144,32 @@ func (s *creator) ListContents(
|
||||
q = q.Where(tbl.Title.Like("%" + *filter.Keyword + "%"))
|
||||
}
|
||||
|
||||
// Pagination
|
||||
filter.Pagination.Format()
|
||||
total, err := q.Count()
|
||||
if err != nil {
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
|
||||
var list []*models.Content
|
||||
err = q.Order(tbl.ID.Desc()).
|
||||
|
||||
// Sorting
|
||||
sort := "latest"
|
||||
if filter.Sort != nil && *filter.Sort != "" {
|
||||
sort = *filter.Sort
|
||||
}
|
||||
switch sort {
|
||||
case "oldest":
|
||||
q = q.Order(tbl.ID.Asc())
|
||||
case "views":
|
||||
q = q.Order(tbl.Views.Desc())
|
||||
case "likes":
|
||||
q = q.Order(tbl.Likes.Desc())
|
||||
default:
|
||||
q = q.Order(tbl.ID.Desc())
|
||||
}
|
||||
|
||||
err = q.Offset(int(filter.Pagination.Offset())).Limit(int(filter.Pagination.Limit)).
|
||||
UnderlyingDB().
|
||||
Preload("ContentAssets").
|
||||
Preload("ContentAssets.Asset").
|
||||
@@ -220,9 +245,13 @@ func (s *creator) ListContents(
|
||||
IsPurchased: false,
|
||||
})
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
return &requests.Pager{
|
||||
Pagination: filter.Pagination,
|
||||
Total: total,
|
||||
Items: data,
|
||||
}, nil
|
||||
}
|
||||
func (s *creator) CreateContent(ctx context.Context, userID int64, form *creator_dto.ContentCreateForm) error {
|
||||
tid, err := s.getTenantID(ctx, userID)
|
||||
if err != nil {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="bg-white rounded-xl shadow-sm border border-slate-100 p-4 mb-6 flex flex-wrap gap-4 items-center">
|
||||
<!-- ... existing filters ... -->
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-bold text-slate-500">状态:</span>
|
||||
<select v-model="filterStatus"
|
||||
@@ -42,6 +43,19 @@
|
||||
<option v-for="k in keys" :key="k" :value="k">{{ k }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm font-bold text-slate-500">排序:</span>
|
||||
<select v-model="filterSort"
|
||||
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 v-for="opt in sortOptions" :key="opt.key" :value="opt.key">{{ opt.value }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<button @click="handleResetFilters" class="h-9 px-3 text-slate-500 hover:text-slate-700 hover:bg-slate-100 rounded transition-colors text-sm font-bold flex items-center gap-1">
|
||||
<i class="pi pi-refresh"></i> 重置
|
||||
</button>
|
||||
|
||||
<div class="ml-auto relative">
|
||||
<i class="pi pi-search absolute left-3 top-1/2 -translate-y-1/2 text-slate-400"></i>
|
||||
<input type="text" placeholder="搜索标题..." v-model="searchKeyword" @keyup.enter="handleSearch"
|
||||
@@ -50,10 +64,35 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading State -->
|
||||
<div v-if="loading" class="space-y-4">
|
||||
<div v-for="i in 3" :key="i" class="bg-white rounded-xl shadow-sm border border-slate-100 p-5 flex gap-6 animate-pulse">
|
||||
<div class="w-40 h-[90px] bg-slate-200 rounded-lg"></div>
|
||||
<div class="flex-1 space-y-3">
|
||||
<div class="h-6 bg-slate-200 rounded w-1/3"></div>
|
||||
<div class="h-4 bg-slate-200 rounded w-1/4"></div>
|
||||
<div class="h-8 bg-slate-200 rounded w-full mt-4"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
<div v-else-if="!loading && contents.length === 0" class="flex flex-col items-center justify-center py-20 bg-white rounded-xl border border-slate-100 border-dashed">
|
||||
<div class="w-16 h-16 bg-slate-50 rounded-full flex items-center justify-center mb-4">
|
||||
<i class="pi pi-folder-open text-3xl text-slate-300"></i>
|
||||
</div>
|
||||
<h3 class="text-slate-900 font-bold mb-1">暂无内容</h3>
|
||||
<p class="text-slate-500 text-sm mb-6">您还没有发布任何内容,快去创作吧!</p>
|
||||
<router-link to="/creator/contents/new" class="px-5 py-2 bg-primary-600 text-white rounded-lg text-sm font-bold hover:bg-primary-700 transition-colors">
|
||||
立即发布
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<!-- Content List -->
|
||||
<div class="space-y-4">
|
||||
<div v-else class="space-y-4">
|
||||
<div v-for="item in contents" :key="item.id"
|
||||
class="bg-white rounded-xl shadow-sm border border-slate-100 p-5 flex gap-6 hover:shadow-md transition-shadow group relative">
|
||||
<!-- ... existing list item ... -->
|
||||
|
||||
<!-- Cover -->
|
||||
<div class="w-40 h-[90px] bg-slate-100 rounded-lg flex-shrink-0 overflow-hidden relative">
|
||||
@@ -176,6 +215,7 @@ import { useRouter } from 'vue-router';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { useConfirm } from 'primevue/useconfirm';
|
||||
import ConfirmDialog from 'primevue/confirmdialog';
|
||||
import Paginator from 'primevue/paginator';
|
||||
import { commonApi } from '../../api/common';
|
||||
import { creatorApi } from '../../api/creator';
|
||||
|
||||
@@ -183,10 +223,12 @@ const router = useRouter();
|
||||
const toast = useToast();
|
||||
const confirm = useConfirm();
|
||||
const contents = ref([]);
|
||||
const loading = ref(false);
|
||||
const filterStatus = ref('all');
|
||||
const filterVisibility = ref('all');
|
||||
const filterGenre = ref('all');
|
||||
const filterKey = ref('all');
|
||||
const filterSort = ref('latest');
|
||||
const searchKeyword = ref('');
|
||||
const statusOptions = ref([]);
|
||||
const visibilityOptions = [
|
||||
@@ -194,7 +236,16 @@ const visibilityOptions = [
|
||||
{ key: 'tenant_only', value: '仅会员' },
|
||||
{ key: 'private', value: '私有' }
|
||||
];
|
||||
const sortOptions = [
|
||||
{ key: 'latest', value: '最新发布' },
|
||||
{ key: 'oldest', value: '最早发布' },
|
||||
{ key: 'views', value: '最多浏览' },
|
||||
{ key: 'likes', value: '最多点赞' }
|
||||
];
|
||||
const genreOptions = ref([]);
|
||||
const totalRecords = ref(0);
|
||||
const rows = ref(10);
|
||||
const first = ref(0);
|
||||
const keys = ['C大调', 'D大调', 'E大调', 'F大调', 'G大调', 'A大调', 'B大调', '降E大调'];
|
||||
|
||||
const fetchOptions = async () => {
|
||||
@@ -210,30 +261,59 @@ const fetchOptions = async () => {
|
||||
};
|
||||
|
||||
const fetchContents = async () => {
|
||||
loading.value = true;
|
||||
try {
|
||||
const params = {};
|
||||
const params = {
|
||||
page: (first.value / rows.value) + 1,
|
||||
limit: rows.value
|
||||
};
|
||||
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 (filterSort.value !== 'latest') params.sort = filterSort.value;
|
||||
if (searchKeyword.value) params.keyword = searchKeyword.value;
|
||||
|
||||
const res = await creatorApi.listContents(params);
|
||||
contents.value = res || [];
|
||||
if (res && res.items) {
|
||||
contents.value = res.items;
|
||||
totalRecords.value = res.total;
|
||||
} else {
|
||||
contents.value = [];
|
||||
totalRecords.value = 0;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const onPage = (event) => {
|
||||
first.value = event.first;
|
||||
rows.value = event.rows;
|
||||
fetchContents();
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
fetchOptions();
|
||||
fetchContents();
|
||||
});
|
||||
|
||||
watch([filterStatus, filterVisibility, filterGenre, filterKey], () => {
|
||||
watch([filterStatus, filterVisibility, filterGenre, filterKey, filterSort], () => {
|
||||
fetchContents();
|
||||
});
|
||||
|
||||
const handleResetFilters = () => {
|
||||
filterStatus.value = 'all';
|
||||
filterVisibility.value = 'all';
|
||||
filterGenre.value = 'all';
|
||||
filterKey.value = 'all';
|
||||
filterSort.value = 'latest';
|
||||
searchKeyword.value = '';
|
||||
fetchContents();
|
||||
};
|
||||
|
||||
const handleSearch = () => {
|
||||
fetchContents();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user