feat: 添加内容置顶过滤选项,更新内容列表查询功能;添加创作者设置 ID 字段;优化租户信息获取逻辑;更新租户 API 接口
This commit is contained in:
@@ -8,6 +8,7 @@ type ContentListFilter struct {
|
|||||||
Genre *string `query:"genre"`
|
Genre *string `query:"genre"`
|
||||||
TenantID *string `query:"tenantId"`
|
TenantID *string `query:"tenantId"`
|
||||||
Sort *string `query:"sort"`
|
Sort *string `query:"sort"`
|
||||||
|
IsPinned *bool `query:"is_pinned"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContentItem struct {
|
type ContentItem struct {
|
||||||
|
|||||||
@@ -106,6 +106,7 @@ type RefundForm struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Settings struct {
|
type Settings struct {
|
||||||
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Bio string `json:"bio"`
|
Bio string `json:"bio"`
|
||||||
Avatar string `json:"avatar"`
|
Avatar string `json:"avatar"`
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ func (s *content) List(ctx context.Context, filter *content_dto.ContentListFilte
|
|||||||
tid := cast.ToInt64(*filter.TenantID)
|
tid := cast.ToInt64(*filter.TenantID)
|
||||||
q = q.Where(tbl.TenantID.Eq(tid))
|
q = q.Where(tbl.TenantID.Eq(tid))
|
||||||
}
|
}
|
||||||
|
if filter.IsPinned != nil {
|
||||||
|
q = q.Where(tbl.IsPinned.Is(*filter.IsPinned))
|
||||||
|
}
|
||||||
|
|
||||||
// Sort
|
// Sort
|
||||||
sort := "latest"
|
sort := "latest"
|
||||||
|
|||||||
@@ -663,6 +663,7 @@ func (s *creator) GetSettings(ctx context.Context, userID int64) (*creator_dto.S
|
|||||||
}
|
}
|
||||||
cfg := t.Config.Data()
|
cfg := t.Config.Data()
|
||||||
return &creator_dto.Settings{
|
return &creator_dto.Settings{
|
||||||
|
ID: cast.ToString(t.ID),
|
||||||
Name: t.Name,
|
Name: t.Name,
|
||||||
Bio: cfg.Bio,
|
Bio: cfg.Bio,
|
||||||
Avatar: cfg.Avatar,
|
Avatar: cfg.Avatar,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package services
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"quyun/v2/app/errorx"
|
"quyun/v2/app/errorx"
|
||||||
"quyun/v2/app/http/v1/dto"
|
"quyun/v2/app/http/v1/dto"
|
||||||
@@ -42,10 +43,15 @@ func (s *tenant) GetPublicProfile(ctx context.Context, userID int64, id string)
|
|||||||
Exists()
|
Exists()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cfg := t.Config.Data()
|
||||||
|
fmt.Printf("DEBUG: Tenant Config: %+v\n", cfg)
|
||||||
return &dto.TenantProfile{
|
return &dto.TenantProfile{
|
||||||
ID: cast.ToString(t.ID),
|
ID: cast.ToString(t.ID),
|
||||||
Name: t.Name,
|
Name: t.Name,
|
||||||
Avatar: "", // Extract from config if available
|
Avatar: cfg.Avatar,
|
||||||
|
Cover: cfg.Cover,
|
||||||
|
Bio: cfg.Bio,
|
||||||
|
Description: cfg.Description,
|
||||||
Stats: dto.Stats{
|
Stats: dto.Stats{
|
||||||
Followers: int(followers),
|
Followers: int(followers),
|
||||||
Contents: int(contents),
|
Contents: int(contents),
|
||||||
|
|||||||
7
frontend/portal/src/api/tenant.js
Normal file
7
frontend/portal/src/api/tenant.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { request } from '../utils/request';
|
||||||
|
|
||||||
|
export const tenantApi = {
|
||||||
|
get: (id) => request(`/tenants/${id}`),
|
||||||
|
follow: (id) => request(`/tenants/${id}/follow`, { method: 'POST' }),
|
||||||
|
unfollow: (id) => request(`/tenants/${id}/follow`, { method: 'DELETE' }),
|
||||||
|
};
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
|
|
||||||
<!-- Footer Link -->
|
<!-- Footer Link -->
|
||||||
<div class="p-4 border-t border-slate-800">
|
<div class="p-4 border-t border-slate-800">
|
||||||
<router-link to="/t/1"
|
<router-link :to="'/t/' + (tenantId || '1')"
|
||||||
class="flex items-center gap-2 px-4 py-2 text-sm text-slate-400 hover:text-white transition-colors">
|
class="flex items-center gap-2 px-4 py-2 text-sm text-slate-400 hover:text-white transition-colors">
|
||||||
<i class="pi pi-external-link"></i> 预览我的主页
|
<i class="pi pi-external-link"></i> 预览我的主页
|
||||||
</router-link>
|
</router-link>
|
||||||
@@ -76,13 +76,27 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue';
|
import { computed, ref, onMounted } from 'vue';
|
||||||
import { useRoute } from 'vue-router';
|
import { useRoute } from 'vue-router';
|
||||||
import AppFooter from '../components/AppFooter.vue';
|
import AppFooter from '../components/AppFooter.vue';
|
||||||
import TopNavbar from '../components/TopNavbar.vue';
|
import TopNavbar from '../components/TopNavbar.vue';
|
||||||
|
import { creatorApi } from '../api/creator';
|
||||||
|
|
||||||
const route = useRoute();
|
const route = useRoute();
|
||||||
|
const tenantId = ref('');
|
||||||
|
|
||||||
const isFullWidth = computed(() => {
|
const isFullWidth = computed(() => {
|
||||||
return ['creator-content-new', 'creator-content-edit'].includes(route.name);
|
return ['creator-content-new', 'creator-content-edit'].includes(route.name);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
try {
|
||||||
|
const res = await creatorApi.getSettings();
|
||||||
|
if (res && res.id) {
|
||||||
|
tenantId.value = res.id;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
<!-- Cover & Header Info Merged -->
|
<!-- Cover & Header Info Merged -->
|
||||||
<div class="relative h-[400px] bg-slate-900 overflow-hidden group">
|
<div class="relative h-[400px] bg-slate-900 overflow-hidden group">
|
||||||
<!-- Background Image -->
|
<!-- Background Image -->
|
||||||
<img :src="tenant.cover"
|
<img :src="tenant.cover || 'https://images.unsplash.com/photo-1611454453122-7b02c6b58188?ixlib=rb-1.2.1&auto=format&fit=crop&w=1950&q=80'"
|
||||||
class="w-full h-full object-cover opacity-90 transition-transform duration-700 group-hover:scale-105">
|
class="w-full h-full object-cover opacity-90 transition-transform duration-700 group-hover:scale-105">
|
||||||
<!-- Gradient Overlay -->
|
<!-- Gradient Overlay -->
|
||||||
<div class="absolute inset-0 bg-gradient-to-t from-black/90 via-black/40 to-transparent"></div>
|
<div class="absolute inset-0 bg-gradient-to-t from-black/90 via-black/40 to-transparent"></div>
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
<!-- Avatar -->
|
<!-- Avatar -->
|
||||||
<div
|
<div
|
||||||
class="w-32 h-32 rounded-full border-4 border-white/20 shadow-2xl overflow-hidden flex-shrink-0 backdrop-blur-sm">
|
class="w-32 h-32 rounded-full border-4 border-white/20 shadow-2xl overflow-hidden flex-shrink-0 backdrop-blur-sm">
|
||||||
<img :src="tenant.avatar" class="w-full h-full object-cover">
|
<img :src="tenant.avatar || 'https://api.dicebear.com/7.x/avataaars/svg?seed=' + (tenant.name || 'User')" class="w-full h-full object-cover">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Info -->
|
<!-- Info -->
|
||||||
@@ -46,10 +46,10 @@
|
|||||||
class="h-11 w-11 border border-white/20 text-white rounded-full flex items-center justify-center hover:bg-white/10 backdrop-blur-md transition-colors"><i
|
class="h-11 w-11 border border-white/20 text-white rounded-full flex items-center justify-center hover:bg-white/10 backdrop-blur-md transition-colors"><i
|
||||||
class="pi pi-ellipsis-h"></i></button>
|
class="pi pi-ellipsis-h"></i></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-6 text-sm text-slate-300 font-medium">
|
<div class="flex gap-6 text-sm text-slate-300 font-medium" v-if="tenant.stats">
|
||||||
<div><span class="font-bold text-white text-xl">1.2万</span> 关注</div>
|
<div><span class="font-bold text-white text-xl">{{ tenant.stats.followers }}</span> 关注</div>
|
||||||
<div><span class="font-bold text-white text-xl">458</span> 内容</div>
|
<div><span class="font-bold text-white text-xl">{{ tenant.stats.contents }}</span> 内容</div>
|
||||||
<div><span class="font-bold text-white text-xl">8.9k</span> 获赞</div>
|
<div><span class="font-bold text-white text-xl">{{ tenant.stats.likes }}</span> 获赞</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -83,19 +83,20 @@
|
|||||||
<!-- 1. Home Tab -->
|
<!-- 1. Home Tab -->
|
||||||
<div v-if="currentTab === 'home'" class="space-y-10">
|
<div v-if="currentTab === 'home'" class="space-y-10">
|
||||||
<!-- Featured (Pinned) -->
|
<!-- Featured (Pinned) -->
|
||||||
<div class="relative h-[400px] rounded-2xl overflow-hidden group cursor-pointer">
|
<div class="relative h-[400px] rounded-2xl overflow-hidden group cursor-pointer"
|
||||||
<img
|
v-if="featuredContent"
|
||||||
src="https://images.unsplash.com/photo-1516450360452-9312f5e86fc7?ixlib=rb-1.2.1&auto=format&fit=crop&w=1200&q=80"
|
@click="$router.push(`/contents/${featuredContent.id}`)">
|
||||||
class="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105">
|
<img :src="featuredContent.cover" class="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105">
|
||||||
<div class="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent"></div>
|
<div class="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent"></div>
|
||||||
<div class="absolute top-4 left-4 px-2 py-1 bg-red-600 text-white text-xs font-bold rounded">置顶</div>
|
<div class="absolute top-4 left-4 px-2 py-1 bg-red-600 text-white text-xs font-bold rounded">置顶</div>
|
||||||
<div class="absolute bottom-0 left-0 p-8 text-white w-full">
|
<div class="absolute bottom-0 left-0 p-8 text-white w-full">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div class="text-sm font-bold opacity-80 mb-2">[京剧] 经典唱段合集</div>
|
<div class="text-sm font-bold opacity-80 mb-2">[{{ featuredContent.genre }}]</div>
|
||||||
<h2 class="text-3xl font-bold mb-2">程派《荒山泪》夜织选段:沉浸式视听体验</h2>
|
<h2 class="text-3xl font-bold mb-2">{{ featuredContent.title }}</h2>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-2xl font-bold text-amber-400">¥ 19.90</div>
|
<div class="text-2xl font-bold text-amber-400" v-if="featuredContent.price > 0">¥ {{ featuredContent.price }}</div>
|
||||||
|
<div class="text-2xl font-bold text-green-400" v-else>免费</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -104,31 +105,25 @@
|
|||||||
<div>
|
<div>
|
||||||
<h3 class="text-xl font-bold text-slate-900 mb-6 pl-4 border-l-4 border-primary-600">最新动态</h3>
|
<h3 class="text-xl font-bold text-slate-900 mb-6 pl-4 border-l-4 border-primary-600">最新动态</h3>
|
||||||
<div class="grid grid-cols-1 gap-6">
|
<div class="grid grid-cols-1 gap-6">
|
||||||
<div v-for="i in 5" :key="i"
|
<div v-for="item in contents" :key="item.id"
|
||||||
|
v-show="!featuredContent || item.id !== featuredContent.id"
|
||||||
class="bg-white rounded-xl border border-slate-100 p-5 flex gap-6 hover:shadow-md transition-shadow group cursor-pointer"
|
class="bg-white rounded-xl border border-slate-100 p-5 flex gap-6 hover:shadow-md transition-shadow group cursor-pointer"
|
||||||
@click="$router.push(`/contents/${i}`)">
|
@click="$router.push(`/contents/${item.id}`)">
|
||||||
<div class="w-64 h-36 bg-slate-100 rounded-lg flex-shrink-0 overflow-hidden relative">
|
<div class="w-64 h-36 bg-slate-100 rounded-lg flex-shrink-0 overflow-hidden relative">
|
||||||
<img
|
<img :src="item.cover || 'https://via.placeholder.com/300x168?text=No+Cover'" class="w-full h-full object-cover">
|
||||||
:src="`https://images.unsplash.com/photo-1533174072545-e8d4aa97edf9?ixlib=rb-1.2.1&auto=format&fit=crop&w=400&q=60`"
|
|
||||||
class="w-full h-full object-cover">
|
|
||||||
<div class="absolute bottom-2 left-2 flex gap-1">
|
|
||||||
<span class="px-1.5 py-0.5 bg-black/60 text-white text-xs rounded flex items-center gap-1"><i
|
|
||||||
class="pi pi-play-circle"></i> 12:40</span>
|
|
||||||
</div>
|
|
||||||
<span v-if="i === 2"
|
|
||||||
class="absolute top-2 right-2 px-1.5 py-0.5 bg-green-500 text-white text-xs font-bold rounded">限免</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 flex flex-col">
|
<div class="flex-1 flex flex-col">
|
||||||
<div>
|
<div>
|
||||||
<h3
|
<h3 class="text-lg font-bold text-slate-900 mb-2 group-hover:text-primary-600 transition-colors line-clamp-1">
|
||||||
class="text-lg font-bold text-slate-900 mb-2 group-hover:text-primary-600 transition-colors line-clamp-1">
|
{{ item.title }}</h3>
|
||||||
梅兰芳大剧院现场实录:2024 京剧名家演唱会</h3>
|
|
||||||
<p class="text-sm text-slate-500 line-clamp-3 leading-relaxed">
|
|
||||||
本场演出汇集了当今京剧界的老中青三代名家,精彩呈现了《四郎探母》、《红鬃烈马》等经典剧目的核心唱段。高清多机位拍摄,还原现场震撼效果...</p>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-auto pt-4 flex items-center justify-between">
|
<div class="mt-auto pt-4 flex items-center justify-between">
|
||||||
<div class="text-xs text-slate-400">2小时前 · 1.2w 阅读</div>
|
<div class="text-xs text-slate-400">
|
||||||
<div class="text-lg font-bold text-red-600">¥ 9.90</div>
|
<span><i class="pi pi-eye mr-1"></i> {{ item.views }}</span>
|
||||||
|
<span class="ml-4"><i class="pi pi-thumbs-up mr-1"></i> {{ item.likes }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-lg font-bold text-red-600" v-if="item.price > 0">¥ {{ item.price }}</div>
|
||||||
|
<div class="text-lg font-bold text-green-600" v-else>免费</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -138,23 +133,9 @@
|
|||||||
|
|
||||||
<!-- 4. About Tab -->
|
<!-- 4. About Tab -->
|
||||||
<div v-if="currentTab === 'about'" class="max-w-3xl mx-auto">
|
<div v-if="currentTab === 'about'" class="max-w-3xl mx-auto">
|
||||||
<div class="prose prose-slate prose-lg text-slate-700">
|
<div class="prose prose-slate prose-lg text-slate-700 whitespace-pre-wrap">
|
||||||
<h2 class="font-bold text-2xl mb-6 pb-4 border-b border-slate-100">关于我们</h2>
|
<h2 class="font-bold text-2xl mb-6 pb-4 border-b border-slate-100">关于我们</h2>
|
||||||
<p>梅派传人小林,师从著名京剧表演艺术家,深耕京剧艺术二十余年。致力于通过新媒体形式传播国粹文化,让更多年轻人爱上戏曲。</p>
|
<p>{{ tenant.description || '暂无详细介绍' }}</p>
|
||||||
<p>本频道主要发布:</p>
|
|
||||||
<ul>
|
|
||||||
<li>经典剧目高清实录</li>
|
|
||||||
<li>唱腔身段教学课程</li>
|
|
||||||
<li>戏曲文化深度解析</li>
|
|
||||||
</ul>
|
|
||||||
<div class="my-8">
|
|
||||||
<img
|
|
||||||
src="https://images.unsplash.com/photo-1516450360452-9312f5e86fc7?ixlib=rb-1.2.1&auto=format&fit=crop&w=800&q=80"
|
|
||||||
class="rounded-xl w-full">
|
|
||||||
<p class="text-center text-sm text-slate-400 mt-2">2023年全国巡演剧照</p>
|
|
||||||
</div>
|
|
||||||
<h3>联系方式</h3>
|
|
||||||
<p>商务合作:business@example.com (点击查看)</p>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -173,29 +154,58 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { useToast } from 'primevue/usetoast';
|
import { useToast } from 'primevue/usetoast';
|
||||||
import { reactive, ref } from 'vue';
|
import { reactive, ref, onMounted } from 'vue';
|
||||||
|
import { useRoute } from 'vue-router';
|
||||||
|
import { tenantApi } from '../../api/tenant';
|
||||||
|
import { contentApi } from '../../api/content';
|
||||||
|
|
||||||
|
const route = useRoute();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
const currentTab = ref('home');
|
const currentTab = ref('home');
|
||||||
const isFollowing = ref(false);
|
const isFollowing = ref(false);
|
||||||
|
const tenant = ref({});
|
||||||
|
const contents = ref([]);
|
||||||
|
const featuredContent = ref(null);
|
||||||
|
|
||||||
const tenant = reactive({
|
const fetchData = async () => {
|
||||||
name: '梅派传人小林',
|
try {
|
||||||
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=Master1',
|
const id = route.params.id;
|
||||||
cover: 'https://images.unsplash.com/photo-1611454453122-7b02c6b58188?ixlib=rb-1.2.1&auto=format&fit=crop&w=1950&q=80',
|
const [t, c, f] = await Promise.all([
|
||||||
bio: '专注京剧程派艺术传承与推广,分享戏曲之美。',
|
tenantApi.get(id),
|
||||||
certType: 'personal'
|
contentApi.list({ tenantId: id, sort: 'latest' }),
|
||||||
});
|
contentApi.list({ tenantId: id, is_pinned: true })
|
||||||
|
]);
|
||||||
|
|
||||||
|
tenant.value = t || {};
|
||||||
|
contents.value = c?.items || [];
|
||||||
|
if (f && f.items && f.items.length > 0) {
|
||||||
|
featuredContent.value = f.items[0];
|
||||||
|
}
|
||||||
|
isFollowing.value = t?.is_following || false;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(fetchData);
|
||||||
|
|
||||||
|
const toggleFollow = async () => {
|
||||||
|
try {
|
||||||
|
if (isFollowing.value) {
|
||||||
|
await tenantApi.unfollow(route.params.id);
|
||||||
|
isFollowing.value = false;
|
||||||
|
} else {
|
||||||
|
await tenantApi.follow(route.params.id);
|
||||||
|
isFollowing.value = true;
|
||||||
|
toast.add({ severity: 'success', summary: '关注成功', detail: '已开启更新提醒', life: 3000 });
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ label: '主页', value: 'home' },
|
{ label: '主页', value: 'home' },
|
||||||
{ label: '关于', value: 'about' }
|
{ label: '关于', value: 'about' }
|
||||||
];
|
];
|
||||||
|
|
||||||
const toggleFollow = () => {
|
|
||||||
isFollowing.value = !isFollowing.value;
|
|
||||||
if (isFollowing.value) {
|
|
||||||
toast.add({ severity: 'success', summary: '关注成功', detail: '已开启更新提醒', life: 3000 });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
Reference in New Issue
Block a user