bug修复和架构完善

This commit is contained in:
2025-08-21 11:50:25 +08:00
parent 29a79b1c6b
commit 6fc61ccbd4
22 changed files with 5511 additions and 770 deletions
+17 -2
View File
@@ -2,11 +2,12 @@
<div class="artwork-card" @click="handleClick">
<div class="artwork-image">
<img
:src="artwork.image_urls.medium"
:src="getImageUrl(artwork.image_urls.medium)"
:alt="artwork.title"
@load="imageLoaded = true"
@error="imageError = true"
:class="{ loaded: imageLoaded, error: imageError }"
crossorigin="anonymous"
/>
<div v-if="!imageLoaded && !imageError" class="image-placeholder">
<LoadingSpinner text="加载中..." />
@@ -24,9 +25,10 @@
<div class="artwork-meta">
<div class="artist-info">
<img
:src="artwork.user.profile_image_urls.medium"
:src="getImageUrl(artwork.user.profile_image_urls.medium)"
:alt="artwork.user.name"
class="artist-avatar"
crossorigin="anonymous"
/>
<span class="artist-name">{{ artwork.user.name }}</span>
</div>
@@ -85,6 +87,19 @@ const imageError = ref(false);
const handleClick = () => {
emit('click', props.artwork);
};
// 处理图片URL,通过后端代理
const getImageUrl = (originalUrl: string) => {
if (!originalUrl) return '';
// 如果是Pixiv的图片URL,通过后端代理
if (originalUrl.includes('i.pximg.net')) {
const encodedUrl = encodeURIComponent(originalUrl);
return `http://localhost:3000/api/proxy/image?url=${encodedUrl}`;
}
return originalUrl;
};
</script>
<style scoped>
+5 -5
View File
@@ -18,31 +18,31 @@ const router = createRouter({
{
path: '/search',
name: 'search',
component: () => import('../views/SearchView.vue'),
component: () => import('@/views/SearchView.vue'),
meta: { requiresAuth: true }
},
{
path: '/artwork/:id',
name: 'artwork',
component: () => import('../views/ArtworkView.vue'),
component: () => import('@/views/ArtworkView.vue'),
meta: { requiresAuth: true }
},
{
path: '/artist/:id',
name: 'artist',
component: () => import('../views/ArtistView.vue'),
component: () => import('@/views/ArtistView.vue'),
meta: { requiresAuth: true }
},
{
path: '/downloads',
name: 'downloads',
component: () => import('../views/DownloadsView.vue'),
component: () => import('@/views/DownloadsView.vue'),
meta: { requiresAuth: true }
},
{
path: '/artists',
name: 'artists',
component: () => import('../views/ArtistsView.vue'),
component: () => import('@/views/ArtistsView.vue'),
meta: { requiresAuth: true }
}
]
+13
View File
@@ -75,6 +75,19 @@ class ArtistService {
async followArtist(id: number, action: 'follow' | 'unfollow'): Promise<ApiResponse> {
return apiService.post(`/api/artist/${id}/follow`, { action });
}
/**
* 获取当前用户关注的作者列表
*/
async getFollowingArtists(options: { offset?: number; limit?: number } = {}): 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());
const query = params.toString();
const url = query ? `/api/artist/following?${query}` : '/api/artist/following';
return apiService.get<{ artists: Artist[]; total: number }>(url);
}
}
export const artistService = new ArtistService();
+72 -33
View File
@@ -1,66 +1,105 @@
import apiService from './api';
import type { ApiResponse, DownloadTask, DownloadParams } from '@/types';
export interface DownloadArtworkRequest extends DownloadParams {
size?: 'original' | 'large' | 'medium' | 'square_medium';
quality?: 'high' | 'medium' | 'low';
format?: 'auto' | 'jpg' | 'png';
}
export interface DownloadMultipleRequest extends DownloadParams {
artworkIds: number[];
concurrent?: number;
}
export interface DownloadArtistRequest extends DownloadParams {
type?: 'art' | 'manga' | 'novel';
filter?: 'for_ios' | 'for_android';
limit?: number;
}
import type { DownloadTask } from '@/types';
class DownloadService {
/**
* 下载单个作品
*/
async downloadArtwork(id: number, params: DownloadArtworkRequest = {}): Promise<ApiResponse<any>> {
return apiService.post(`/api/download/artwork/${id}`, params);
async downloadArtwork(artworkId: number, options: {
size?: string;
quality?: string;
format?: string;
} = {}) {
return apiService.post(`/api/download/artwork/${artworkId}`, options);
}
/**
* 批量下载作品
*/
async downloadMultipleArtworks(params: DownloadMultipleRequest): Promise<ApiResponse<any>> {
return apiService.post('/api/download/artworks', params);
async downloadMultipleArtworks(artworkIds: number[], options: {
size?: string;
quality?: string;
format?: string;
concurrent?: number;
} = {}) {
return apiService.post('/api/download/artworks', {
artworkIds,
...options
});
}
/**
* 下载作者作品
*/
async downloadArtistArtworks(id: number, params: DownloadArtistRequest = {}): Promise<ApiResponse<any>> {
return apiService.post(`/api/download/artist/${id}`, params);
async downloadArtistArtworks(artistId: number, options: {
type?: string;
limit?: number;
size?: string;
quality?: string;
format?: string;
} = {}) {
return apiService.post(`/api/download/artist/${artistId}`, options);
}
/**
* 获取下载进度
* 获取任务进度
*/
async getDownloadProgress(taskId: string): Promise<ApiResponse<DownloadTask>> {
return apiService.get<DownloadTask>(`/api/download/progress/${taskId}`);
async getTaskProgress(taskId: string) {
return apiService.get(`/api/download/progress/${taskId}`);
}
/**
* 获取所有任务
*/
async getAllTasks() {
return apiService.get('/api/download/tasks');
}
/**
* 取消下载任务
*/
async cancelDownload(taskId: string): Promise<ApiResponse> {
return apiService.delete(`/api/download/cancel/${taskId}`);
async cancelTask(taskId: string) {
return apiService.post(`/api/download/cancel/${taskId}`);
}
/**
* 获取下载历史
*/
async getDownloadHistory(offset: number = 0, limit: number = 20): Promise<ApiResponse<{ tasks: DownloadTask[]; total: number; offset: number; limit: number }>> {
return apiService.get<{ tasks: DownloadTask[]; total: number; offset: number; limit: number }>(`/api/download/history?offset=${offset}&limit=${limit}`);
async getDownloadHistory(limit: number = 50, offset: number = 0) {
return apiService.get('/api/download/history', {
params: { limit, offset }
});
}
/**
* 获取下载的文件列表
*/
async getDownloadedFiles() {
return apiService.get('/api/download/files');
}
/**
* 检查作品是否已下载
*/
async checkArtworkDownloaded(artworkId: number) {
return apiService.get(`/api/download/check/${artworkId}`);
}
/**
* 获取已下载的作品ID列表
*/
async getDownloadedArtworkIds() {
return apiService.get('/api/download/downloaded-ids');
}
/**
* 删除下载的文件
*/
async deleteDownloadedFiles(artist: string, artwork: string) {
return apiService.delete('/api/download/files', {
data: { artist, artwork }
});
}
}
export const downloadService = new DownloadService();
export default downloadService;
export default new DownloadService();
+4
View File
@@ -105,10 +105,14 @@ export interface DownloadTask {
failed: number;
start_time: string;
end_time?: string;
error?: string;
artwork_id?: number;
artist_id?: number;
files?: Array<{
path: string;
url: string;
size: string;
filename: string;
}>;
results?: any[];
}
+16 -2
View File
@@ -14,9 +14,10 @@
<div class="artist-header">
<div class="artist-profile">
<img
:src="artist.profile_image_urls.medium"
:src="getImageUrl(artist.profile_image_urls.medium)"
:alt="artist.name"
class="artist-avatar"
crossorigin="anonymous"
/>
<div class="artist-info">
<h1 class="artist-name">{{ artist.name }}</h1>
@@ -64,7 +65,7 @@
<div class="section-header">
<h2>作品列表</h2>
<div class="artwork-filters">
<select v-model="artworkType" @change="fetchArtworks" class="filter-select">
<select v-model="artworkType" @change="() => fetchArtworks()" class="filter-select">
<option value="art">插画</option>
<option value="manga">漫画</option>
<option value="novel">小说</option>
@@ -244,6 +245,19 @@ const handleDownloadAll = async () => {
}
};
// 处理图片URL,通过后端代理
const getImageUrl = (originalUrl: string) => {
if (!originalUrl) return '';
// 如果是Pixiv的图片URL,通过后端代理
if (originalUrl.includes('i.pximg.net')) {
const encodedUrl = encodeURIComponent(originalUrl);
return `http://localhost:3000/api/proxy/image?url=${encodedUrl}`;
}
return originalUrl;
};
// 点击作品
const handleArtworkClick = (artwork: Artwork) => {
router.push(`/artwork/${artwork.id}`);
+23 -10
View File
@@ -42,9 +42,10 @@
>
<div class="artist-header">
<img
:src="artist.profile_image_urls.medium"
:src="getImageUrl(artist.profile_image_urls.medium)"
:alt="artist.name"
class="artist-avatar"
crossorigin="anonymous"
/>
<div class="artist-info">
<h3 class="artist-name">{{ artist.name }}</h3>
@@ -105,9 +106,10 @@
>
<div class="artist-header">
<img
:src="artist.profile_image_urls.medium"
:src="getImageUrl(artist.profile_image_urls.medium)"
:alt="artist.name"
class="artist-avatar"
crossorigin="anonymous"
/>
<div class="artist-info">
<h3 class="artist-name">{{ artist.name }}</h3>
@@ -181,14 +183,12 @@ const fetchFollowingArtists = async () => {
loading.value = true;
error.value = null;
// 这里需要根据实际API调整
// const response = await artistService.getFollowingArtists();
// if (response.success && response.data) {
// followingArtists.value = response.data.artists;
// }
// 暂时使用模拟数据
followingArtists.value = [];
const response = await artistService.getFollowingArtists();
if (response.success && response.data) {
followingArtists.value = response.data.artists;
} else {
throw new Error(response.error || '获取关注列表失败');
}
} catch (err) {
error.value = err instanceof Error ? err.message : '获取关注列表失败';
console.error('获取关注列表失败:', err);
@@ -292,6 +292,19 @@ const clearError = () => {
error.value = null;
};
// 处理图片URL,通过后端代理
const getImageUrl = (originalUrl: string) => {
if (!originalUrl) return '';
// 如果是Pixiv的图片URL,通过后端代理
if (originalUrl.includes('i.pximg.net')) {
const encodedUrl = encodeURIComponent(originalUrl);
return `http://localhost:3000/api/proxy/image?url=${encodedUrl}`;
}
return originalUrl;
};
onMounted(() => {
fetchFollowingArtists();
});
+18 -3
View File
@@ -14,11 +14,12 @@
<div class="artwork-gallery">
<div class="main-image">
<img
:src="currentImageUrl"
:src="getImageUrl(currentImageUrl)"
:alt="artwork.title"
@load="imageLoaded = true"
@error="imageError = true"
:class="{ loaded: imageLoaded, error: imageError }"
crossorigin="anonymous"
/>
<div v-if="!imageLoaded && !imageError" class="image-placeholder">
<LoadingSpinner text="加载中..." />
@@ -37,7 +38,7 @@
class="thumbnail"
:class="{ active: currentPage === index }"
>
<img :src="page.image_urls.square_medium" :alt="`第 ${index + 1} 页`" />
<img :src="getImageUrl(page.image_urls.square_medium)" :alt="`第 ${index + 1} 页`" crossorigin="anonymous" />
</button>
</div>
</div>
@@ -59,9 +60,10 @@
<!-- 作者信息 -->
<div class="artist-info">
<img
:src="artwork.user.profile_image_urls.medium"
:src="getImageUrl(artwork.user.profile_image_urls.medium)"
:alt="artwork.user.name"
class="artist-avatar"
crossorigin="anonymous"
/>
<div class="artist-details">
<h3 class="artist-name">{{ artwork.user.name }}</h3>
@@ -223,6 +225,19 @@ const formatDate = (dateString: string) => {
return new Date(dateString).toLocaleDateString('zh-CN');
};
// 处理图片URL,通过后端代理
const getImageUrl = (originalUrl: string) => {
if (!originalUrl) return '';
// 如果是Pixiv的图片URL,通过后端代理
if (originalUrl.includes('i.pximg.net')) {
const encodedUrl = encodeURIComponent(originalUrl);
return `http://localhost:3000/api/proxy/image?url=${encodedUrl}`;
}
return originalUrl;
};
// 清除错误
const clearError = () => {
error.value = null;
File diff suppressed because it is too large Load Diff
+143 -22
View File
@@ -4,21 +4,75 @@
<div class="container">
<h1 class="page-title">搜索作品</h1>
<div class="search-form">
<div class="search-input-group">
<input
v-model="searchKeyword"
type="text"
placeholder="输入关键词搜索作品..."
class="search-input"
@keyup.enter="handleSearch"
/>
<button @click="handleSearch" class="search-btn" :disabled="loading">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
</svg>
</button>
</div>
<div class="search-form">
<!-- 搜索类型选择 -->
<div class="search-type-tabs">
<button
@click="searchMode = 'keyword'"
class="tab-btn"
:class="{ active: searchMode === 'keyword' }"
>
关键词搜索
</button>
<button
@click="searchMode = 'artwork'"
class="tab-btn"
:class="{ active: searchMode === 'artwork' }"
>
作品ID
</button>
<button
@click="searchMode = 'artist'"
class="tab-btn"
:class="{ active: searchMode === 'artist' }"
>
作者ID
</button>
</div>
<!-- 关键词搜索 -->
<div v-if="searchMode === 'keyword'" class="search-input-group">
<input
v-model="searchKeyword"
type="text"
placeholder="输入关键词搜索作品..."
class="search-input"
@keyup.enter="handleSearch"
/>
<button @click="handleSearch" class="search-btn" :disabled="loading">
<svg viewBox="0 0 24 24" fill="currentColor">
<path d="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/>
</svg>
</button>
</div>
<!-- 作品ID搜索 -->
<div v-if="searchMode === 'artwork'" class="search-input-group">
<input
v-model="artworkId"
type="text"
placeholder="输入作品ID..."
class="search-input"
@keyup.enter="handleArtworkSearch"
/>
<button @click="handleArtworkSearch" class="search-btn" :disabled="loading">
查看作品
</button>
</div>
<!-- 作者ID搜索 -->
<div v-if="searchMode === 'artist'" class="search-input-group">
<input
v-model="artistId"
type="text"
placeholder="输入作者ID..."
class="search-input"
@keyup.enter="handleArtistSearch"
/>
<button @click="handleArtistSearch" class="search-btn" :disabled="loading">
查看作者
</button>
</div>
<div class="search-filters">
<select v-model="searchType" class="filter-select">
@@ -111,6 +165,11 @@
// 搜索状态
const searchKeyword = ref('');
const searchMode = ref<'keyword' | 'artwork' | 'artist'>('keyword');
const artworkId = ref('');
const artistId = ref('');
// 关键词搜索参数
const searchType = ref<'all' | 'art' | 'manga' | 'novel'>('all');
const searchSort = ref<'date_desc' | 'date_asc' | 'popular_desc'>('date_desc');
const searchDuration = ref<'all' | 'within_last_day' | 'within_last_week' | 'within_last_month'>('all');
@@ -197,9 +256,43 @@
router.push(`/artwork/${artwork.id}`);
};
const clearError = () => {
error.value = null;
};
// 作品ID搜索
const handleArtworkSearch = () => {
const idStr = artworkId.value?.toString().trim();
if (!idStr) {
error.value = '请输入作品ID';
return;
}
const id = parseInt(idStr);
if (isNaN(id)) {
error.value = '请输入有效的作品ID';
return;
}
router.push(`/artwork/${id}`);
};
// 作者ID搜索
const handleArtistSearch = () => {
const idStr = artistId.value?.toString().trim();
if (!idStr) {
error.value = '请输入作者ID';
return;
}
const id = parseInt(idStr);
if (isNaN(id)) {
error.value = '请输入有效的作者ID';
return;
}
router.push(`/artist/${id}`);
};
const clearError = () => {
error.value = null;
};
</script>
<style scoped>
@@ -233,10 +326,38 @@
gap: 1rem;
}
.search-input-group {
display: flex;
gap: 0.5rem;
}
.search-type-tabs {
display: flex;
gap: 0.5rem;
margin-bottom: 1rem;
}
.tab-btn {
padding: 0.5rem 1rem;
border: 1px solid #d1d5db;
background: white;
color: #6b7280;
border-radius: 0.375rem;
cursor: pointer;
transition: all 0.2s;
font-size: 0.875rem;
}
.tab-btn:hover {
background: #f3f4f6;
color: #374151;
}
.tab-btn.active {
background: #3b82f6;
color: white;
border-color: #3b82f6;
}
.search-input-group {
display: flex;
gap: 0.5rem;
}
.search-input {
flex: 1;