diff --git a/module/templates/index.html b/module/templates/index.html index 8b30966..631f7f7 100644 --- a/module/templates/index.html +++ b/module/templates/index.html @@ -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 @@ + +
+
文件类型
+ +
+
+
@@ -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 `${ext}`; + }).join(''); + return `
${g.name}
${chips}
`; + }).join(''); + pop.innerHTML = groups + ` +
+ 清空 + 完成 +
`; + } + + 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() {