增加非公开关注和关注区分
This commit is contained in:
@@ -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
@@ -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) {
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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>
|
||||||
Reference in New Issue
Block a user