bug修复和架构完善
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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
@@ -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();
|
||||
@@ -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[];
|
||||
}
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
+509
-391
File diff suppressed because it is too large
Load Diff
+143
-22
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user