增加图片缓存,页数跳转,搜索页面返回query

This commit is contained in:
2025-08-25 14:26:41 +08:00
parent 09b6091293
commit f4b867916b
9 changed files with 1472 additions and 36 deletions
+348
View File
@@ -0,0 +1,348 @@
<template>
<div class="cache-manager">
<div class="cache-header">
<h3>图片缓存管理</h3>
<div class="cache-actions">
<button @click="refreshStats" class="btn btn-secondary" :disabled="loading">
{{ loading ? '刷新中...' : '刷新' }}
</button>
<button @click="clearExpiredCache" class="btn btn-warning" :disabled="loading">
清理过期缓存
</button>
<button @click="clearAllCache" class="btn btn-danger" :disabled="loading">
清理所有缓存
</button>
</div>
</div>
<div v-if="error" class="error-message">
{{ error }}
</div>
<div v-if="stats" class="cache-stats">
<div class="stat-item">
<div class="stat-label">缓存文件数</div>
<div class="stat-value">{{ stats.fileCount }}</div>
</div>
<div class="stat-item">
<div class="stat-label">缓存大小</div>
<div class="stat-value">{{ formatFileSize(stats.totalSize) }}</div>
</div>
<div class="stat-item">
<div class="stat-label">最大缓存大小</div>
<div class="stat-value">{{ formatFileSize(stats.maxSize) }}</div>
</div>
<div class="stat-item">
<div class="stat-label">缓存有效期</div>
<div class="stat-value">{{ formatDuration(stats.maxAge) }}</div>
</div>
<div class="stat-item">
<div class="stat-label">使用率</div>
<div class="stat-value">
<div class="usage-bar">
<div
class="usage-fill"
:style="{ width: `${usagePercentage}%` }"
:class="{ 'usage-high': usagePercentage > 80 }"
></div>
</div>
<span class="usage-text">{{ usagePercentage.toFixed(1) }}%</span>
</div>
</div>
</div>
<div v-if="loading && !stats" class="loading">
加载缓存信息中...
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue';
import apiService from '@/services/api';
interface CacheStats {
fileCount: number;
totalSize: number;
maxSize: number;
maxAge: number;
}
const stats = ref<CacheStats | null>(null);
const loading = ref(false);
const error = ref<string | null>(null);
// 计算使用率百分比
const usagePercentage = computed(() => {
if (!stats.value) return 0;
return (stats.value.totalSize / stats.value.maxSize) * 100;
});
// 格式化文件大小
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
// 格式化持续时间
const formatDuration = (milliseconds: number): string => {
const hours = Math.floor(milliseconds / (1000 * 60 * 60));
const days = Math.floor(hours / 24);
if (days > 0) {
return `${days}`;
} else if (hours > 0) {
return `${hours} 小时`;
} else {
const minutes = Math.floor(milliseconds / (1000 * 60));
return `${minutes} 分钟`;
}
};
// 获取缓存统计信息
const fetchCacheStats = async () => {
try {
loading.value = true;
error.value = null;
const response = await apiService.get<CacheStats>('/api/proxy/cache/stats');
if (response.success && response.data) {
stats.value = response.data;
} else {
throw new Error(response.error || '获取缓存统计失败');
}
} catch (err) {
error.value = err instanceof Error ? err.message : '获取缓存统计失败';
console.error('获取缓存统计失败:', err);
} finally {
loading.value = false;
}
};
// 刷新统计信息
const refreshStats = () => {
fetchCacheStats();
};
// 清理过期缓存
const clearExpiredCache = async () => {
if (!confirm('确定要清理过期的缓存文件吗?')) return;
try {
loading.value = true;
error.value = null;
const response = await apiService.delete('/api/proxy/cache/expired');
if (response.success) {
alert('过期缓存已清理');
await fetchCacheStats(); // 刷新统计信息
} else {
throw new Error(response.error || '清理过期缓存失败');
}
} catch (err) {
error.value = err instanceof Error ? err.message : '清理过期缓存失败';
console.error('清理过期缓存失败:', err);
} finally {
loading.value = false;
}
};
// 清理所有缓存
const clearAllCache = async () => {
if (!confirm('确定要清理所有缓存文件吗?这将删除所有缓存的图片。')) return;
try {
loading.value = true;
error.value = null;
const response = await apiService.delete('/api/proxy/cache');
if (response.success) {
alert('所有缓存已清理');
await fetchCacheStats(); // 刷新统计信息
} else {
throw new Error(response.error || '清理所有缓存失败');
}
} catch (err) {
error.value = err instanceof Error ? err.message : '清理所有缓存失败';
console.error('清理所有缓存失败:', err);
} finally {
loading.value = false;
}
};
// 组件挂载时获取统计信息
onMounted(() => {
fetchCacheStats();
});
</script>
<style scoped>
.cache-manager {
background: white;
border-radius: 0.75rem;
padding: 1.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
.cache-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.5rem;
}
.cache-header h3 {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: #1f2937;
}
.cache-actions {
display: flex;
gap: 0.75rem;
}
.btn {
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-weight: 500;
text-decoration: none;
transition: all 0.2s;
border: none;
cursor: pointer;
font-size: 0.875rem;
}
.btn:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.btn-secondary {
background: #f3f4f6;
color: #374151;
border: 1px solid #d1d5db;
}
.btn-secondary:hover:not(:disabled) {
background: #e5e7eb;
}
.btn-warning {
background: #fef3c7;
color: #92400e;
border: 1px solid #f59e0b;
}
.btn-warning:hover:not(:disabled) {
background: #fde68a;
}
.btn-danger {
background: #fee2e2;
color: #991b1b;
border: 1px solid #ef4444;
}
.btn-danger:hover:not(:disabled) {
background: #fecaca;
}
.error-message {
background: #fee2e2;
color: #991b1b;
padding: 0.75rem;
border-radius: 0.375rem;
margin-bottom: 1rem;
border: 1px solid #fecaca;
}
.cache-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.stat-item {
background: #f8fafc;
padding: 1rem;
border-radius: 0.5rem;
border: 1px solid #e2e8f0;
}
.stat-label {
font-size: 0.875rem;
color: #64748b;
margin-bottom: 0.5rem;
font-weight: 500;
}
.stat-value {
font-size: 1.125rem;
font-weight: 600;
color: #1f2937;
display: flex;
align-items: center;
gap: 0.5rem;
}
.usage-bar {
flex: 1;
height: 0.5rem;
background: #e2e8f0;
border-radius: 0.25rem;
overflow: hidden;
}
.usage-fill {
height: 100%;
background: #10b981;
transition: width 0.3s ease;
}
.usage-fill.usage-high {
background: #ef4444;
}
.usage-text {
font-size: 0.875rem;
color: #6b7280;
min-width: 3rem;
text-align: right;
}
.loading {
text-align: center;
color: #6b7280;
padding: 2rem;
}
@media (max-width: 768px) {
.cache-header {
flex-direction: column;
gap: 1rem;
align-items: stretch;
}
.cache-actions {
flex-direction: column;
}
.cache-stats {
grid-template-columns: 1fr;
}
}
</style>
+170 -10
View File
@@ -56,8 +56,6 @@
</div>
</div>
<!-- 作品列表 -->
<div class="artworks-section">
<div class="section-header">
@@ -102,11 +100,23 @@
<button @click="goToPage(currentPage + 1)" class="page-btn" :disabled="currentPage >= totalPages">
下一页
<svg viewBox="0 0 24 24" fill="currentColor" class="page-icon">
<path d="M8.59 16.59L10 18l6-6-6-6-1.41 1.41L13.17 12z" />
<path d="8.59 16.59L10 18l6-6-6-6-1.41 1.41L13.17 12z" />
</svg>
</button>
</div>
<!-- 跳转到指定页面 -->
<div v-if="totalPages > 1 && artworks.length > 0" class="jump-to-page">
<div class="jump-input-group">
<label for="jumpPage">跳转到:</label>
<input v-model="jumpPageInput" type="number" id="jumpPage" class="jump-input" :min="1" :max="totalPages"
placeholder="页码" @keyup.enter="handleJumpToPage" />
<button @click="handleJumpToPage" class="jump-btn" :disabled="!jumpPageInput || jumping">
{{ jumping ? '跳转中...' : '跳转' }}
</button>
</div>
</div>
<!-- 页面信息 -->
<div v-if="totalPages > 1 && artworks.length > 0" class="page-info">
<span> {{ currentPage }} {{ totalPages }} </span>
@@ -247,7 +257,7 @@ const fetchArtistInfo = async () => {
};
// 获取作者作品
const fetchArtworks = async (page = 1) => {
const fetchArtworks = async (page = 1, isJumpToPage = false) => {
if (!artist.value) return;
const cacheKey = getCacheKey(artworkType.value, page);
@@ -313,8 +323,14 @@ const fetchArtworks = async (page = 1) => {
throw new Error(response.error || '获取作品列表失败');
}
} catch (err) {
error.value = err instanceof Error ? err.message : '获取作品列表失败';
console.error('获取作品列表失败:', err);
// 只有在跳转到指定页面失败时才显示错误
if (isJumpToPage) {
error.value = `跳转失败:无法跳转到第 ${page}`;
} else {
error.value = err instanceof Error ? err.message : '获取作品列表失败';
}
} finally {
artworksLoading.value = false;
}
@@ -323,12 +339,30 @@ const fetchArtworks = async (page = 1) => {
// 处理类型切换
const handleTypeChange = () => {
currentPage.value = 1;
// 清除URL中的页码参数
router.push({
query: {
...route.query,
page: undefined
}
});
fetchArtworks(1);
};
// 跳转到指定页面
const goToPage = (page: number) => {
if (page < 1 || page > totalPages.value || page === currentPage.value) return;
if (page < 1 || page === currentPage.value) return;
// 更新URL参数
router.push({
query: {
...route.query,
page: page.toString()
}
});
fetchArtworks(page);
};
@@ -407,22 +441,63 @@ const clearError = () => {
error.value = null;
};
// 跳转到指定页面输入框
const jumpPageInput = ref<string | number>('');
const jumping = ref(false);
// 处理跳转到指定页面
const handleJumpToPage = async () => {
const page = parseInt(jumpPageInput.value as string);
if (isNaN(page) || page < 1) {
error.value = '请输入有效的页码';
return;
}
jumping.value = true;
jumpPageInput.value = ''; // 清空输入框
// 更新URL参数
router.push({
query: {
...route.query,
page: page.toString()
}
});
try {
await fetchArtworks(page, true);
} finally {
jumping.value = false;
}
};
// 监听路由变化
watch(() => route.params.id, () => {
// 清除缓存并重新加载
clearCache();
fetchArtistInfo();
// 检查是否有返回的页面信息
// 检查是否有返回的页面信息或指定的页码
const returnPage = parseInt(route.query.page as string);
if (returnPage && returnPage > 0) {
currentPage.value = returnPage;
fetchArtworks(returnPage);
fetchArtworks(returnPage, true);
} else {
fetchArtworks(1);
}
});
// 监听URL查询参数变化
watch(() => route.query.page, (newPage) => {
if (newPage && artist.value) {
const page = parseInt(newPage as string);
if (page > 0 && page !== currentPage.value) {
currentPage.value = page;
fetchArtworks(page, true);
}
}
});
// 组件卸载时清理缓存
onUnmounted(() => {
clearCache();
@@ -431,11 +506,11 @@ onUnmounted(() => {
onMounted(async () => {
await fetchArtistInfo();
// 检查是否有返回的页面信息
// 检查是否有返回的页面信息或指定的页码
const returnPage = parseInt(route.query.page as string);
if (returnPage && returnPage > 0) {
currentPage.value = returnPage;
await fetchArtworks(returnPage);
await fetchArtworks(returnPage, true);
} else {
await fetchArtworks(1);
}
@@ -749,6 +824,73 @@ onMounted(async () => {
font-size: 0.875rem;
}
.jump-to-page {
display: flex;
justify-content: center;
margin-top: 1rem;
margin-bottom: 1rem;
padding: 1rem;
background: #f9fafb;
border-radius: 0.5rem;
border: 1px solid #e5e7eb;
}
.jump-input-group {
display: flex;
align-items: center;
gap: 0.75rem;
}
.jump-input-group label {
font-size: 0.875rem;
color: #374151;
font-weight: 500;
white-space: nowrap;
}
.jump-input {
padding: 0.5rem 0.75rem;
border: 1px solid #d1d5db;
border-radius: 0.375rem;
background: white;
font-size: 0.875rem;
color: #374151;
min-width: 80px;
text-align: center;
transition: border-color 0.2s;
}
.jump-input:focus {
outline: none;
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.jump-btn {
padding: 0.5rem 1rem;
border-radius: 0.375rem;
background: #3b82f6;
color: white;
font-weight: 600;
text-decoration: none;
transition: all 0.2s;
border: none;
cursor: pointer;
font-size: 0.875rem;
min-width: 80px;
}
.jump-btn:hover:not(:disabled) {
background: #2563eb;
transform: translateY(-1px);
}
.jump-btn:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
}
@media (max-width: 768px) {
.container {
padding: 0 1rem;
@@ -806,5 +948,23 @@ onMounted(async () => {
gap: 0.5rem;
text-align: center;
}
.jump-to-page {
flex-direction: column;
gap: 0.5rem;
}
.jump-input-group {
flex-direction: column;
align-items: flex-start;
}
.jump-input {
width: 100%;
}
.jump-btn {
width: 100%;
}
}
</style>
+148 -10
View File
@@ -7,16 +7,19 @@
<div class="search-form">
<!-- 搜索类型选择 -->
<div class="search-type-tabs">
<button @click="searchMode = 'keyword'" class="tab-btn" :class="{ active: searchMode === 'keyword' }">
<button @click="handleSearchModeChange('keyword')" class="tab-btn"
:class="{ active: searchMode === 'keyword' }">
关键词搜索
</button>
<button @click="searchMode = 'tags'" class="tab-btn" :class="{ active: searchMode === 'tags' }">
<button @click="handleSearchModeChange('tags')" class="tab-btn" :class="{ active: searchMode === 'tags' }">
标签搜索
</button>
<button @click="searchMode = 'artwork'" class="tab-btn" :class="{ active: searchMode === 'artwork' }">
<button @click="handleSearchModeChange('artwork')" class="tab-btn"
:class="{ active: searchMode === 'artwork' }">
作品ID
</button>
<button @click="searchMode = 'artist'" class="tab-btn" :class="{ active: searchMode === 'artist' }">
<button @click="handleSearchModeChange('artist')" class="tab-btn"
:class="{ active: searchMode === 'artist' }">
作者ID
</button>
</div>
@@ -81,20 +84,20 @@
</div>
<div class="search-filters">
<select v-model="searchType" class="filter-select">
<select v-model="searchType" @change="updateFiltersInUrl" class="filter-select">
<option value="all">全部类型</option>
<option value="art">插画</option>
<option value="manga">漫画</option>
<option value="novel">小说</option>
</select>
<select v-model="searchSort" class="filter-select">
<select v-model="searchSort" @change="updateFiltersInUrl" class="filter-select">
<option value="date_desc">最新</option>
<option value="date_asc">最旧</option>
<option value="popular_desc">最受欢迎</option>
</select>
<select v-model="searchDuration" class="filter-select">
<select v-model="searchDuration" @change="updateFiltersInUrl" class="filter-select">
<option value="all">全部时间</option>
<option value="within_last_day">最近一天</option>
<option value="within_last_week">最近一周</option>
@@ -203,6 +206,14 @@ const handleSearch = async () => {
return;
}
// 更新URL参数
const query: any = { ...route.query };
query.keyword = searchKeyword.value.trim();
query.mode = 'keyword';
// 清除标签相关参数
delete query.tags;
router.push({ query });
try {
loading.value = true;
error.value = null;
@@ -299,6 +310,15 @@ const handleArtworkSearch = () => {
return;
}
// 更新URL参数
const query: any = { ...route.query };
query.artworkId = idStr;
query.mode = 'artwork';
// 清除其他搜索参数
delete query.keyword;
delete query.tags;
router.push({ query });
router.push(`/artwork/${id}`);
};
@@ -316,6 +336,15 @@ const handleArtistSearch = () => {
return;
}
// 更新URL参数
const query: any = { ...route.query };
query.artistId = idStr;
query.mode = 'artist';
// 清除其他搜索参数
delete query.keyword;
delete query.tags;
router.push({ query });
// 切换到作者搜索模式并跳转
searchMode.value = 'artist';
router.push(`/artist/${id}`);
@@ -327,11 +356,80 @@ const addTag = () => {
if (tag && !searchTags.value.includes(tag)) {
searchTags.value.push(tag);
tagInput.value = '';
// 更新URL参数
updateSearchTagsInUrl();
}
};
const removeTag = (index: number) => {
searchTags.value.splice(index, 1);
// 更新URL参数
updateSearchTagsInUrl();
};
// 更新URL中的搜索标签参数
const updateSearchTagsInUrl = () => {
const query: any = { ...route.query };
if (searchTags.value.length > 0) {
query.tags = searchTags.value;
query.mode = 'tags';
} else {
// 如果没有标签,清除相关参数
delete query.tags;
delete query.mode;
}
router.push({ query });
};
// 更新搜索过滤器到URL
const updateFiltersInUrl = () => {
const query: any = { ...route.query };
if (searchType.value !== 'all') query.type = searchType.value;
else delete query.type;
if (searchSort.value !== 'date_desc') query.sort = searchSort.value;
else delete query.sort;
if (searchDuration.value !== 'all') query.duration = searchDuration.value;
else delete query.duration;
router.push({ query });
};
// 处理搜索模式切换
const handleSearchModeChange = (mode: 'keyword' | 'tags' | 'artwork' | 'artist') => {
searchMode.value = mode;
// 清除其他模式的输入
if (mode !== 'keyword') searchKeyword.value = '';
if (mode !== 'tags') {
searchTags.value = [];
tagInput.value = '';
}
if (mode !== 'artwork') artworkId.value = '';
if (mode !== 'artist') artistId.value = '';
// 更新URL参数,清除不相关的参数
const query: any = { ...route.query };
query.mode = mode;
// 根据模式清除不相关的参数
if (mode !== 'keyword') delete query.keyword;
if (mode !== 'tags') {
delete query.tags;
delete query.tag;
}
if (mode !== 'artwork') delete query.artworkId;
if (mode !== 'artist') delete query.artistId;
router.push({ query });
};
const handleTagSearch = async () => {
@@ -339,6 +437,14 @@ const handleTagSearch = async () => {
return;
}
// 更新URL参数
const query: any = { ...route.query };
query.tags = searchTags.value;
query.mode = 'tags';
// 清除关键词相关参数
delete query.keyword;
router.push({ query });
try {
loading.value = true;
error.value = null;
@@ -384,13 +490,37 @@ const handleArtistDownload = (artist: any) => {
// 监听路由变化,处理URL参数
watch(() => route.query, () => {
const urlMode = route.query.mode as string;
const urlKeyword = route.query.keyword as string;
const urlTag = route.query.tag as string;
const urlTags = route.query.tags;
const urlType = route.query.type as string;
const urlSort = route.query.sort as string;
const urlDuration = route.query.duration as string;
const urlArtworkId = route.query.artworkId as string;
const urlArtistId = route.query.artistId as string;
// 恢复搜索模式
if (urlMode) {
searchMode.value = urlMode as 'keyword' | 'tags' | 'artwork' | 'artist';
}
// 恢复关键词
if (urlKeyword) {
searchKeyword.value = urlKeyword;
}
// 恢复作品ID
if (urlArtworkId) {
artworkId.value = urlArtworkId;
}
// 恢复作者ID
if (urlArtistId) {
artistId.value = urlArtistId;
}
// 恢复标签
if (urlMode === 'tags') {
// 自动设置标签搜索模式
searchMode.value = 'tags';
if (urlTags) {
// 处理多个标签
if (Array.isArray(urlTags)) {
@@ -411,7 +541,15 @@ watch(() => route.query, () => {
if (searchTags.value.length > 0) {
handleTagSearch();
}
} else if (urlMode === 'keyword' && urlKeyword) {
// 如果是关键词搜索模式且有关键词,自动执行搜索
handleSearch();
}
// 恢复过滤器
if (urlType) searchType.value = urlType as 'all' | 'art' | 'manga' | 'novel';
if (urlSort) searchSort.value = urlSort as 'date_desc' | 'date_asc' | 'popular_desc';
if (urlDuration) searchDuration.value = urlDuration as 'all' | 'within_last_day' | 'within_last_week' | 'within_last_month';
}, { immediate: true });
</script>