feat: 新增App图标搜索器增强版功能

引入完整的App图标搜索器增强版,包括前端页面、后端缓存系统、样式和脚本。主要功能包括实时搜索、多尺寸图标下载、图片预览、响应式设计和智能缓存机制。后端通过ImageCache类实现图片缓存,前端通过JavaScript优化搜索交互和图片加载体验。新增README.md提供详细的部署和开发指南。
This commit is contained in:
Snowz 2025-04-12 15:38:09 +08:00
commit 79b13fb280
6 changed files with 774 additions and 0 deletions

164
README.md Normal file
View File

@ -0,0 +1,164 @@
# App图标搜索器增强版
一个高效、现代的 App 图标搜索和下载工具,基于 iTunes Search API 开发。支持多种尺寸图标下载,具有响应式设计和用户友好的界面。
## 功能特点
- 实时搜索:支持即时搜索结果展示
- 多尺寸下载:支持 75px、100px、256px、512px 尺寸
- 图片预览:支持图标大图预览
- 响应式设计:完美适配移动端和桌面端
- 智能缓存:支持服务端和客户端双重缓存机制
- 用户友好:简洁的界面设计和流畅的交互体验
## 技术栈
### 前端
- HTML5 + CSS3
- 原生 JavaScript (ES6+)
- CSS Grid 和 Flexbox 布局
- Service Worker 离线缓存
### 后端
- PHP 7.4+
- iTunes Search API
- 文件系统缓存
### 服务器
- Apache/Nginx
- PHP-FPM
- 图片缓存系统
## 部署指南
### 环境要求
- PHP >= 7.4
- Apache/Nginx Web服务器
- 允许外部API访问
- PHP GD库用于图片处理
- 足够的磁盘空间用于缓存
### Apache 部署步骤
```
<VirtualHost *:80>
ServerName your-domain.com
DocumentRoot /path/to/icons-enhanced
<Directory /path/to/icons-enhanced>
Options -Indexes +FollowSymLinks
AllowOverride All
Require all granted
</Directory>
<FilesMatch "\.(jpg|jpeg|png|gif)$">
Header set Cache-Control "max-age=31536000, public"
</FilesMatch>
</VirtualHost>
```
### Nginx 部署步骤
```
server {
listen 80;
server_name your-domain.com;
root /path/to/icons-enhanced;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
location ~* \.(jpg|jpeg|png|gif)$ {
expires 1y;
add_header Cache-Control "public, no-transform";
access_log off;
}
}
```
### 配置权限
```
chmod -R 755 .
chmod -R 777 cache/images
```
## 自定义开发指南
### 前端定制
1. 样式修改
- 主题颜色:修改 css/styles.css 中的 CSS 变量
- 布局调整:修改 Grid 和 Flexbox 相关样式
- 响应式断点:调整 media queries 中的尺寸
2. 交互优化
- 搜索逻辑:修改 js/scripts.js 中的防抖函数
- 动画效果:调整 transition 和 transform 相关属性
- 预览功能:自定义模态框行为
### 后端定制
1. 缓存策略
- 修改缓存时间:调整 ImageCache.php 中的 cacheDuration
- 自定义缓存目录:更改 cacheDir 配置
- 添加缓存清理机制
2. API 调整
- 修改搜索参数:调整 iTunes API 请求参数
- 添加错误处理:完善异常处理机制
- 扩展搜索源:添加其他图标源支持
### 性能优化
1. 图片优化
- 实现图片懒加载
- 添加图片压缩功能
- 使用 WebP 格式支持
2. 缓存优化
- 实现浏览器缓存
- 添加 CDN 支持
- 优化缓存策略
## 常见问题
1. 图片加载失败
- 检查服务器权限设置
- 验证缓存目录权限
- 确认外部API访问是否正常
2. 搜索响应慢
- 调整防抖时间
- 优化数据请求策略
- 检查服务器性能
3. 移动端适配问题
- 检查视口设置
- 调整响应式断点
- 优化触摸交互
## 维护建议
1. 定期维护
- 清理过期缓存
- 更新依赖版本
- 检查API可用性
2. 性能监控
- 监控服务器负载
- 跟踪API响应时间
- 分析用户使用数据
## 贡献指南
欢迎提交 Pull Request 或 Issue。在提交之前请确保
1. 代码符合现有风格
2. 添加必要的注释和文档
3. 测试所有功能正常
## 许可证
MIT License - 详见根目录 LICENSE 文件
## 作者
[Snowz]
## 更新日志
### v1.0.0 (2025-03-31)
- 初始版本发布
- 基础搜索功能
- 多尺寸下载支持
### v1.1.0 (计划中)
- 添加图片懒加载
- 优化搜索体验
- 改进缓存机制

32
cache/ImageCache.php vendored Normal file
View File

@ -0,0 +1,32 @@
<?php
class ImageCache {
private $cacheDir;
private $cacheDuration = 604800; // 7天缓存
public function __construct() {
$this->cacheDir = __DIR__ . '/images/';
if (!file_exists($this->cacheDir)) {
mkdir($this->cacheDir, 0777, true);
}
}
public function getCachedImage($url) {
$cacheFile = $this->cacheDir . md5($url);
if (file_exists($cacheFile) && (time() - filemtime($cacheFile) < $this->cacheDuration)) {
return file_get_contents($cacheFile);
}
$imageContent = file_get_contents($url);
if ($imageContent !== false) {
file_put_contents($cacheFile, $imageContent);
return $imageContent;
}
return false;
}
public function getCacheUrl($url) {
return 'cache/image.php?url=' . urlencode($url);
}
}

37
cache/image.php vendored Normal file
View File

@ -0,0 +1,37 @@
<?php
require_once 'ImageCache.php';
$url = isset($_GET['url']) ? $_GET['url'] : '';
if (empty($url)) {
header("HTTP/1.0 404 Not Found");
exit;
}
$imageCache = new ImageCache();
$image = $imageCache->getCachedImage($url);
if ($image === false) {
header("HTTP/1.0 404 Not Found");
exit;
}
$extension = pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION);
switch(strtolower($extension)) {
case 'jpg':
case 'jpeg':
header('Content-Type: image/jpeg');
break;
case 'png':
header('Content-Type: image/png');
break;
case 'gif':
header('Content-Type: image/gif');
break;
default:
header('Content-Type: image/jpeg');
}
header('Cache-Control: public, max-age=31536000');
header('Expires: ' . gmdate('D, d M Y H:i:s \G\M\T', time() + 31536000));
echo $image;

317
css/styles.css Normal file
View File

@ -0,0 +1,317 @@
:root {
--primary-color: #4a90e2;
--secondary-color: #2c3e50;
--background-color: #f5f6fa;
--card-background: #ffffff;
--text-color: #2c3e50;
--border-radius: 12px;
--transition: all 0.3s ease;
--shadow-sm: 0 2px 4px rgba(0, 0, 0, 0.1);
--shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 8px 15px rgba(0, 0, 0, 0.1);
}
/* 基础样式 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background-color: var(--background-color);
color: var(--text-color);
line-height: 1.6;
}
/* 布局容器 */
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
/* 搜索区域 */
.search-container {
text-align: center;
margin-bottom: 3rem;
width: 100%;
}
h1 {
font-size: 2.5rem;
color: var(--primary-color);
margin-bottom: 1.5rem;
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
}
/* 搜索区域 */
.search-box {
display: flex;
max-width: 600px;
margin: 0 auto 1rem;
position: relative;
}
input[type="text"] {
flex: 1;
padding: 0.8rem 1rem;
font-size: 1.1rem;
border: 2px solid #e1e1e1;
border-radius: var(--border-radius) 0 0 var(--border-radius);
transition: var(--transition);
outline: none;
}
input[type="text"]:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 3px rgba(74, 144, 226, 0.1);
}
button[type="submit"] {
padding: 0.8rem 1.5rem;
background-color: var(--primary-color);
color: white;
border: none;
border-radius: 0 var(--border-radius) var(--border-radius) 0;
cursor: pointer;
transition: var(--transition);
min-width: 60px;
}
button[type="submit"]:hover {
background-color: #357abd;
}
button[type="submit"]:active {
transform: translateY(1px);
}
.search-options {
margin-top: 1rem;
display: flex;
justify-content: center;
}
select {
padding: 0.5rem;
border-radius: var(--border-radius);
border: 2px solid #e1e1e1;
background-color: white;
cursor: pointer;
}
/* 图标卡片 */
.icon-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 2rem;
margin-top: 2rem;
}
.icon-card {
background: var(--card-background);
border-radius: 16px;
padding: 1.5rem;
box-shadow: var(--shadow-md);
transition: var(--transition);
text-align: center;
display: flex;
flex-direction: column;
gap: 1rem;
}
.icon-card:hover {
transform: translateY(-5px);
box-shadow: var(--shadow-lg);
}
/* 图标图片 */
.icon-image {
text-align: center;
margin-bottom: 1rem;
}
.icon-image img {
width: 100px;
height: 100px;
border-radius: 22px;
cursor: pointer;
transition: transform 0.3s ease;
}
.icon-image img:hover {
transform: scale(1.05);
}
/* 应用名称 */
.app-name {
font-size: 1.1rem;
margin: 0.5rem 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: var(--text-color);
}
/* 下载按钮 */
.download-options {
display: flex;
flex-direction: column;
gap: 6px;
margin-top: 12px;
width: 100%;
padding: 0 2px;
}
.download-options-row {
display: flex;
gap: 6px;
width: 100%;
}
.size-btn {
background-color: var(--primary-color);
color: white;
text-decoration: none;
border-radius: 10px;
font-size: 0.85rem;
text-align: center;
transition: var(--transition);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
border: none;
box-shadow: var(--shadow-sm);
}
.size-btn:hover {
background-color: #357abd;
transform: translateY(-2px);
box-shadow: var(--shadow-md);
}
.size-btn:active {
transform: translateY(0);
}
.size-btn-small {
padding: 8px 0;
min-height: 65px;
max-width: calc(33.33% - 4px);
}
.size-btn-large {
padding: 8px 0;
min-height: 45px;
}
/* 模态框 */
.modal {
display: none;
position: fixed;
z-index: 1000;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.9);
}
/* 返回顶部按钮 */
#back-to-top {
position: fixed;
bottom: 20px;
right: 20px;
display: none;
padding: 1rem;
border-radius: 50%;
background-color: var(--primary-color);
color: white;
border: none;
cursor: pointer;
transition: var(--transition);
}
/* 响应式设计 */
@media (max-width: 768px) {
.container {
padding: 1rem;
}
h1 {
font-size: 2rem;
}
.icon-grid {
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 1rem;
}
.icon-card {
padding: 1.2rem;
gap: 0.8rem;
}
.size-btn-small {
min-height: 50px;
padding: 6px 0;
}
.size-btn-large {
min-height: 38px;
padding: 6px 0;
}
.size-label {
font-size: 0.85rem;
}
.size-desc {
font-size: 0.65rem;
}
}
/* 页脚样式 */
footer {
background-color: var(--secondary-color);
color: white;
padding: 2rem 0;
margin-top: 3rem;
width: 100%;
}
.footer-content {
max-width: 1200px;
margin: 0 auto;
text-align: center;
padding: 0 1rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.social-links {
display: flex;
justify-content: center;
gap: 1rem;
}
.social-links a {
color: white;
font-size: 1.5rem;
text-decoration: none;
transition: var(--transition);
padding: 0.5rem;
}
.social-links a:hover {
color: var(--primary-color);
}

119
index.php Normal file
View File

@ -0,0 +1,119 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>App图标搜索器 - 增强版</title>
<meta name="description" content="一款可以在线搜索APP图标并下载小工具">
<meta name="keywords" content="photo8,App图标搜索器">
<link rel="shortcut icon" href="https://api.photo8.site/path/img/icon-64@3x.png" type="image/png">
<link rel="stylesheet" href="css/styles.css">
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
</head>
<body>
<div class="container">
<div class="search-container">
<h1><i class="fas fa-search"></i> App图标搜索器</h1>
<form method="GET" action="" id="searchForm">
<div class="search-box">
<input type="text" name="term" id="searchInput" placeholder="输入APP名字..." value="<?php echo htmlspecialchars($searchTerm ?? ''); ?>">
<button type="submit"><i class="fas fa-search"></i></button>
</div>
<div class="search-options">
<select name="limit" id="limitSelect">
<option value="20">20个结果</option>
<option value="50">50个结果</option>
<option value="100">100个结果</option>
</select>
</div>
</form>
</div>
<div class="icon-grid" id="iconGrid">
<?php
function fetchAppData($term, $limit, $offset) {
$url = "https://itunes.apple.com/search?term=" . urlencode($term) . "&country=CN&entity=software&limit=" . $limit . "&offset=" . $offset;
$json = @file_get_contents($url);
if ($json === false) {
return [];
}
$data = json_decode($json, true);
return $data['results'] ?? [];
}
$searchTerm = isset($_GET['term']) ? trim($_GET['term']) : '';
$limit = isset($_GET['limit']) ? (int)$_GET['limit'] : 20;
if ($searchTerm) {
$apps = fetchAppData($searchTerm, $limit, 0);
if (!empty($apps)) {
foreach ($apps as $app) {
$iconSizes = [
['size' => '75x75bb', 'label' => '75px', 'desc' => '小图'],
['size' => '100x100bb', 'label' => '100px', 'desc' => '标准'],
['size' => '256x256bb', 'label' => '256px', 'desc' => '高清'],
['size' => '512x512bb', 'label' => '512px', 'desc' => '超清']
];
echo '<div class="icon-card">';
require_once 'cache/ImageCache.php';
$imageCache = new ImageCache();
// 启用图片缓存
echo '<div class="icon-image">';
$cachedImageUrl = $imageCache->getCacheUrl($app['artworkUrl100']);
echo '<img src="' . htmlspecialchars($cachedImageUrl) . '" alt="' . htmlspecialchars($app['trackName']) . '"
data-high-res="' . str_replace('100x100bb', '512x512bb', $cachedImageUrl) . '">';
echo '</div>';
echo '<h3 class="app-name">' . htmlspecialchars($app['trackName']) . '</h3>';
echo '<div class="download-options">';
echo '<div class="download-options-row">';
// 前三个按钮 (75px, 100px, 256px)
foreach (array_slice($iconSizes, 0, 3) as $size) {
$sizeUrl = str_replace('100x100bb', $size['size'], $app['artworkUrl100']);
echo '<a href="' . htmlspecialchars($sizeUrl) . '" class="size-btn size-btn-small" target="_blank">';
echo '<span class="size-label">' . $size['label'] . '</span>';
echo '<span class="size-desc">' . $size['desc'] . '</span>';
echo '</a>';
}
echo '</div>';
// 最后一个按钮 (512px)
$lastSize = end($iconSizes);
$lastSizeUrl = str_replace('100x100bb', $lastSize['size'], $app['artworkUrl100']);
echo '<a href="' . htmlspecialchars($lastSizeUrl) . '" class="size-btn size-btn-large" target="_blank">';
echo '<span class="size-label">' . $lastSize['label'] . '</span>';
echo '<span class="size-desc">' . $lastSize['desc'] . '</span>';
echo '</a>';
echo '</div>';
echo '</div>';
}
} else {
echo '<div class="no-results"><i class="fas fa-exclamation-circle"></i><p>没有找到相关应用</p></div>';
}
}
?>
</div>
</div>
<div id="imagePreview" class="modal">
<span class="close">&times;</span>
<img class="modal-content" id="previewImage">
<div id="imageCaption"></div>
</div>
<?php if ($searchTerm): ?>
<footer>
<div class="footer-content">
<p>© 2024 PHOTO8 - App图标搜索器</p>
<div class="social-links">
<a href="#" title="微博"><i class="fab fa-weibo"></i></a>
<a href="#" title="微信"><i class="fab fa-weixin"></i></a>
</div>
</div>
</footer>
<?php endif; ?>
<button id="back-to-top" title="返回顶部"><i class="fas fa-arrow-up"></i></button>
<script src="js/scripts.js"></script>
</body>
</html>

105
js/scripts.js Normal file
View File

@ -0,0 +1,105 @@
document.addEventListener('DOMContentLoaded', function() {
const backToTop = document.getElementById('back-to-top');
const modal = document.getElementById('imagePreview');
const modalImg = document.getElementById('previewImage');
const closeBtn = document.getElementsByClassName('close')[0];
const searchInput = document.getElementById('searchInput');
const iconGrid = document.getElementById('iconGrid');
// 返回顶部按钮
window.onscroll = function() {
if (document.body.scrollTop > 20 || document.documentElement.scrollTop > 20) {
backToTop.style.display = 'block';
} else {
backToTop.style.display = 'none';
}
};
backToTop.onclick = function() {
window.scrollTo({
top: 0,
behavior: 'smooth'
});
};
// 图标预览模态框
document.querySelectorAll('.icon-image img').forEach(img => {
img.onclick = function() {
modal.style.display = 'block';
modalImg.src = this.getAttribute('data-high-res');
modalImg.alt = this.alt;
};
});
closeBtn.onclick = function() {
modal.style.display = 'none';
};
window.onclick = function(event) {
if (event.target == modal) {
modal.style.display = 'none';
}
};
// 搜索输入优化
// 搜索防抖功能
function debounce(func, wait) {
let timeout;
return function (...args) {
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(this, args), wait);
};
}
document.addEventListener('DOMContentLoaded', function() {
const searchForm = document.getElementById('searchForm');
const searchInput = document.getElementById('searchInput');
// 防止表单自动提交
searchForm.addEventListener('submit', function(e) {
e.preventDefault();
if (searchInput.value.trim()) {
this.submit();
}
});
// 输入防抖
const debouncedSearch = debounce(function(value) {
if (value.trim().length >= 2) {
searchForm.submit();
}
}, 800);
// 监听输入事件
searchInput.addEventListener('input', function(e) {
const value = e.target.value;
debouncedSearch(value);
});
// 添加加载状态
searchForm.addEventListener('submit', function() {
const button = this.querySelector('button[type="submit"]');
button.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
button.disabled = true;
// 2秒后恢复按钮状态如果请求完成会被新页面覆盖
setTimeout(() => {
button.innerHTML = '<i class="fas fa-search"></i>';
button.disabled = false;
}, 2000);
});
});
// 图标加载动画
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('fade-in');
}
});
});
document.querySelectorAll('.icon-card').forEach(card => {
observer.observe(card);
});
});