Files
python_script/flask-dev-api/templates/pin_tu_search.html
2026-06-16 03:30:57 +08:00

562 lines
21 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>搜索 - {{ root_display }}</title>
<link rel="stylesheet" href="{{ url_for('static', filename='font-awesome.css') }}">
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background: #f5f5f5;
color: #333;
min-height: 100vh;
}
.search-header {
background: #fff;
border-bottom: 1px solid #e0e0e0;
padding: 14px 20px;
display: flex;
align-items: center;
gap: 10px;
}
.back-link {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
border-radius: 6px;
font-size: 13px;
color: #0078d4;
text-decoration: none;
transition: background 0.15s;
}
.back-link:hover { background: #e8f0fe; }
.search-bar {
flex: 1;
display: flex;
align-items: center;
background: #f5f5f5;
border: 1px solid #ddd;
border-radius: 8px;
padding: 8px 14px;
gap: 8px;
transition: border-color 0.2s;
max-width: 600px;
}
.search-bar:focus-within { border-color: #0078d4; }
.search-bar i { color: #999; }
.search-bar input {
flex: 1;
border: none;
background: none;
outline: none;
font-size: 14px;
color: #333;
}
.search-bar input::placeholder { color: #bbb; }
.search-bar .clear-btn {
color: #ccc;
cursor: pointer;
font-size: 14px;
transition: color 0.15s;
}
.search-bar .clear-btn:hover { color: #999; }
.search-bar .go-btn {
background: #0078d4;
color: #fff;
border: none;
border-radius: 6px;
padding: 6px 16px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: background 0.15s;
white-space: nowrap;
margin-left: 4px;
}
.search-bar .go-btn:hover { background: #006cbd; }
.search-status {
padding: 10px 20px;
font-size: 12px;
color: #999;
background: #fff;
border-bottom: 1px solid #e0e0e0;
}
.search-content {
max-width: 1000px;
margin: 0 auto;
padding: 16px 20px;
}
.result-item {
display: flex;
align-items: center;
padding: 10px 16px;
background: #fff;
border: 1px solid #e0e0e0;
border-radius: 8px;
margin-bottom: 8px;
cursor: pointer;
transition: all 0.15s;
gap: 12px;
}
.result-item:hover { border-color: #0078d4; background: #f0f8ff; }
.result-icon {
font-size: 20px;
width: 28px;
text-align: center;
flex-shrink: 0;
}
.result-icon.folder { color: #ffb300; }
.result-icon.image { color: #0078d4; }
.result-icon.video { color: #e53935; }
.result-icon.file { color: #999; }
.result-info { flex: 1; min-width: 0; }
.result-name { font-size: 14px; font-weight: 500; }
.result-name mark { background: #fff3cd; color: #333; padding: 0 1px; border-radius: 2px; }
.result-path { font-size: 12px; color: #999; margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.result-actions {
display: flex;
align-items: center;
gap: 6px;
flex-shrink: 0;
}
.result-type {
font-size: 11px;
color: #999;
background: #f0f0f0;
padding: 2px 8px;
border-radius: 4px;
flex-shrink: 0;
}
.btn-copy {
display: inline-flex;
align-items: center;
justify-content: center;
width: 30px; height: 30px;
border: 1px solid #e0e0e0;
border-radius: 6px;
background: #fff;
color: #999;
font-size: 13px;
cursor: pointer;
transition: all 0.15s;
flex-shrink: 0;
}
.btn-copy:hover { background: #f0f0f0; color: #333; border-color: #ccc; }
.btn-copy.copied { background: #e8f5e9; color: #2e7d32; border-color: #a5d6a7; }
.empty-state {
text-align: center;
padding: 80px 20px;
color: #ccc;
}
.empty-state i { font-size: 48px; margin-bottom: 16px; display: block; }
.empty-state p { font-size: 14px; }
.loading { text-align: center; padding: 40px; color: #999; font-size: 13px; }
.spinner {
display: inline-block;
width: 14px; height: 14px;
border: 2px solid rgba(0,120,212,0.2);
border-top-color: #0078d4;
border-radius: 50%;
animation: spin 0.8s linear infinite;
margin-right: 8px;
vertical-align: middle;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* 搜索历史 */
.history-section {
max-width: 1000px;
margin: 0 auto;
padding: 16px 20px 0;
}
.history-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.history-header h3 {
font-size: 13px;
color: #999;
font-weight: 600;
}
.history-clear-btn {
font-size: 12px;
color: #999;
background: none;
border: none;
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
transition: all 0.15s;
}
.history-clear-btn:hover { color: #c62828; background: #fce4ec; }
.history-list { display: flex; flex-wrap: wrap; gap: 8px; }
.history-item {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 12px;
background: #fff;
border: 1px solid #e0e0e0;
border-radius: 20px;
font-size: 13px;
color: #555;
cursor: pointer;
transition: all 0.15s;
user-select: none;
}
.history-item:hover { border-color: #0078d4; color: #0078d4; background: #f0f8ff; }
.history-item .hi-keyword { max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.history-item .hi-count {
font-size: 11px;
color: #bbb;
}
.history-item .hi-del {
color: #ccc;
font-size: 11px;
padding: 0 2px;
border-radius: 3px;
transition: all 0.15s;
line-height: 1;
}
.history-item .hi-del:hover { color: #c62828; background: #fce4ec; }
/* 自定义确认弹窗 */
.confirm-overlay {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.4);
display: flex; justify-content: center; align-items: center;
z-index: 10000; opacity: 0; pointer-events: none; transition: opacity 0.2s ease;
}
.confirm-overlay.active { opacity: 1; pointer-events: auto; }
.confirm-card {
background: #fff; border-radius: 14px; padding: 28px 32px;
width: 360px; max-width: 90vw; text-align: center;
box-shadow: 0 12px 40px rgba(0,0,0,0.2);
transform: scale(0.9); transition: transform 0.2s ease;
}
.confirm-overlay.active .confirm-card { transform: scale(1); }
.confirm-icon {
width: 48px; height: 48px; border-radius: 50%;
background: #fff3e0; color: #e65100;
display: inline-flex; align-items: center; justify-content: center;
font-size: 22px; margin-bottom: 14px;
}
.confirm-title { font-size: 16px; font-weight: 600; color: #333; margin-bottom: 8px; }
.confirm-msg { font-size: 13px; color: #888; margin-bottom: 24px; line-height: 1.5; }
.confirm-btns { display: flex; gap: 10px; justify-content: center; }
.confirm-btns button {
flex: 1; padding: 10px 20px; border: none; border-radius: 8px;
font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.15s;
}
.confirm-cancel { background: #f0f0f0; color: #666; }
.confirm-cancel:hover { background: #e0e0e0; }
.confirm-ok { background: #c62828; color: #fff; }
.confirm-ok:hover { background: #b71c1c; }
</style>
</head>
<body>
<div class="search-header">
<a href="/pin-tu/browse/{{ root_b64 }}" class="back-link"><i class="fas fa-arrow-left"></i> 返回</a>
<div class="search-bar">
<i class="fas fa-search"></i>
<input type="text" id="searchInput" value="{{ query }}" placeholder="搜索文件或文件夹..." autofocus>
<span class="clear-btn" onclick="clearSearch()"><i class="fas fa-times-circle"></i></span>
<button class="go-btn" onclick="doSearch()">搜索</button>
</div>
</div>
<!-- 搜索历史 -->
<div class="history-section" id="historySection" style="display:none;">
<div class="history-header">
<h3><i class="fas fa-clock"></i> 搜索历史</h3>
<button class="history-clear-btn" onclick="clearAllHistory()">清空历史</button>
</div>
<div class="history-list" id="historyList"></div>
</div>
<div class="search-status" id="searchStatus" style="display:none;"></div>
<div class="search-content" id="searchContent">
{% if query %}
<div class="loading"><span class="spinner"></span>搜索中...</div>
{% else %}
<div class="empty-state">
<i class="fas fa-search"></i>
<p>输入关键词搜索文件和文件夹</p>
</div>
{% endif %}
</div>
<!-- 自定义确认弹窗 -->
<div class="confirm-overlay" id="confirmOverlay">
<div class="confirm-card">
<div class="confirm-icon"><i class="fas fa-times-circle"></i></div>
<div class="confirm-title" id="confirmTitle">确认操作</div>
<div class="confirm-msg" id="confirmMsg">确定要执行此操作吗?</div>
<div class="confirm-btns">
<button class="confirm-cancel" onclick="confirmResolve(false)">取消</button>
<button class="confirm-ok" id="confirmOkBtn" onclick="confirmResolve(true)">确定</button>
</div>
</div>
</div>
<script>
let _confirmResolver = null;
function showConfirm(title, msg, okText) {
document.getElementById('confirmTitle').textContent = title || '确认操作';
document.getElementById('confirmMsg').textContent = msg || '确定要执行此操作吗?';
document.getElementById('confirmOkBtn').textContent = okText || '确定';
document.getElementById('confirmOverlay').classList.add('active');
return new Promise(function(resolve) { _confirmResolver = resolve; });
}
function confirmResolve(val) {
document.getElementById('confirmOverlay').classList.remove('active');
if (_confirmResolver) { _confirmResolver(val); _confirmResolver = null; }
}
const ROOT_B64 = '{{ root_b64 }}';
const input = document.getElementById('searchInput');
const content = document.getElementById('searchContent');
const status = document.getElementById('searchStatus');
const historySection = document.getElementById('historySection');
const historyList = document.getElementById('historyList');
let lastResults = [];
const extImage = {'JPG':1,'JPEG':1,'PNG':1,'GIF':1,'BMP':1,'TIFF':1,'WEBP':1};
const extVideo = {'MP4':1,'AVI':1,'MOV':1,'WMV':1,'FLV':1,'WEBM':1,'M4V':1,'MKV':1};
function getIcon(r) {
if (r.type === 'folder') return { cls: 'folder', icon: 'fa-folder' };
if (extImage[r.type]) return { cls: 'image', icon: 'fa-image' };
if (extVideo[r.type]) return { cls: 'video', icon: 'fa-video' };
return { cls: 'file', icon: 'fa-file' };
}
function escapeHtml(text) {
const d = document.createElement('div');
d.textContent = text;
return d.innerHTML;
}
function highlight(text, q) {
if (!q) return escapeHtml(text);
const escaped = escapeHtml(text);
const re = new RegExp('(' + q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + ')', 'gi');
return escaped.replace(re, '<mark>$1</mark>');
}
// ===== 复制 =====
function copyName(idx) {
var r = lastResults[idx];
if (!r) return;
var name = r.name;
var btn = document.querySelector('.btn-copy[data-idx="' + idx + '"]');
// 始终用 textarea + execCommand兼容 HTTP 环境
var ta = document.createElement('textarea');
ta.value = name;
ta.style.cssText = 'position:fixed;left:-9999px;top:-9999px;opacity:0;';
document.body.appendChild(ta);
ta.focus();
ta.select();
try {
document.execCommand('copy');
if (btn) {
btn.classList.add('copied');
btn.innerHTML = '<i class="fas fa-check"></i>';
setTimeout(function() {
btn.classList.remove('copied');
btn.innerHTML = '<i class="fas fa-copy"></i>';
}, 1500);
}
} catch (e) {
alert('复制失败,请手动复制');
}
document.body.removeChild(ta);
}
// ===== 搜索 =====
function clearSearch() {
input.value = '';
content.innerHTML = '<div class="empty-state"><i class="fas fa-search"></i><p>输入关键词搜索文件和文件夹</p></div>';
status.style.display = 'none';
lastResults = [];
input.focus();
loadHistory();
}
function renderResults(data, q) {
lastResults = data.results;
status.style.display = 'block';
if (!data.results.length) {
status.textContent = '未找到匹配"' + q + '"的结果';
content.innerHTML = '<div class="empty-state"><i class="fas fa-search"></i><p>未找到匹配结果</p></div>';
return;
}
status.textContent = '找到 ' + data.results.length + ' 个结果' + (data.results.length >= 50 ? '最多显示50条' : '');
historySection.style.display = 'none';
content.innerHTML = data.results.map(function(r, idx) {
var ic = getIcon(r);
var parent = r.path.includes('/') ? r.path.substring(0, r.path.lastIndexOf('/')) : '';
return '<div class="result-item" data-idx="' + idx + '">' +
'<span class="result-icon ' + ic.cls + '"><i class="fas ' + ic.icon + '"></i></span>' +
'<div class="result-info">' +
'<div class="result-name">' + highlight(r.name, q) + '</div>' +
(parent ? '<div class="result-path">' + escapeHtml(parent) + '</div>' : '') +
'</div>' +
'<div class="result-actions">' +
'<span class="result-type">' + escapeHtml(r.type) + '</span>' +
'<button class="btn-copy" data-idx="' + idx + '" title="复制名称"><i class="fas fa-copy"></i></button>' +
'</div>' +
'</div>';
}).join('');
// 绑定点击事件
content.querySelectorAll('.result-item').forEach(function(el) {
el.addEventListener('click', function(e) {
// 如果点的是复制按钮,不跳转
if (e.target.closest('.btn-copy')) return;
goResult(parseInt(this.dataset.idx));
});
});
content.querySelectorAll('.btn-copy').forEach(function(btn) {
btn.addEventListener('click', function(e) {
e.stopPropagation();
copyName(parseInt(this.dataset.idx));
});
});
}
async function doSearch() {
var q = input.value.trim();
if (!q) {
content.innerHTML = '<div class="empty-state"><i class="fas fa-search"></i><p>输入关键词搜索文件和文件夹</p></div>';
status.style.display = 'none';
lastResults = [];
loadHistory();
return;
}
content.innerHTML = '<div class="loading"><span class="spinner"></span>搜索中...</div>';
status.style.display = 'none';
historySection.style.display = 'none';
try {
var res = await fetch('/pin-tu/search/' + ROOT_B64 + '?q=' + encodeURIComponent(q));
if (!res.ok) throw new Error('HTTP ' + res.status);
var data = await res.json();
renderResults(data, q);
// 记录搜索历史
recordSearch(q, data.results.length);
} catch (e) {
lastResults = [];
content.innerHTML = '<div class="empty-state"><p style="color:#c62828;">搜索请求失败:' + escapeHtml(e.message) + '</p></div>';
}
}
function goResult(idx) {
var r = lastResults[idx];
if (!r) return;
var folder = r.type === 'folder' ? r.path : (r.path.includes('/') ? r.path.substring(0, r.path.lastIndexOf('/')) : '');
var url = '/pin-tu/browse/' + ROOT_B64;
var params = [];
if (folder) params.push('p=' + encodeURIComponent(folder));
if (extImage[r.type]) params.push('open=' + encodeURIComponent(r.name));
else if (extVideo[r.type]) params.push('openVideo=' + encodeURIComponent(r.name));
if (params.length) url += '?' + params.join('&');
window.location.href = url;
}
// ===== 搜索历史 =====
async function recordSearch(keyword, count) {
try {
await fetch('/pin-tu/api/search-history/' + ROOT_B64, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ keyword: keyword, result_count: count })
});
} catch (e) { /* 静默失败 */ }
}
async function loadHistory() {
try {
var res = await fetch('/pin-tu/api/search-history/' + ROOT_B64);
var data = await res.json();
if (!data.history || !data.history.length) {
historySection.style.display = 'none';
return;
}
historySection.style.display = 'block';
historyList.innerHTML = data.history.map(function(h) {
return '<div class="history-item" data-keyword="' + escapeHtml(h.keyword) + '">' +
'<span class="hi-keyword">' + escapeHtml(h.keyword) + '</span>' +
'<span class="hi-count">' + h.result_count + '条</span>' +
'<span class="hi-del" data-id="' + h.id + '" title="删除"><i class="fas fa-times-circle"></i></span>' +
'</div>';
}).join('');
// 绑定事件
historyList.querySelectorAll('.history-item').forEach(function(el) {
el.addEventListener('click', function(e) {
if (e.target.closest('.hi-del')) return;
input.value = this.dataset.keyword;
doSearch();
});
});
historyList.querySelectorAll('.hi-del').forEach(function(btn) {
btn.addEventListener('click', function(e) {
e.stopPropagation();
delHistory(parseInt(this.dataset.id));
});
});
} catch (e) { historySection.style.display = 'none'; }
}
function useHistory(keyword) {
input.value = keyword;
doSearch();
}
async function delHistory(id) {
try {
await fetch('/pin-tu/api/search-history/' + ROOT_B64, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids: [id] })
});
loadHistory();
} catch (e) {}
}
async function clearAllHistory() {
var ok = await showConfirm('清空搜索历史', '确定清空所有搜索历史?此操作不可撤销。', '清空');
if (!ok) return;
try {
await fetch('/pin-tu/api/search-history/' + ROOT_B64, {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ clear: true })
});
historySection.style.display = 'none';
} catch (e) {}
}
// Enter 搜索
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter') doSearch();
});
// 页面加载
{% if query %}
doSearch();
{% else %}
loadHistory();
{% endif %}
</script>
</body>
</html>