feat: 过滤条件新增文件类型多选,支持 mp3/mp4/jpg 等常见类型
部署到群晖 / deploy (push) Successful in 1m42s

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
yuming
2026-05-09 17:21:36 +08:00
parent 6fd4239ab4
commit 383c8ce17c
+160 -16
View File
@@ -377,6 +377,43 @@
background: var(--orange); color: #0d1117; font-size: 10px; font-weight: 700;
}
/* ════ 文件类型多选下拉 ════ */
.fb-field-ext { flex-shrink: 0; border-right: 1px solid var(--border); padding: 0 12px; }
.ext-pop {
display: none;
position: absolute; top: calc(100% - 4px); left: 8px; z-index: 320;
width: 320px; max-height: 360px; overflow: auto;
background: var(--surface); border: 1px solid var(--border);
border-radius: 8px; box-shadow: 0 6px 18px rgba(0,0,0,.45);
padding: 10px 12px;
}
.ext-pop.on { display: block; }
.ext-grp-title {
font-size: 11px; color: var(--muted); font-weight: 600;
margin: 8px 0 5px; letter-spacing: .5px;
}
.ext-grp-title:first-child { margin-top: 0; }
.ext-grp { display: flex; flex-wrap: wrap; gap: 5px; }
.ext-chip {
display: inline-flex; align-items: center;
padding: 3px 9px; border-radius: 12px;
border: 1px solid var(--border); background: transparent;
color: var(--muted); font-size: 11px; cursor: pointer;
transition: all .12s; user-select: none;
}
.ext-chip:hover { border-color: var(--accent); color: var(--accent); }
.ext-chip.on { border-color: var(--orange); color: var(--orange); background: rgba(210,153,34,.12); }
.ext-pop-foot {
display: flex; justify-content: space-between; align-items: center;
margin-top: 10px; padding-top: 8px; border-top: 1px solid var(--border);
font-size: 11px;
}
.ext-pop-foot a {
color: var(--muted); cursor: pointer; text-decoration: none;
}
.ext-pop-foot a:hover { color: var(--accent); }
.ext-pop-foot a.danger:hover { color: var(--red); }
/* ════ 过滤器弹窗 ════ */
.filter-mask {
display: none; position: fixed; inset: 0;
@@ -968,6 +1005,15 @@
</button>
</div>
<!-- 文件类型 -->
<div class="fb-field fb-field-ext">
<div class="fb-label">文件类型</div>
<button class="btn-filter" id="ext-open-btn" onclick="toggleExtPop(event)">
🗂 全部 <span class="filter-badge" id="ext-badge" style="display:none">0</span>
</button>
<div class="ext-pop" id="ext-pop" onclick="event.stopPropagation()"></div>
</div>
<!-- 保存路径 -->
<div class="fb-field fb-field-path">
<div class="fb-label">
@@ -1126,25 +1172,117 @@
return d.toLocaleDateString('zh-CN');
}
// ── 文件类型多选 ──
// 分组定义;扩展名要全小写(底层 file_extension 字段保证小写)
const EXT_GROUPS = [
{ name: '🎬 视频', items: ['mp4','mov','mkv','avi','webm','flv','wmv'] },
{ name: '🎵 音频', items: ['mp3','flac','wav','m4a','aac','ogg','opus'] },
{ name: '🖼 图片', items: ['jpg','png','gif','webp','heic','bmp'] },
{ name: '📄 文档', items: ['pdf','zip','rar','7z','txt','doc','docx','xlsx','pptx','epub'] },
];
let selectedExtensions = []; // 当前主表单选中的扩展名列表
function renderExtPop() {
const pop = document.getElementById('ext-pop');
if (!pop) return;
const groups = EXT_GROUPS.map(g => {
const chips = g.items.map(ext => {
const on = selectedExtensions.includes(ext) ? ' on' : '';
return `<span class="ext-chip${on}" onclick="toggleExt('${ext}')">${ext}</span>`;
}).join('');
return `<div class="ext-grp-title">${g.name}</div><div class="ext-grp">${chips}</div>`;
}).join('');
pop.innerHTML = groups + `
<div class="ext-pop-foot">
<a onclick="clearExtensions()" class="danger">清空</a>
<a onclick="closeExtPop()">完成</a>
</div>`;
}
function toggleExtPop(ev) {
if (ev) ev.stopPropagation();
const pop = document.getElementById('ext-pop');
const open = !pop.classList.contains('on');
if (open) {
renderExtPop();
pop.classList.add('on');
// 点击外部关闭:注册一次性 listener
setTimeout(() => document.addEventListener('click', closeExtPopOnOutside), 0);
} else {
closeExtPop();
}
}
function closeExtPop() {
document.getElementById('ext-pop').classList.remove('on');
document.removeEventListener('click', closeExtPopOnOutside);
}
function closeExtPopOnOutside() { closeExtPop(); }
function toggleExt(ext) {
const i = selectedExtensions.indexOf(ext);
if (i >= 0) selectedExtensions.splice(i, 1);
else selectedExtensions.push(ext);
renderExtPop();
updateExtBtn();
}
function clearExtensions() {
selectedExtensions = [];
renderExtPop();
updateExtBtn();
}
function updateExtBtn() {
const btn = document.getElementById('ext-open-btn');
const badge = document.getElementById('ext-badge');
if (!btn) return;
const n = selectedExtensions.length;
if (n > 0) {
btn.className = 'btn-filter active';
btn.firstChild.textContent = '🗂 已选 ';
badge.style.display = '';
badge.textContent = n;
} else {
btn.className = 'btn-filter';
btn.firstChild.textContent = '🗂 全部 ';
badge.style.display = 'none';
}
}
function buildExtensionFilter(exts) {
if (!exts || !exts.length) return '';
if (exts.length === 1) return `file_extension == '${exts[0]}'`;
return `file_extension == r'(${exts.join('|')})'`;
}
// 从表达式里提取已经写在 file_extension == r'(...)' / 'xxx' 里的扩展名列表
function parseExtensionsFromFilter(filter) {
if (!filter) return [];
const reg = filter.match(/file_extension\s*==\s*r'\(([^)]+)\)'/i);
if (reg) return reg[1].split('|').map(s => s.trim().toLowerCase()).filter(Boolean);
const single = filter.match(/file_extension\s*==\s*'([^']+)'/i);
if (single) return [single[1].trim().toLowerCase()];
return [];
}
// ── 加载配置 ──
function parseFilterDisplay(filter) {
if (!filter) return '';
const startM = filter.match(/message_date\s*>=\s*(\d{4}-\d{2}-\d{2})/);
const endM = filter.match(/message_date\s*<=\s*(\d{4}-\d{2}-\d{2})/);
const exts = parseExtensionsFromFilter(filter);
const parts = [];
if (startM || endM) {
if (startM && endM) parts.push('📅 ' + startM[1] + ' 至 ' + endM[1]);
else if (startM) parts.push('📅 ' + startM[1] + ' 起');
else parts.push('📅 至 ' + endM[1]);
// 检查是否还有日期之外的自定义条件
const rest = filter
.replace(/message_date\s*>=\s*\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}/gi, '')
.replace(/message_date\s*<=\s*\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}/gi, '')
.replace(/\band\b/gi, '').trim();
if (rest) parts.push('🔍 ' + rest);
} else {
parts.push('🔍 ' + filter);
}
if (exts.length) parts.push('🗂 ' + exts.join(', '));
// 检查是否还有日期/扩展名之外的自定义条件
const rest = filter
.replace(/message_date\s*>=\s*\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}/gi, '')
.replace(/message_date\s*<=\s*\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2}/gi, '')
.replace(/file_extension\s*==\s*r'\([^)]+\)'/gi, '')
.replace(/file_extension\s*==\s*'[^']+'/gi, '')
.replace(/\band\b/gi, '').trim();
if (rest) parts.push('🔍 ' + rest);
if (!parts.length) parts.push('🔍 ' + filter);
return parts.join(' · ');
}
@@ -1310,19 +1448,25 @@
}
function buildFilterFromForm(startDate, endDate) {
// 高级过滤器表达式存在时优先用它,文件类型选择被忽略(与日期同样的策略)
const custom = filterExpression || '';
if (custom) return custom;
if (!startDate) return '';
return 'message_date >= ' + startDate + ' 00:00:00' +
(endDate ? ' and message_date <= ' + endDate + ' 23:59:59' : '');
const segs = [];
if (startDate) {
segs.push('message_date >= ' + startDate + ' 00:00:00');
if (endDate) segs.push('message_date <= ' + endDate + ' 23:59:59');
}
const extSeg = buildExtensionFilter(selectedExtensions);
if (extSeg) segs.push(extSeg);
return segs.join(' and ');
}
// 把队列里的起止日期格式化成 "📅 2025-11-20 至 2026-04-23";无日期则 fallback 到原 filter
// 把队列里的起止日期/扩展名格式化展示;优先用解析 filter(能覆盖日期+文件类型)
function formatQueueDateRange(startDate, endDate, fallbackFilter) {
if (startDate) {
return '📅 ' + startDate + ' 至 ' + (endDate || startDate);
}
return fallbackFilter || '无过滤条件';
const parsed = parseFilterDisplay(fallbackFilter || '');
if (parsed) return parsed;
if (startDate) return '📅 ' + startDate + ' 至 ' + (endDate || startDate);
return '无过滤条件';
}
function renderQueue() {