增加非公开关注和关注区分

This commit is contained in:
2025-08-29 08:15:58 +08:00
parent 47ced6da37
commit 3a00c9dce7
5 changed files with 197 additions and 48 deletions
+2 -3
View File
@@ -61,12 +61,11 @@ router.get('/search', async (req, res) => {
*/ */
router.get('/following', async (req, res) => { router.get('/following', async (req, res) => {
try { try {
const { offset = 0, limit = 30 } = req.query; const { restrict = 'public' } = req.query;
const artistService = new ArtistService(req.backend.getAuth()); const artistService = new ArtistService(req.backend.getAuth());
const result = await artistService.getFollowingArtists({ const result = await artistService.getFollowingArtists({
offset: parseInt(offset), restrict
limit: parseInt(limit)
}); });
if (result.success) { if (result.success) {
+39 -20
View File
@@ -93,7 +93,7 @@ class ArtistService {
*/ */
async getArtistFollowing(artistId, options = {}) { async getArtistFollowing(artistId, options = {}) {
try { try {
const { restrict = 'public', offset = 0, limit = 30 } = options; const { restrict = 'public', offset = 0, limit = 100 } = options;
const params = { const params = {
user_id: artistId, user_id: artistId,
@@ -124,7 +124,7 @@ class ArtistService {
*/ */
async getArtistFollowers(artistId, options = {}) { async getArtistFollowers(artistId, options = {}) {
try { try {
const { offset = 0, limit = 30 } = options; const { offset = 0, limit = 100 } = options;
const params = { const params = {
user_id: artistId, user_id: artistId,
@@ -154,7 +154,7 @@ class ArtistService {
*/ */
async getFollowingArtists(options = {}) { async getFollowingArtists(options = {}) {
try { try {
const { offset = 0, limit = 30 } = options; const { offset = 0, limit = 30, restrict = 'public' } = options;
// 检查认证状态 // 检查认证状态
if (!this.auth || !this.auth.accessToken) { if (!this.auth || !this.auth.accessToken) {
@@ -180,29 +180,48 @@ class ArtistService {
}; };
} }
const params = { let allArtists = [];
user_id: currentUserId, let currentOffset = offset;
restrict: 'public', let hasMore = true;
offset,
limit,
};
const response = await this.makeRequest('GET', `/v1/user/following?${stringify(params)}`); // 循环获取所有关注的作者
while (hasMore) {
const params = {
user_id: currentUserId,
restrict,
offset: currentOffset,
limit,
};
// 转换数据格式以匹配前端期望 console.log(`请求关注列表: offset=${currentOffset}, limit=${limit}`);
const artists = (response.user_previews || []).map(user => ({ const response = await this.makeRequest('GET', `/v1/user/following?${stringify(params)}`);
id: user.user.id,
name: user.user.name, // 转换数据格式以匹配前端期望
account: user.user.account, const artists = (response.user_previews || []).map(user => ({
profile_image_urls: user.user.profile_image_urls, id: user.user.id,
is_followed: user.user.is_followed || false, name: user.user.name,
})); account: user.user.account,
profile_image_urls: user.user.profile_image_urls,
is_followed: user.user.is_followed || false,
}));
allArtists.push(...artists);
console.log(`本次获取到 ${artists.length} 个作者,累计 ${allArtists.length}`);
// 如果返回的数量少于limit,说明已经获取完所有数据
if (artists.length < limit) {
hasMore = false;
console.log('已获取完所有关注的作者');
} else {
currentOffset += artists.length;
}
}
return { return {
success: true, success: true,
data: { data: {
artists: artists, artists: allArtists,
total: artists.length, total: allArtists.length,
}, },
}; };
} catch (error) { } catch (error) {
+2 -3
View File
@@ -85,10 +85,9 @@ class ArtistService {
/** /**
* 获取当前用户关注的作者列表 * 获取当前用户关注的作者列表
*/ */
async getFollowingArtists(options: { offset?: number; limit?: number } = {}): Promise<ApiResponse<{ artists: Artist[]; total: number }>> { async getFollowingArtists(options: { restrict?: 'public' | 'private' } = {}): Promise<ApiResponse<{ artists: Artist[]; total: number }>> {
const params = new URLSearchParams(); const params = new URLSearchParams();
if (options.offset !== undefined) params.append('offset', options.offset.toString()); if (options.restrict) params.append('restrict', options.restrict);
if (options.limit !== undefined) params.append('limit', options.limit.toString());
const query = params.toString(); const query = params.toString();
const url = query ? `/api/artist/following?${query}` : '/api/artist/following'; const url = query ? `/api/artist/following?${query}` : '/api/artist/following';
+12 -6
View File
@@ -26,7 +26,7 @@ export const useArtistStore = defineStore('artist', () => {
}); });
// 获取关注的作者 // 获取关注的作者
const fetchFollowingArtists = async (forceRefresh = false) => { const fetchFollowingArtists = async (forceRefresh = false, options: { restrict?: 'public' | 'private' } = {}) => {
// 如果数据不是过期的且不是强制刷新,直接返回缓存的数据 // 如果数据不是过期的且不是强制刷新,直接返回缓存的数据
if (!forceRefresh && !isDataStale.value && hasFollowingArtists.value) { if (!forceRefresh && !isDataStale.value && hasFollowingArtists.value) {
return { return {
@@ -39,12 +39,18 @@ export const useArtistStore = defineStore('artist', () => {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
const response = await artistService.getFollowingArtists(); const restrict = options.restrict || 'public';
// 后端会自动循环获取所有数据
const response = await artistService.getFollowingArtists({
restrict: restrict
});
if (response.success && response.data) { if (response.success && response.data) {
followingArtists.value = response.data.artists; followingArtists.value = response.data.artists;
lastFetchTime.value = Date.now(); lastFetchTime.value = Date.now();
} else { } else {
throw new Error(response.error || '获取关注列表失败'); throw new Error('获取关注列表失败');
} }
return response; return response;
@@ -70,7 +76,7 @@ export const useArtistStore = defineStore('artist', () => {
searchResults.value = response.data.artists; searchResults.value = response.data.artists;
return response; return response;
} else { } else {
throw new Error(response.error || '搜索失败'); throw new Error('搜索失败');
} }
} catch (err) { } catch (err) {
error.value = err instanceof Error ? err.message : '搜索失败'; error.value = err instanceof Error ? err.message : '搜索失败';
@@ -97,7 +103,7 @@ export const useArtistStore = defineStore('artist', () => {
followingArtists.value.push(artistToAdd); followingArtists.value.push(artistToAdd);
} }
} else { } else {
throw new Error(response.error || '关注失败'); throw new Error('关注失败');
} }
return response; return response;
@@ -123,7 +129,7 @@ export const useArtistStore = defineStore('artist', () => {
artist.is_followed = false; artist.is_followed = false;
} }
} else { } else {
throw new Error(response.error || '取消关注失败'); throw new Error('取消关注失败');
} }
return response; return response;
+142 -16
View File
@@ -4,6 +4,23 @@
<div class="page-header"> <div class="page-header">
<h1 class="page-title">作者管理</h1> <h1 class="page-title">作者管理</h1>
<div class="header-actions"> <div class="header-actions">
<!-- 关注类型切换 -->
<div class="follow-type-toggle">
<button
@click="switchFollowType('public')"
:class="['toggle-btn', { active: followType === 'public' }]"
:disabled="artistStore.loading"
>
公开关注
</button>
<button
@click="switchFollowType('private')"
:class="['toggle-btn', { active: followType === 'private' }]"
:disabled="artistStore.loading"
>
非公开关注
</button>
</div>
<button @click="handleRefresh" class="btn btn-secondary" :disabled="artistStore.loading"> <button @click="handleRefresh" class="btn btn-secondary" :disabled="artistStore.loading">
<svg viewBox="0 0 24 24" fill="currentColor" class="refresh-icon"> <svg viewBox="0 0 24 24" fill="currentColor" class="refresh-icon">
<path <path
@@ -44,6 +61,11 @@
</div> </div>
</div> </div>
<!-- 作者统计信息 -->
<div v-if="artistStore.hasFollowingArtists" class="stats-section">
<span> {{ artistStore.followingArtists.length }} 个关注的作者</span>
</div>
<div v-if="artistStore.followingArtists.length > 0" class="artists-grid"> <div v-if="artistStore.followingArtists.length > 0" class="artists-grid">
<ArtistCard v-for="artist in artistStore.followingArtists" :key="artist.id" :artist="artist" <ArtistCard v-for="artist in artistStore.followingArtists" :key="artist.id" :artist="artist"
:show-follow-button="false" :show-unfollow-button="true" @unfollow="handleUnfollow" :show-follow-button="false" :show-unfollow-button="true" @unfollow="handleUnfollow"
@@ -130,8 +152,8 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch } from 'vue'; import { ref, onMounted, watch } from 'vue';
import { useRouter } from 'vue-router'; import { useRouter, useRoute } from 'vue-router';
import { useAuthStore } from '@/stores/auth'; import { useAuthStore } from '@/stores/auth';
import { useArtistStore } from '@/stores/artist'; import { useArtistStore } from '@/stores/artist';
import downloadService from '@/services/download'; import downloadService from '@/services/download';
@@ -139,6 +161,7 @@ import downloadService from '@/services/download';
import ArtistCard from '@/components/artist/ArtistCard.vue'; import ArtistCard from '@/components/artist/ArtistCard.vue';
const router = useRouter(); const router = useRouter();
const route = useRoute();
const authStore = useAuthStore(); const authStore = useAuthStore();
const artistStore = useArtistStore(); const artistStore = useArtistStore();
@@ -149,15 +172,44 @@ const downloadLimit = ref('50');
const downloading = ref(false); const downloading = ref(false);
const downloadSuccess = ref<string | null>(null); const downloadSuccess = ref<string | null>(null);
// 关注类型切换
const followType = ref<'public' | 'private'>('public');
// 获取关注的作者 // 获取关注的作者
const fetchFollowingArtists = async () => { const fetchFollowingArtists = async (forceRefresh = false) => {
try { try {
await artistStore.fetchFollowingArtists(); artistStore.loading = true;
artistStore.error = null;
const response = await artistStore.fetchFollowingArtists(forceRefresh, {
restrict: followType.value
});
if (!response.success) {
throw new Error('获取关注列表失败');
}
} catch (err) { } catch (err) {
artistStore.error = err instanceof Error ? err.message : '获取关注列表失败';
console.error('获取关注列表失败:', err); console.error('获取关注列表失败:', err);
} finally {
artistStore.loading = false;
} }
}; };
// 切换关注类型
const switchFollowType = async (type: 'public' | 'private') => {
followType.value = type;
// 更新URL参数
router.push({
query: {
type: type
}
});
await fetchFollowingArtists(true);
};
// 关注作者 // 关注作者
const handleFollow = async (artistId: number) => { const handleFollow = async (artistId: number) => {
try { try {
@@ -171,6 +223,8 @@ const handleFollow = async (artistId: number) => {
const handleUnfollow = async (artistId: number) => { const handleUnfollow = async (artistId: number) => {
try { try {
await artistStore.unfollowArtist(artistId); await artistStore.unfollowArtist(artistId);
// 重新获取数据
await fetchFollowingArtists(true);
} catch (err) { } catch (err) {
console.error('取消关注失败:', err); console.error('取消关注失败:', err);
} }
@@ -228,24 +282,30 @@ const handleDownloadArtist = async () => {
// 刷新数据 // 刷新数据
const handleRefresh = async () => { const handleRefresh = async () => {
try { try {
await artistStore.refreshData(); await fetchFollowingArtists(true);
} catch (err) { } catch (err) {
console.error('刷新失败:', err); console.error('刷新失败:', err);
} }
}; };
// 监听路由变化
watch(() => route.query, () => {
// 监听数据过期状态,自动刷新 // 恢复关注类型状态
watch(() => artistStore.isDataStale, (isStale) => { const urlType = route.query.type as string;
if (isStale && artistStore.hasFollowingArtists) { if (urlType && ['public', 'private'].includes(urlType) && urlType !== followType.value) {
console.log('数据已过期,自动刷新...'); followType.value = urlType as 'public' | 'private';
fetchFollowingArtists(); fetchFollowingArtists(true);
} }
}); });
onMounted(() => { onMounted(async () => {
fetchFollowingArtists(); // 检查URL参数并恢复状态
const urlType = route.query.type as string;
if (urlType && ['public', 'private'].includes(urlType)) {
followType.value = urlType as 'public' | 'private';
}
await fetchFollowingArtists();
}); });
</script> </script>
@@ -281,6 +341,45 @@ onMounted(() => {
gap: 1rem; gap: 1rem;
} }
.follow-type-toggle {
display: flex;
gap: 0.25rem;
background: #f3f4f6;
border: 1px solid #d1d5db;
border-radius: 0.5rem;
padding: 0.25rem;
margin-right: 1rem;
}
.toggle-btn {
background: none;
border: none;
padding: 0.5rem 1rem;
cursor: pointer;
font-size: 0.875rem;
color: #6b7280;
border-radius: 0.375rem;
transition: all 0.2s;
font-weight: 500;
}
.toggle-btn:hover:not(:disabled) {
background: #e5e7eb;
color: #374151;
}
.toggle-btn:disabled {
background: #f9fafb;
color: #9ca3af;
cursor: not-allowed;
}
.toggle-btn.active {
background: #4f46e5;
color: white;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.refresh-icon { .refresh-icon {
width: 1rem; width: 1rem;
height: 1rem; height: 1rem;
@@ -348,14 +447,25 @@ onMounted(() => {
height: 0.875rem; height: 0.875rem;
} }
.stats-section {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
padding: 0.75rem 1rem;
background: #f3f4f6;
border-radius: 0.5rem;
font-size: 0.875rem;
color: #6b7280;
}
.artists-grid { .artists-grid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 1.5rem; gap: 1.5rem;
margin-bottom: 2rem;
} }
.empty-section { .empty-section {
text-align: center; text-align: center;
padding: 4rem 0; padding: 4rem 0;
@@ -574,8 +684,24 @@ onMounted(() => {
align-items: stretch; align-items: stretch;
} }
.header-actions {
flex-direction: column;
gap: 1rem;
}
.follow-type-toggle {
margin-right: 0;
justify-content: center;
}
.artists-grid { .artists-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.stats-section {
flex-direction: column;
gap: 0.5rem;
text-align: center;
}
} }
</style> </style>