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

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) => {
try {
const { offset = 0, limit = 30 } = req.query;
const { restrict = 'public' } = req.query;
const artistService = new ArtistService(req.backend.getAuth());
const result = await artistService.getFollowingArtists({
offset: parseInt(offset),
limit: parseInt(limit)
restrict
});
if (result.success) {
+39 -20
View File
@@ -93,7 +93,7 @@ class ArtistService {
*/
async getArtistFollowing(artistId, options = {}) {
try {
const { restrict = 'public', offset = 0, limit = 30 } = options;
const { restrict = 'public', offset = 0, limit = 100 } = options;
const params = {
user_id: artistId,
@@ -124,7 +124,7 @@ class ArtistService {
*/
async getArtistFollowers(artistId, options = {}) {
try {
const { offset = 0, limit = 30 } = options;
const { offset = 0, limit = 100 } = options;
const params = {
user_id: artistId,
@@ -154,7 +154,7 @@ class ArtistService {
*/
async getFollowingArtists(options = {}) {
try {
const { offset = 0, limit = 30 } = options;
const { offset = 0, limit = 30, restrict = 'public' } = options;
// 检查认证状态
if (!this.auth || !this.auth.accessToken) {
@@ -180,29 +180,48 @@ class ArtistService {
};
}
const params = {
user_id: currentUserId,
restrict: 'public',
offset,
limit,
};
let allArtists = [];
let currentOffset = offset;
let hasMore = true;
const response = await this.makeRequest('GET', `/v1/user/following?${stringify(params)}`);
// 循环获取所有关注的作者
while (hasMore) {
const params = {
user_id: currentUserId,
restrict,
offset: currentOffset,
limit,
};
// 转换数据格式以匹配前端期望
const artists = (response.user_previews || []).map(user => ({
id: user.user.id,
name: user.user.name,
account: user.user.account,
profile_image_urls: user.user.profile_image_urls,
is_followed: user.user.is_followed || false,
}));
console.log(`请求关注列表: offset=${currentOffset}, limit=${limit}`);
const response = await this.makeRequest('GET', `/v1/user/following?${stringify(params)}`);
// 转换数据格式以匹配前端期望
const artists = (response.user_previews || []).map(user => ({
id: user.user.id,
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 {
success: true,
data: {
artists: artists,
total: artists.length,
artists: allArtists,
total: allArtists.length,
},
};
} 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();
if (options.offset !== undefined) params.append('offset', options.offset.toString());
if (options.limit !== undefined) params.append('limit', options.limit.toString());
if (options.restrict) params.append('restrict', options.restrict);
const query = params.toString();
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) {
return {
@@ -39,12 +39,18 @@ export const useArtistStore = defineStore('artist', () => {
loading.value = true;
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) {
followingArtists.value = response.data.artists;
lastFetchTime.value = Date.now();
} else {
throw new Error(response.error || '获取关注列表失败');
throw new Error('获取关注列表失败');
}
return response;
@@ -70,7 +76,7 @@ export const useArtistStore = defineStore('artist', () => {
searchResults.value = response.data.artists;
return response;
} else {
throw new Error(response.error || '搜索失败');
throw new Error('搜索失败');
}
} catch (err) {
error.value = err instanceof Error ? err.message : '搜索失败';
@@ -97,7 +103,7 @@ export const useArtistStore = defineStore('artist', () => {
followingArtists.value.push(artistToAdd);
}
} else {
throw new Error(response.error || '关注失败');
throw new Error('关注失败');
}
return response;
@@ -123,7 +129,7 @@ export const useArtistStore = defineStore('artist', () => {
artist.is_followed = false;
}
} else {
throw new Error(response.error || '取消关注失败');
throw new Error('取消关注失败');
}
return response;
+142 -16
View File
@@ -4,6 +4,23 @@
<div class="page-header">
<h1 class="page-title">作者管理</h1>
<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">
<svg viewBox="0 0 24 24" fill="currentColor" class="refresh-icon">
<path
@@ -44,6 +61,11 @@
</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">
<ArtistCard v-for="artist in artistStore.followingArtists" :key="artist.id" :artist="artist"
:show-follow-button="false" :show-unfollow-button="true" @unfollow="handleUnfollow"
@@ -130,8 +152,8 @@
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted, watch } from 'vue';
import { useRouter } from 'vue-router';
import { ref, onMounted, watch } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { useAuthStore } from '@/stores/auth';
import { useArtistStore } from '@/stores/artist';
import downloadService from '@/services/download';
@@ -139,6 +161,7 @@ import downloadService from '@/services/download';
import ArtistCard from '@/components/artist/ArtistCard.vue';
const router = useRouter();
const route = useRoute();
const authStore = useAuthStore();
const artistStore = useArtistStore();
@@ -149,15 +172,44 @@ const downloadLimit = ref('50');
const downloading = ref(false);
const downloadSuccess = ref<string | null>(null);
// 关注类型切换
const followType = ref<'public' | 'private'>('public');
// 获取关注的作者
const fetchFollowingArtists = async () => {
const fetchFollowingArtists = async (forceRefresh = false) => {
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) {
artistStore.error = err instanceof Error ? err.message : '获取关注列表失败';
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) => {
try {
@@ -171,6 +223,8 @@ const handleFollow = async (artistId: number) => {
const handleUnfollow = async (artistId: number) => {
try {
await artistStore.unfollowArtist(artistId);
// 重新获取数据
await fetchFollowingArtists(true);
} catch (err) {
console.error('取消关注失败:', err);
}
@@ -228,24 +282,30 @@ const handleDownloadArtist = async () => {
// 刷新数据
const handleRefresh = async () => {
try {
await artistStore.refreshData();
await fetchFollowingArtists(true);
} catch (err) {
console.error('刷新失败:', err);
}
};
// 监听数据过期状态,自动刷新
watch(() => artistStore.isDataStale, (isStale) => {
if (isStale && artistStore.hasFollowingArtists) {
console.log('数据已过期,自动刷新...');
fetchFollowingArtists();
// 监听路由变化
watch(() => route.query, () => {
// 恢复关注类型状态
const urlType = route.query.type as string;
if (urlType && ['public', 'private'].includes(urlType) && urlType !== followType.value) {
followType.value = urlType as 'public' | 'private';
fetchFollowingArtists(true);
}
});
onMounted(() => {
fetchFollowingArtists();
onMounted(async () => {
// 检查URL参数并恢复状态
const urlType = route.query.type as string;
if (urlType && ['public', 'private'].includes(urlType)) {
followType.value = urlType as 'public' | 'private';
}
await fetchFollowingArtists();
});
</script>
@@ -281,6 +341,45 @@ onMounted(() => {
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 {
width: 1rem;
height: 1rem;
@@ -348,14 +447,25 @@ onMounted(() => {
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 {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 1.5rem;
margin-bottom: 2rem;
}
.empty-section {
text-align: center;
padding: 4rem 0;
@@ -574,8 +684,24 @@ onMounted(() => {
align-items: stretch;
}
.header-actions {
flex-direction: column;
gap: 1rem;
}
.follow-type-toggle {
margin-right: 0;
justify-content: center;
}
.artists-grid {
grid-template-columns: 1fr;
}
.stats-section {
flex-direction: column;
gap: 0.5rem;
text-align: center;
}
}
</style>