1235 lines
115 KiB
JavaScript
1235 lines
115 KiB
JavaScript
(function() {
|
||
if (window.__SpiderUI_V44_Init) return;
|
||
window.__SpiderUI_V44_Init = true;
|
||
|
||
// =========================================================================
|
||
// === [BLOCK 1: 核心样式与UI模板] ===
|
||
// =========================================================================
|
||
const hostStyle = document.createElement('style');
|
||
hostStyle.innerHTML = `.spider-highlight { outline: 3px solid #ff3b30 !important; outline-offset: -3px; background: rgba(255, 59, 48, 0.15) !important; box-shadow: inset 0 0 0 2000px rgba(255,59,48,0.1) !important; cursor: crosshair !important; transition: all 0.1s; }`;
|
||
document.documentElement.appendChild(hostStyle);
|
||
|
||
const host = document.createElement('div');
|
||
host.id = '__spider_ui_host';
|
||
host.style.cssText = 'position: fixed !important; top: 0 !important; left: 0 !important; width: 100vw !important; height: 100vh !important; z-index: 2147483647 !important; pointer-events: none !important;';
|
||
document.documentElement.appendChild(host);
|
||
const shadow = host.attachShadow({ mode: 'open' });
|
||
|
||
const svgIcons = {
|
||
close: '<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2.5" fill="none"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>',
|
||
dock: '<svg viewBox="0 0 24 24" width="16" height="16" stroke="currentColor" stroke-width="2.5" fill="none"><path d="M12 3v18M8 7l4-4 4 4M8 17l4 4 4-4"/></svg>',
|
||
clear: '<svg viewBox="0 0 24 24" width="14" height="14" stroke="#ff3b30" stroke-width="2" fill="none"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg>',
|
||
edit: '<svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" stroke-width="2" fill="none"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"></path><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"></path></svg>',
|
||
magic: '<svg viewBox="0 0 24 24" width="14" height="14" stroke="#007aff" stroke-width="2" fill="none"><path d="M21 16.5l-5.14-5.14-2.83 2.83L18.17 19.33A2 2 0 0 0 21 16.5z"></path><path d="M15 9l-4-4L2 14l4 4z"></path></svg>',
|
||
play: '<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2" fill="currentColor"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg>',
|
||
download: '<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2" fill="none"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path><polyline points="7 10 12 15 17 10"></polyline><line x1="12" y1="15" x2="12" y2="3"></line></svg>',
|
||
copy: '<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2" fill="none"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>',
|
||
plus: '<svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" stroke-width="3" fill="none"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>',
|
||
target: '<svg viewBox="0 0 24 24" width="12" height="12" stroke="currentColor" stroke-width="2" fill="none"><circle cx="12" cy="12" r="10"></circle><circle cx="12" cy="12" r="6"></circle><circle cx="12" cy="12" r="2"></circle></svg>'
|
||
};
|
||
|
||
const style = document.createElement('style');
|
||
style.innerHTML = `
|
||
:host { font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif; font-size: 13px; color: #333; line-height: 1.4; }
|
||
* { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; outline: none; list-style: none; }
|
||
#sp_fab { pointer-events: auto; position: fixed !important; right: 20px !important; bottom: 80px !important; width: 48px; height: 48px; background: #007aff; color: #fff; border-radius: 24px; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold; box-shadow: 0 4px 12px rgba(0,122,255,0.4); cursor: pointer; transition: 0.2s; z-index: 2147483647; }
|
||
#sp_fab:active { transform: scale(0.9); }
|
||
#sp_panel { pointer-events: auto; position: fixed !important; bottom: 0 !important; left: 0 !important; right: 0 !important; max-height: 75vh; background: #f2f2f7; border-radius: 16px 16px 0 0; box-shadow: 0 -5px 20px rgba(0,0,0,0.2); display: none; flex-direction: column; transform: translateY(100%); transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1), border-radius 0.3s; z-index: 2147483647; }
|
||
#sp_panel.show { transform: translateY(0); }
|
||
#sp_panel.pos-top { bottom: auto !important; top: 0 !important; transform: translateY(-100%); border-radius: 0 0 16px 16px; box-shadow: 0 5px 20px rgba(0,0,0,0.2); }
|
||
#sp_panel.pos-top.show { transform: translateY(0); }
|
||
#sp_panel.pos-top .sp_header { border-radius: 0 0 16px 16px; padding-top: calc(12px + env(safe-area-inset-top)); }
|
||
#sp_panel.pos-top .sp_body { padding-bottom: 16px; }
|
||
.sp_header { background: #fff; padding: 12px 16px 8px; border-radius: 16px 16px 0 0; }
|
||
.sp_title_bar { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
|
||
.sp_title_bar span { font-size: 16px; font-weight: 700; color: #000; }
|
||
.sp_title_bar span.picking { color: #007aff; }
|
||
.sp_icon_btn { background: #e5e5ea; color: #8e8e93; border: none; height: 28px; border-radius: 14px; cursor: pointer; display: flex; align-items: center; justify-content: center; padding: 0 8px; transition: 0.2s; gap: 4px; font-weight: bold; font-size: 12px; min-width: 28px; }
|
||
.sp_icon_btn:active { background: #d1d1d6; color: #333; }
|
||
.sp_tabs { display: flex; background: #e5e5ea; border-radius: 8px; padding: 2px; }
|
||
.sp_tab { flex: 1; text-align: center; padding: 6px 0; font-size: 13px; font-weight: 600; color: #8e8e93; border-radius: 6px; cursor: pointer; transition: 0.2s; }
|
||
.sp_tab.active { background: #fff; color: #000; box-shadow: 0 1px 4px rgba(0,0,0,0.06); }
|
||
.sp_body { flex: 1; overflow-y: auto; padding: 12px 16px; padding-bottom: calc(16px + env(safe-area-inset-bottom)); scroll-behavior: smooth; max-height: 75vh; }
|
||
#sp_panel.picking-mode .sp_body { max-height: 0 !important; padding-top: 0 !important; padding-bottom: 0 !important; margin: 0 !important; opacity: 0 !important; overflow: hidden !important; }
|
||
#sp_panel.picking-mode .sp_tabs { display: none !important; }
|
||
#sp_panel.picking-mode:not(.pos-top) .sp_header { padding-bottom: calc(12px + env(safe-area-inset-bottom)); }
|
||
.sp_status_bar { background: #d1e8ff; color: #007aff; padding: 8px; border-radius: 8px; text-align: center; font-weight: bold; margin-bottom: 12px; display: none; }
|
||
.sp_reuse_bar { display: flex; gap: 16px; margin-bottom: 12px; padding: 8px 12px; background: #fff; border: 1px solid #e5e5ea; border-radius: 8px; align-items: center; }
|
||
.sp_chk_label { font-size: 13px; color: #007aff; display: flex; align-items: center; cursor: pointer; font-weight: bold; }
|
||
.sp_chk_label input[type="checkbox"] { margin: 0 6px 0 0; transform: scale(1.1); accent-color: #007aff; cursor: pointer; }
|
||
.sp_group_title { font-size: 13px; color: #8e8e93; font-weight: bold; margin-bottom: 8px; margin-top: 6px; padding-left: 4px; }
|
||
.sp_task_grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 8px; margin-bottom: 16px; }
|
||
.sp_task_item { background: #fff; border: 1px solid #e5e5ea; border-radius: 8px; padding: 8px 10px; display: flex; flex-direction: column; cursor: pointer; transition: 0.2s; }
|
||
.sp_task_item.active { border-color: #007aff; background: #e6f2ff; box-shadow: 0 0 0 1px #007aff; }
|
||
.sp_task_name { font-weight: 600; font-size: 13px; margin-bottom: 2px; display: flex; justify-content: space-between; align-items:center; color: #333; }
|
||
.sp_task_item.done .sp_task_name::after { content: '✓'; color: #34c759; font-size: 14px; font-weight: bold; margin-left: 4px; }
|
||
.sp_task_val { font-size: 11px; color: #8e8e93; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.sp_btn_test { background: #34c759; color: #fff; border: none; padding: 10px; border-radius: 8px; font-weight: bold; font-size: 14px; width: 100%; cursor: pointer; margin-bottom: 12px; box-shadow: 0 2px 6px rgba(52, 199, 89, 0.3); display: flex; align-items: center; justify-content: center; gap: 8px; }
|
||
.sp_btn_test:active { background: #28a745; }
|
||
.sp_out_header { display: flex; justify-content: space-between; align-items: flex-end; border-bottom: 1px solid #c7c7cc; margin-bottom: 8px; }
|
||
.sp_out_tabs { display: flex; gap: 16px; }
|
||
.sp_out_tab { padding: 4px 0; font-size: 13px; font-weight: bold; color: #8e8e93; cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -1px; display: flex; align-items: center; gap: 4px; }
|
||
.sp_out_tab.active { color: #007aff; border-bottom-color: #007aff; }
|
||
.sp_action_btns { display: flex; gap: 8px; margin-bottom: 4px; display: none; }
|
||
.sp_copy_btn { background: #007aff; color: #fff; border: none; border-radius: 4px; padding: 4px 10px; font-size: 12px; font-weight: bold; cursor: pointer; display: flex; align-items: center; gap: 4px; }
|
||
.sp_dl_btn { background: #34c759; }
|
||
.sp_pane { display: none; width: 100%; height: 180px; background: #fff; border: 1px solid #e5e5ea; border-radius: 8px; padding: 8px; font-family: ui-monospace, monospace; font-size: 11px; color: #1c1c1e; overflow-y: auto; resize: none; white-space: pre-wrap; word-break: break-all; }
|
||
.sp_pane.active { display: block; }
|
||
#sp_test_result { background: #1e1e1e; color: #4af626; }
|
||
|
||
/* 属性配置弹窗与折叠样式 */
|
||
#sp_dialog_overlay { pointer-events: auto; position: fixed !important; inset: 0 !important; background: rgba(0,0,0,0.5); z-index: 2147483648; display: none; align-items: flex-end; justify-content: center; }
|
||
#sp_dialog_overlay.pos-top { align-items: flex-start; }
|
||
#sp_dialog { width: 100%; background: #f2f2f7; border-radius: 16px 16px 0 0; padding: 16px; padding-bottom: calc(16px + env(safe-area-inset-bottom)); transform: translateY(100%); transition: transform 0.3s cubic-bezier(0.2, 0.8, 0.2, 1), border-radius 0.3s; box-shadow: 0 -5px 20px rgba(0,0,0,0.2); max-height: 92vh; display: flex; flex-direction: column; }
|
||
#sp_dialog.show { transform: translateY(0); }
|
||
#sp_dialog.pos-top { border-radius: 0 0 16px 16px; padding-bottom: 16px; padding-top: calc(16px + env(safe-area-inset-top)); transform: translateY(-100%); box-shadow: 0 5px 20px rgba(0,0,0,0.2); }
|
||
#sp_dialog.pos-top.show { transform: translateY(0); }
|
||
|
||
#sp_dialog.collapsed { background: transparent; box-shadow: none; pointer-events: none; }
|
||
#sp_dialog.collapsed .sp_dialog_card#sp_dialog_header_card { background: rgba(255,255,255,0.95); backdrop-filter: blur(10px); box-shadow: 0 4px 12px rgba(0,0,0,0.15); pointer-events: auto; padding-bottom: 12px; }
|
||
#sp_dialog.collapsed .sp_hide_on_collapse { display: none !important; }
|
||
|
||
.sp_dialog_card { background: #fff; border-radius: 12px; padding: 12px; margin-bottom: 12px; flex-shrink: 0; }
|
||
.sp_current_field_hint { display: inline-block; background: #e6f2ff; color: #007aff; padding: 4px 8px; border-radius: 6px; font-size: 12px; font-weight: bold; border: 1px solid #b3d9ff; }
|
||
.sp_path_view { background: #f2f2f7; padding: 8px; border-radius: 6px; font-family: monospace; font-size: 11px; color: #ff9500; word-break: break-all; border: 1px solid #e5e5ea; margin-top: 8px; }
|
||
.sp_attr_list { background: #fff; border-radius: 12px; margin-bottom: 12px; flex-shrink: 1; overflow-y: auto; min-height: 50px; max-height: 30vh; }
|
||
.sp_attr_item { display: flex; padding: 14px 12px; border-bottom: 1px solid #f2f2f7; align-items: center; cursor: pointer; transition: 0.2s; flex-wrap: wrap; }
|
||
.sp_attr_item:last-child { border-bottom: none; }
|
||
.sp_attr_item:active { background: #f9f9f9; }
|
||
.sp_attr_item input[type="radio"] { margin-right: 12px; transform: scale(1.3); accent-color: #007aff; }
|
||
.sp_attr_name { font-weight: bold; width: 80px; font-size: 13px; color: #000; }
|
||
.sp_attr_val { color: #666; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; }
|
||
#sp_filter_card { flex-shrink: 1; overflow-y: auto; min-height: 80px; max-height: 35vh; display: flex; flex-direction: column; }
|
||
.sp_preset_chip { background: #f2f2f7; color: #007aff; border: none; border-radius: 6px; padding: 6px 10px; font-size: 12px; cursor: pointer; font-weight:bold; transition:0.2s; display:flex; align-items:center; justify-content:center; }
|
||
.sp_preset_chip:active { background: #e5e5ea; }
|
||
.sp_filter_item { display: flex; gap: 6px; align-items: center; background: #f9f9f9; padding: 8px; border-radius: 8px; border: 1px solid #e5e5ea; flex-shrink:0; }
|
||
.sp_filter_input { flex: 1; border: 1px solid #e5e5ea; border-radius: 6px; padding: 6px 8px; font-size: 12px; outline: none; background: #fff; color: #333; transition: border-color 0.2s; width: 10px; }
|
||
.sp_filter_input:focus { border-color: #007aff; }
|
||
.sp_magic_btn { background: transparent; border: none; padding: 0; cursor: pointer; position: absolute; right: 6px; top: 50%; transform: translateY(-50%); display:flex; align-items:center; }
|
||
.sp_regex_menu { position: absolute; right: 0; top: calc(100% + 4px); background: #fff; border: 1px solid #e5e5ea; border-radius: 8px; box-shadow: 0 4px 16px rgba(0,0,0,0.15); z-index: 100; display: none; flex-direction: column; padding: 4px; width: 90px; }
|
||
.sp_regex_menu_item { padding: 8px 12px; font-size: 12px; cursor: pointer; border-radius: 4px; color: #333; transition: background 0.2s; }
|
||
.sp_regex_menu_item:hover, .sp_regex_menu_item:active { background: #f2f2f7; color: #007aff; font-weight: bold; }
|
||
.sp_dialog_btns { display: flex; gap: 10px; flex-shrink: 0; padding-top: 4px; }
|
||
.sp_dialog_btn { flex: 1; padding: 12px; border: none; border-radius: 12px; font-weight: bold; font-size: 15px; cursor: pointer; transition: 0.2s; display:flex; justify-content:center; align-items:center; }
|
||
.btn_cancel { background: #e5e5ea; color: #8e8e93; }
|
||
.btn_cancel:active { background: #d1d1d6; color: #333; }
|
||
.btn_confirm { background: #007aff; color: #fff; box-shadow: 0 2px 6px rgba(0,122,255,0.3); }
|
||
.btn_confirm:active { background: #0056b3; }
|
||
.btn_parent { background: #e5e5ea; color: #007aff; padding: 6px 12px; border-radius: 10px; font-size: 13px; border: none; font-weight: bold; cursor: pointer; transition: 0.2s; white-space: nowrap; }
|
||
.btn_parent:active { background: #d1d1d6; }
|
||
|
||
/* 树状手风琴筛选专属样式 */
|
||
.sp_flt_class_row { display: flex; gap: 8px; align-items: center; background: #fff; padding: 10px; border-radius: 8px; border: 1px solid #e5e5ea; margin-bottom: 8px; }
|
||
.sp_flt_class_input { flex: 1; border: 1px solid #e5e5ea; border-radius: 6px; padding: 6px 8px; font-size: 13px; outline: none; background: #f9f9f9; width: 10px; }
|
||
.sp_flt_class_input:focus { border-color: #007aff; background: #fff; }
|
||
.sp_flt_btn_add { background: #e6f2ff; color: #007aff; border: none; border-radius: 6px; padding: 6px 10px; font-size: 12px; font-weight: bold; cursor: pointer; display: flex; align-items: center; gap: 4px; white-space: nowrap; transition: 0.2s; }
|
||
.sp_flt_btn_add:active { background: #cce4ff; }
|
||
.sp_flt_accordion { background: #f2f2f7; padding: 10px; border-radius: 8px; margin: -4px 0 12px 10px; border-left: 3px solid #007aff; display: none; }
|
||
.sp_flt_accordion.open { display: block; }
|
||
.sp_flt_presets { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
|
||
.sp_flt_item_row { display: flex; gap: 6px; align-items: center; background: #fff; padding: 8px; border-radius: 8px; border: 1px solid #e5e5ea; margin-bottom: 6px; }
|
||
.sp_flt_btn_pick { background: #f2f2f7; border: 1px solid #e5e5ea; border-radius: 6px; padding: 6px 10px; font-size: 12px; font-weight: bold; color: #333; cursor: pointer; flex-shrink: 0; transition: 0.2s; }
|
||
.sp_flt_btn_pick:active { background: #e5e5ea; }
|
||
.sp_flt_btn_pick.done { color: #34c759; border-color: #34c759; background: #ebf9eb; }
|
||
.sp_flt_btn_del { background: transparent; color: #ff3b30; border: none; padding: 4px; cursor: pointer; display: flex; align-items: center; justify-content: center; border-radius: 4px; }
|
||
.sp_flt_btn_del:active { background: #ffe5e5; }
|
||
.sp_flt_btn_batch { background: #ff9500; color: #fff; border: none; border-radius: 6px; padding: 6px 10px; font-size: 12px; font-weight: bold; cursor: pointer; display: flex; align-items: center; gap: 4px; transition: 0.2s; }
|
||
.sp_flt_btn_batch:active { background: #e08300; }
|
||
|
||
@media (min-width: 768px) {
|
||
#sp_panel { width: 600px !important; left: 50% !important; right: auto !important; margin-left: -300px !important; border-radius: 16px 16px 0 0 !important; }
|
||
#sp_panel.pos-top { border-radius: 0 0 16px 16px !important; }
|
||
#sp_dialog { width: 600px !important; border-radius: 16px 16px 0 0 !important; }
|
||
#sp_dialog.pos-top { border-radius: 0 0 16px 16px !important; }
|
||
}
|
||
`;
|
||
shadow.appendChild(style);
|
||
|
||
const uiTemplate = document.createElement('div');
|
||
uiTemplate.innerHTML = `
|
||
<div id="sp_fab">配置</div>
|
||
<div id="sp_panel">
|
||
<div class="sp_header">
|
||
<div class="sp_title_bar">
|
||
<span id="sp_panel_title">Spider 规则构建</span>
|
||
<div style="display:flex; gap:8px;">
|
||
<button class="sp_icon_btn" id="sp_btn_manual" title="跳过点选" style="display:none; color:#ff9500; width:auto; padding:0 10px; gap:4px; font-size:12px; font-weight:bold;">${svgIcons.edit} 固定值</button>
|
||
<button class="sp_icon_btn" id="sp_clear_cache" title="清空配置">${svgIcons.clear}</button>
|
||
<button class="sp_icon_btn" id="sp_pos_panel" title="切换位置">${svgIcons.dock}</button>
|
||
<button class="sp_icon_btn" id="sp_close_panel">${svgIcons.close}</button>
|
||
</div>
|
||
</div>
|
||
<div class="sp_tabs">
|
||
<div class="sp_tab active" data-tab="global">全局</div>
|
||
<div class="sp_tab" data-tab="home">推荐</div>
|
||
<div class="sp_tab" data-tab="category">分类</div>
|
||
<div class="sp_tab" data-tab="filter">筛选</div>
|
||
<div class="sp_tab" data-tab="detail">详情</div>
|
||
<div class="sp_tab" data-tab="search">搜索</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sp_body" id="sp_panel_body">
|
||
<div class="sp_status_bar" id="sp_status"></div>
|
||
<div class="sp_reuse_bar" id="sp_reuse_bar" style="display:none;"></div>
|
||
<div id="sp_task_container"></div>
|
||
<button class="sp_btn_test" id="sp_run_test">${svgIcons.play} 运行测试 & 生成代码</button>
|
||
<div class="sp_out_header" id="sp_out_header" style="display:none;">
|
||
<div class="sp_out_tabs">
|
||
<div class="sp_out_tab active" data-target="sp_test_result">测试结果</div>
|
||
<div class="sp_out_tab" id="sp_tab_code" data-target="sp_code_output">方法代码</div>
|
||
</div>
|
||
<div class="sp_action_btns" id="sp_action_btns">
|
||
<button class="sp_copy_btn sp_dl_btn" id="sp_dl_btn">${svgIcons.download} 下载 .js</button>
|
||
<button class="sp_copy_btn" id="sp_copy_btn">${svgIcons.copy} 复制</button>
|
||
</div>
|
||
</div>
|
||
<pre id="sp_test_result" class="sp_pane active" style="display:none;">等待测试...</pre>
|
||
<textarea id="sp_code_output" class="sp_pane" readonly style="display:none;"></textarea>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="sp_dialog_overlay">
|
||
<div id="sp_dialog">
|
||
<div class="sp_dialog_card" id="sp_dialog_header_card" style="margin-bottom:12px; transition:0.3s;">
|
||
<div style="display:flex; justify-content:space-between; align-items:flex-start;">
|
||
<div>
|
||
<span id="sp_dialog_title" style="font-size:15px; font-weight:bold; color:#333; display:block; margin-bottom:6px;">配置属性</span>
|
||
<div class="sp_current_field_hint" id="sp_field_hint"></div>
|
||
</div>
|
||
<div style="display:flex; gap:6px; align-items:center;">
|
||
<button class="btn_parent" id="sp_btn_restore" style="display:none; background:#34c759; color:#fff;">恢复面板</button>
|
||
<button class="btn_parent" id="sp_btn_child_node">缩小</button>
|
||
<button class="btn_parent" id="sp_btn_parent_node">放大</button>
|
||
<button class="sp_icon_btn" id="sp_pos_dialog" style="height:26px; width:26px; border-radius:8px; margin-left:4px;">${svgIcons.dock}</button>
|
||
</div>
|
||
</div>
|
||
<div id="sp_path_wrap" class="sp_hide_on_collapse">
|
||
<div id="sp_path_controls" style="display:flex; justify-content:space-between; align-items:center; margin-top:12px; margin-bottom:4px;">
|
||
<span style="font-size:11px; color:#8e8e93; font-weight:bold;">元素路径</span>
|
||
<label class="sp_chk_label"><input type="checkbox" id="sp_chk_absolute"> 绝对定位匹配</label>
|
||
</div>
|
||
<div class="sp_path_view" id="sp_css_path"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="sp_attr_list sp_hide_on_collapse" id="sp_attr_container"></div>
|
||
|
||
<div class="sp_dialog_card sp_hide_on_collapse" id="sp_filter_card" style="display:none; flex-direction:column;">
|
||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px; padding-bottom:8px; border-bottom:1px solid #e5e5ea;">
|
||
<span style="font-size:13px; font-weight:bold; color:#333;">数据清洗附加处理</span>
|
||
</div>
|
||
<div style="display:flex; flex-wrap:wrap; gap:8px; margin-bottom:12px; flex-shrink:0;">
|
||
<button class="sp_preset_chip" id="sp_btn_add_replace">+ 替换</button>
|
||
<button class="sp_preset_chip" id="sp_btn_add_split">+ 分割</button>
|
||
<button class="sp_preset_chip" id="sp_btn_add_between">+ 取中间</button>
|
||
<button class="sp_preset_chip" id="sp_btn_add_regex">+ 正则</button>
|
||
<button class="sp_preset_chip" id="sp_btn_add_urldecode">+ URL解码</button>
|
||
<button class="sp_preset_chip" id="sp_btn_add_nospace">+ 去空白</button>
|
||
<button class="sp_preset_chip" id="sp_btn_add_prepend">+ 前缀</button>
|
||
<button class="sp_preset_chip" id="sp_btn_add_append">+ 后缀</button>
|
||
<button class="sp_preset_chip" id="sp_btn_add_nohtml">+ 去HTML</button>
|
||
</div>
|
||
<div id="sp_filter_list" style="display:flex; flex-direction:column; gap:8px;"></div>
|
||
</div>
|
||
|
||
<div class="sp_dialog_btns sp_hide_on_collapse">
|
||
<button class="sp_dialog_btn" id="sp_btn_reselect" style="display:none; background:#ff3b30; color:#fff; flex: 0.6;">重选</button>
|
||
<button class="sp_dialog_btn btn_cancel" id="sp_btn_cancel">取消</button>
|
||
<button class="sp_dialog_btn btn_confirm" id="sp_btn_confirm">保存配置</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
shadow.appendChild(uiTemplate);
|
||
|
||
const UI = {
|
||
fab: shadow.querySelector('#sp_fab'), panel: shadow.querySelector('#sp_panel'), panelBody: shadow.querySelector('#sp_panel_body'),
|
||
panelTitle: shadow.querySelector('#sp_panel_title'), taskContainer: shadow.querySelector('#sp_task_container'), tabs: shadow.querySelectorAll('.sp_tab'),
|
||
statusBar: shadow.querySelector('#sp_status'), codeOutput: shadow.querySelector('#sp_code_output'),
|
||
dialogOverlay: shadow.querySelector('#sp_dialog_overlay'), dialogBox: shadow.querySelector('#sp_dialog'),
|
||
attrContainer: shadow.querySelector('#sp_attr_container'), cssPath: shadow.querySelector('#sp_css_path'),
|
||
pathControls: shadow.querySelector('#sp_path_controls'), btnParentNode: shadow.querySelector('#sp_btn_parent_node'), btnChildNode: shadow.querySelector('#sp_btn_child_node'),
|
||
chkAbsolute: shadow.querySelector('#sp_chk_absolute'), btnReselect: shadow.querySelector('#sp_btn_reselect'), btnClearCache: shadow.querySelector('#sp_clear_cache'),
|
||
btnManual: shadow.querySelector('#sp_btn_manual'), testResult: shadow.querySelector('#sp_test_result'), outHeader: shadow.querySelector('#sp_out_header'),
|
||
outTabs: shadow.querySelectorAll('.sp_out_tab'), copyBtn: shadow.querySelector('#sp_copy_btn'), downloadBtn: shadow.querySelector('#sp_dl_btn'), actionBtns: shadow.querySelector('#sp_action_btns'),
|
||
tabCodeText: shadow.querySelector('#sp_tab_code'), reuseBar: shadow.querySelector('#sp_reuse_bar'), fieldHint: shadow.querySelector('#sp_field_hint'),
|
||
btnPosPanel: shadow.querySelector('#sp_pos_panel'), btnPosDialog: shadow.querySelector('#sp_pos_dialog'), filterCard: shadow.querySelector('#sp_filter_card'), filterList: shadow.querySelector('#sp_filter_list'),
|
||
dialogTitle: shadow.querySelector('#sp_dialog_title'), btnRestore: shadow.querySelector('#sp_btn_restore')
|
||
};
|
||
|
||
// =========================================================================
|
||
// === [BLOCK 2: 状态管理与本地缓存] ===
|
||
// =========================================================================
|
||
const TAB_SCHEMAS = {
|
||
home: { isList: true, fields: [{ id:'container', name:'卡片容器', isBatch: true }, { id:'vod_name', name:'标题' }, { id:'vod_pic', name:'图片' }, { id:'vod_id', name:'链接' }, { id:'vod_remarks', name:'备注' }] },
|
||
category: { isList: true, fields: [{ id:'container', name:'卡片容器', isBatch: true }, { id:'vod_name', name:'标题' }, { id:'vod_pic', name:'图片' }, { id:'vod_id', name:'链接' }, { id:'vod_remarks', name:'备注' }, { id:'pagecount', name:'总页数', global:true }, { id:'limit', name:'每页数量', global:true }, { id:'total', name:'总记录数', global:true }] },
|
||
detail: { isList: false, fields: [{ id:'vod_name', name:'名称' }, { id:'vod_pic', name:'海报' }, { id:'vod_year', name:'年份' }, { id:'vod_director', name:'导演' }, { id:'vod_actor', name:'主演' }, { id:'vod_content', name:'简介' }, { id:'vod_play_from', name:'线路名称', isPlay: true, isBatch: true }, { id:'vod_play_title', name:'剧集标题', isPlay: true, isBatch: true }, { id:'vod_play_url', name:'播放链接', isPlay: true, isBatch: true }] }
|
||
};
|
||
TAB_SCHEMAS.search = TAB_SCHEMAS.category;
|
||
|
||
let currentTab = 'global', currentPickingFieldId = null, currentElement = null, tempExtraction = null, elementHistory = [];
|
||
let isPosTop = false, useAbsoluteSelector = false;
|
||
let currentFilterPicking = null; // 格式: { classIdx: 0, filterIdx: 0, type: 'n'|'v' }
|
||
|
||
let spiderConfig = {
|
||
global: { base: window.location.origin, home: '', category: '', cat_pg1: false, detail: '', search: '', search_pg1: false },
|
||
home:{}, category:{}, detail:{}, search:{},
|
||
filter: {
|
||
list: [
|
||
{ name: '电影', val: '', expand: true, items: [] },
|
||
{ name: '剧集', val: '', expand: false, items: [] },
|
||
{ name: '动漫', val: '', expand: false, items: [] },
|
||
{ name: '综艺', val: '', expand: false, items: [] },
|
||
{ name: '短剧', val: '', expand: false, items: [] }
|
||
]
|
||
}
|
||
};
|
||
|
||
Object.keys(TAB_SCHEMAS).forEach(t => {
|
||
spiderConfig[t].container = { sel:'', node:null };
|
||
TAB_SCHEMAS[t].fields.forEach(f => { if(f.id!=='container') spiderConfig[t][f.id] = { sel:'', extract:null, node:null }; });
|
||
});
|
||
|
||
function saveConfig() {
|
||
try {
|
||
const cacheObj = { currentTab, isPosTop, data: {}, globalConfig: spiderConfig.global, filterConfig: spiderConfig.filter };
|
||
['home', 'category', 'detail', 'search'].forEach(t => {
|
||
cacheObj.data[t] = { _reuse: spiderConfig[t]._reuse };
|
||
TAB_SCHEMAS[t].fields.forEach(f => {
|
||
if (spiderConfig[t][f.id]) cacheObj.data[t][f.id] = { sel: spiderConfig[t][f.id].sel, extract: spiderConfig[t][f.id].extract };
|
||
});
|
||
});
|
||
localStorage.setItem('__SpiderUI_Cache', JSON.stringify(cacheObj));
|
||
} catch(e) {}
|
||
}
|
||
|
||
function loadConfig() {
|
||
try {
|
||
const cacheRaw = localStorage.getItem('__SpiderUI_Cache');
|
||
if (cacheRaw) {
|
||
const cache = JSON.parse(cacheRaw);
|
||
if (cache.currentTab && (TAB_SCHEMAS[cache.currentTab] || cache.currentTab === 'global' || cache.currentTab === 'filter')) currentTab = cache.currentTab;
|
||
if (typeof cache.isPosTop === 'boolean') isPosTop = cache.isPosTop;
|
||
if (cache.globalConfig) { spiderConfig.global = cache.globalConfig; if (!spiderConfig.global.base) spiderConfig.global.base = window.location.origin; }
|
||
if (cache.filterConfig && cache.filterConfig.list) { spiderConfig.filter = cache.filterConfig; }
|
||
|
||
['home', 'category', 'detail', 'search'].forEach(t => {
|
||
if (spiderConfig[t] && cache.data[t]) {
|
||
spiderConfig[t]._reuse = cache.data[t]._reuse;
|
||
TAB_SCHEMAS[t].fields.forEach(f => {
|
||
if (cache.data[t][f.id]) {
|
||
spiderConfig[t][f.id].sel = cache.data[t][f.id].sel || '';
|
||
spiderConfig[t][f.id].extract = cache.data[t][f.id].extract || null;
|
||
if (f.id === 'container' && spiderConfig[t][f.id].sel) { try { spiderConfig[t][f.id].node = document.querySelector(spiderConfig[t][f.id].sel); } catch(e) {} }
|
||
}
|
||
});
|
||
}
|
||
});
|
||
}
|
||
} catch(e) {}
|
||
}
|
||
|
||
function copyConfig(srcTab, destTab) {
|
||
const src = spiderConfig[srcTab]; const dest = spiderConfig[destTab];
|
||
TAB_SCHEMAS[destTab].fields.forEach(f => {
|
||
if (src[f.id]) {
|
||
dest[f.id].sel = src[f.id].sel;
|
||
if (f.id === 'container') { try { dest[f.id].node = src[f.id].sel ? document.querySelector(src[f.id].sel) : null; } catch(e) {} }
|
||
else { dest[f.id].extract = src[f.id].extract ? JSON.parse(JSON.stringify(src[f.id].extract)) : null; }
|
||
}
|
||
});
|
||
saveConfig(); renderTaskList();
|
||
}
|
||
loadConfig();
|
||
|
||
// =========================================================================
|
||
// === [BLOCK 3: 树状筛选专享渲染引擎] ===
|
||
// =========================================================================
|
||
function renderFilterTab() {
|
||
UI.reuseBar.style.display = 'none';
|
||
let html = `<div class="sp_group_title" style="display:flex; justify-content:space-between; align-items:center;">
|
||
<span>大类与筛选提取 (树状结构)</span><button class="sp_flt_btn_add" id="sp_flt_add_class">${svgIcons.plus} 添加分类</button>
|
||
</div>`;
|
||
|
||
spiderConfig.filter.list.forEach((cls, cIdx) => {
|
||
html += `
|
||
<div class="sp_flt_class_row">
|
||
<input type="text" class="sp_flt_class_input fc_name" data-cidx="${cIdx}" placeholder="分类名(如:电影)" value="${cls.name}">
|
||
<input type="text" class="sp_flt_class_input fc_val" data-cidx="${cIdx}" placeholder="分类值(留空则不生成筛选)" value="${cls.val}">
|
||
<button class="sp_flt_btn_del f_del_class" data-cidx="${cIdx}" title="删除此分类">${svgIcons.clear}</button>
|
||
<button class="sp_flt_btn_add f_toggle_exp" data-cidx="${cIdx}">${svgIcons.plus} 筛选</button>
|
||
</div>
|
||
<div class="sp_flt_accordion ${cls.expand ? 'open' : ''}">
|
||
<div class="sp_flt_presets">
|
||
<button class="sp_flt_btn_batch f_batch_sniff" data-cidx="${cIdx}">${svgIcons.target} 点选页面维度名,批量生成</button>
|
||
<div style="flex:1;"></div>
|
||
<span style="font-size:11px; color:#8e8e93; margin-right:4px; display:flex; align-items:center;">添加维度:</span>
|
||
<button class="sp_preset_chip f_add_item" data-cidx="${cIdx}" data-key="cate" data-name="剧情">+剧情</button>
|
||
<button class="sp_preset_chip f_add_item" data-cidx="${cIdx}" data-key="type" data-name="类型">+类型</button>
|
||
<button class="sp_preset_chip f_add_item" data-cidx="${cIdx}" data-key="area" data-name="地区">+地区</button>
|
||
<button class="sp_preset_chip f_add_item" data-cidx="${cIdx}" data-key="year" data-name="年份">+年份</button>
|
||
<button class="sp_preset_chip f_add_item" data-cidx="${cIdx}" data-key="lang" data-name="语言">+语言</button>
|
||
<button class="sp_preset_chip f_add_item" data-cidx="${cIdx}" data-key="letter" data-name="字母">+字母</button>
|
||
<button class="sp_preset_chip f_add_item" data-cidx="${cIdx}" data-key="by" data-name="排序">+排序</button>
|
||
</div>`;
|
||
|
||
cls.items.forEach((flt, fIdx) => {
|
||
let nDone = flt.n && flt.n.sel ? 'done' : ''; let vDone = flt.v && flt.v.sel ? 'done' : '';
|
||
html += `
|
||
<div class="sp_flt_item_row">
|
||
<input type="text" class="sp_flt_class_input fi_key" data-cidx="${cIdx}" data-fidx="${fIdx}" placeholder="Key" value="${flt.key}" style="width:45px; flex:none;">
|
||
<input type="text" class="sp_flt_class_input fi_name" data-cidx="${cIdx}" data-fidx="${fIdx}" placeholder="维度名" value="${flt.name}" style="width:60px;">
|
||
<button class="sp_flt_btn_pick f_pick_n ${nDone}" data-cidx="${cIdx}" data-fidx="${fIdx}">选名 ${nDone?'✓':''}</button>
|
||
<button class="sp_flt_btn_pick f_pick_v ${vDone}" data-cidx="${cIdx}" data-fidx="${fIdx}">选值 ${vDone?'✓':''}</button>
|
||
<div style="flex:1;"></div>
|
||
<button class="sp_flt_btn_del f_del_item" data-cidx="${cIdx}" data-fidx="${fIdx}">${svgIcons.clear}</button>
|
||
</div>`;
|
||
});
|
||
html += `</div>`;
|
||
});
|
||
|
||
UI.taskContainer.innerHTML = html;
|
||
bindFilterEvents();
|
||
}
|
||
|
||
let currentBatchSniff = null; // 格式: { classIdx: 0 }
|
||
|
||
function bindFilterEvents() {
|
||
shadow.querySelector('#sp_flt_add_class').onclick = () => { spiderConfig.filter.list.push({ name: '', val: '', expand: true, items: [] }); saveConfig(); renderFilterTab(); };
|
||
shadow.querySelectorAll('.fc_name').forEach(el => el.oninput = (e) => { spiderConfig.filter.list[e.target.dataset.cidx].name = e.target.value; saveConfig(); });
|
||
shadow.querySelectorAll('.fc_val').forEach(el => el.oninput = (e) => { spiderConfig.filter.list[e.target.dataset.cidx].val = e.target.value; saveConfig(); });
|
||
shadow.querySelectorAll('.f_del_class').forEach(btn => btn.onclick = (e) => { if(confirm('[警告] 确定删除此分类及下方所有筛选吗?')){ spiderConfig.filter.list.splice(e.target.closest('button').dataset.cidx, 1); saveConfig(); renderFilterTab(); } });
|
||
shadow.querySelectorAll('.f_toggle_exp').forEach(btn => btn.onclick = (e) => { let cIdx = e.target.closest('button').dataset.cidx; spiderConfig.filter.list[cIdx].expand = !spiderConfig.filter.list[cIdx].expand; saveConfig(); renderFilterTab(); });
|
||
|
||
// 快捷预置:不再新建,而是新建后直接显示
|
||
shadow.querySelectorAll('.f_add_item').forEach(btn => btn.onclick = (e) => { spiderConfig.filter.list[e.target.dataset.cidx].items.push({ key: e.target.dataset.key, name: e.target.dataset.name, n: null, v: null }); saveConfig(); renderFilterTab(); });
|
||
|
||
shadow.querySelectorAll('.fi_key').forEach(el => el.oninput = (e) => { spiderConfig.filter.list[e.target.dataset.cidx].items[e.target.dataset.fidx].key = e.target.value; saveConfig(); });
|
||
shadow.querySelectorAll('.fi_name').forEach(el => el.oninput = (e) => { spiderConfig.filter.list[e.target.dataset.cidx].items[e.target.dataset.fidx].name = e.target.value; saveConfig(); });
|
||
shadow.querySelectorAll('.f_del_item').forEach(btn => btn.onclick = (e) => { let b = e.target.closest('button'); spiderConfig.filter.list[b.dataset.cidx].items.splice(b.dataset.fidx, 1); saveConfig(); renderFilterTab(); });
|
||
|
||
// 🎯 批量嗅探维度绑定
|
||
shadow.querySelectorAll('.f_batch_sniff').forEach(btn => btn.onclick = (e) => {
|
||
e.stopPropagation();
|
||
let cIdx = btn.dataset.cidx;
|
||
currentBatchSniff = { classIdx: cIdx };
|
||
UI.statusBar.style.display = 'none';
|
||
UI.panelTitle.textContent = `[${spiderConfig.filter.list[cIdx].name}] - 请在页面点击任意维度名(如:按剧情)`;
|
||
UI.panelTitle.classList.add('picking'); UI.panel.classList.add('picking-mode'); UI.panel.style.cursor = 'pointer';
|
||
togglePicking(true);
|
||
});
|
||
|
||
shadow.querySelectorAll('.f_pick_n, .f_pick_v').forEach(btn => btn.onclick = (e) => {
|
||
e.stopPropagation();
|
||
let isN = btn.classList.contains('f_pick_n'); let cIdx = btn.dataset.cidx, fIdx = btn.dataset.fidx;
|
||
let targetObj = spiderConfig.filter.list[cIdx].items[fIdx][isN ? 'n' : 'v'];
|
||
currentFilterPicking = { classIdx: cIdx, filterIdx: fIdx, type: isN ? 'n' : 'v' };
|
||
|
||
if (targetObj && targetObj.sel) {
|
||
useAbsoluteSelector = true;
|
||
try { currentElement = document.querySelector(targetObj.sel); } catch(e){ currentElement = null;}
|
||
tempExtraction = { type: targetObj.extract.type, val: targetObj.extract.val||'', key: targetObj.extract.key||'', sel: targetObj.sel, filters: targetObj.extract.filters ? JSON.parse(JSON.stringify(targetObj.extract.filters)) : [] };
|
||
openDialog(true);
|
||
} else {
|
||
useAbsoluteSelector = true; tempExtraction = null; UI.statusBar.style.display = 'none';
|
||
UI.panelTitle.textContent = `提取 [${spiderConfig.filter.list[cIdx].name}] - ${spiderConfig.filter.list[cIdx].items[fIdx].name} (${isN?'选名':'选值'})`;
|
||
UI.panelTitle.classList.add('picking'); UI.panel.classList.add('picking-mode'); UI.panel.style.cursor = 'pointer';
|
||
togglePicking(true);
|
||
}
|
||
});
|
||
}
|
||
|
||
// =========================================================================
|
||
// === [BLOCK 4: 基础视图与交互逻辑] ===
|
||
// =========================================================================
|
||
UI.btnClearCache.onclick = () => { if(confirm('[警告] 确定要清空所有已配置的规则缓存吗?')) { localStorage.removeItem('__SpiderUI_Cache'); location.reload(); } };
|
||
function applyPositionMode() { if (isPosTop) { UI.panel.classList.add('pos-top'); UI.dialogOverlay.classList.add('pos-top'); UI.dialogBox.classList.add('pos-top'); } else { UI.panel.classList.remove('pos-top'); UI.dialogOverlay.classList.remove('pos-top'); UI.dialogBox.classList.remove('pos-top'); } }
|
||
function togglePosition(e) { if(e) e.stopPropagation(); isPosTop = !isPosTop; saveConfig(); applyPositionMode(); }
|
||
UI.btnPosPanel.onclick = togglePosition; UI.btnPosDialog.onclick = togglePosition; applyPositionMode();
|
||
function resetPanelTitle() { UI.panelTitle.textContent = 'Spider 规则构建'; UI.panelTitle.classList.remove('picking'); UI.btnManual.style.display = 'none'; }
|
||
|
||
function renderTaskList() {
|
||
if (currentTab === 'global') {
|
||
UI.reuseBar.style.display = 'none';
|
||
let conf = spiderConfig.global;
|
||
let html = `
|
||
<div class="sp_group_title">基础环境</div>
|
||
<div class="sp_dialog_card" style="padding: 12px; margin-bottom: 12px;">
|
||
<div style="margin-bottom:8px;"><span style="font-size:12px;font-weight:bold;">根域名 (BASE)</span></div>
|
||
<input type="text" id="cfg_base" placeholder="如: https://www.example.com" class="sp_filter_input" style="width:100%; font-size:13px;" value="${conf.base || ''}">
|
||
</div>
|
||
<div class="sp_group_title">引擎路由宏模板</div>
|
||
<div class="sp_dialog_card" style="padding: 12px; margin-bottom: 12px;">
|
||
<div style="margin-bottom:8px;"><span style="font-size:12px;font-weight:bold;">首页路由 (homeContent)</span></div>
|
||
<input type="text" id="cfg_home" placeholder="如: {BASE}/api/home" class="sp_filter_input" style="width:100%; margin-bottom:8px;" value="${conf.home || ''}">
|
||
<div style="display:flex; gap:6px;"><button class="sp_preset_chip cfg_mac" data-target="cfg_home" data-val="{BASE}">+{BASE}</button></div>
|
||
</div>
|
||
<div class="sp_dialog_card" style="padding: 12px; margin-bottom: 12px;">
|
||
<div style="margin-bottom:8px;"><span style="font-size:12px;font-weight:bold;">分类路由 (categoryContent)</span></div>
|
||
<input type="text" id="cfg_category" placeholder="如: {BASE}/vod/show/id/{tid}/page/{pg}.html" class="sp_filter_input" style="width:100%; margin-bottom:8px;" value="${conf.category || ''}">
|
||
<div style="display:flex; gap:6px; margin-bottom:8px;"><button class="sp_preset_chip cfg_mac" data-target="cfg_category" data-val="{BASE}">+{BASE}</button><button class="sp_preset_chip cfg_mac" data-target="cfg_category" data-val="{tid}">+{tid} 分类</button><button class="sp_preset_chip cfg_mac" data-target="cfg_category" data-val="{pg}">+{pg} 页码</button></div>
|
||
<label class="sp_chk_label"><input type="checkbox" id="cfg_cat_pg1" ${conf.cat_pg1 ? 'checked' : ''}> 第一页清理分页后缀</label>
|
||
</div>
|
||
<div class="sp_dialog_card" style="padding: 12px; margin-bottom: 12px;">
|
||
<div style="margin-bottom:8px;"><span style="font-size:12px;font-weight:bold;">详情路由 (detailContent)</span></div>
|
||
<input type="text" id="cfg_detail" placeholder="如: {BASE}/vod/detail/id/{id}.html" class="sp_filter_input" style="width:100%; margin-bottom:8px;" value="${conf.detail || ''}">
|
||
<div style="display:flex; gap:6px;"><button class="sp_preset_chip cfg_mac" data-target="cfg_detail" data-val="{BASE}">+{BASE}</button><button class="sp_preset_chip cfg_mac" data-target="cfg_detail" data-val="{id}">+{id} 视频ID</button></div>
|
||
</div>
|
||
<div class="sp_dialog_card" style="padding: 12px; margin-bottom: 12px;">
|
||
<div style="margin-bottom:8px;"><span style="font-size:12px;font-weight:bold;">搜索路由 (searchContent)</span></div>
|
||
<input type="text" id="cfg_search" placeholder="如: {BASE}/search/{wd}----------{pg}---.html" class="sp_filter_input" style="width:100%; margin-bottom:8px;" value="${conf.search || ''}">
|
||
<div style="display:flex; gap:6px; margin-bottom:8px;"><button class="sp_preset_chip cfg_mac" data-target="cfg_search" data-val="{BASE}">+{BASE}</button><button class="sp_preset_chip cfg_mac" data-target="cfg_search" data-val="{wd}">+{wd} 搜索词</button><button class="sp_preset_chip cfg_mac" data-target="cfg_search" data-val="{pg}">+{pg} 页码</button></div>
|
||
<label class="sp_chk_label"><input type="checkbox" id="cfg_search_pg1" ${conf.search_pg1 ? 'checked' : ''}> 第一页清理分页后缀</label>
|
||
</div>`;
|
||
UI.taskContainer.innerHTML = html;
|
||
const bindInput = (id, key) => { const el = shadow.querySelector('#'+id); if(el) el.oninput = (e) => { spiderConfig.global[key] = e.target.value; saveConfig(); }; };
|
||
const bindCheck = (id, key) => { const el = shadow.querySelector('#'+id); if(el) el.onchange = (e) => { spiderConfig.global[key] = e.target.checked; saveConfig(); }; };
|
||
bindInput('cfg_base', 'base'); bindInput('cfg_home', 'home'); bindInput('cfg_category', 'category'); bindCheck('cfg_cat_pg1', 'cat_pg1'); bindInput('cfg_detail', 'detail'); bindInput('cfg_search', 'search'); bindCheck('cfg_search_pg1', 'search_pg1');
|
||
shadow.querySelectorAll('.cfg_mac').forEach(btn => { btn.onclick = (e) => { const input = shadow.querySelector('#' + e.target.dataset.target); if (input) { const val = e.target.dataset.val; const start = input.selectionStart; input.value = input.value.slice(0, start) + val + input.value.slice(input.selectionEnd); input.dispatchEvent(new Event('input')); input.focus(); input.selectionStart = input.selectionEnd = start + val.length; } }; });
|
||
return;
|
||
}
|
||
|
||
if (currentTab === 'filter') { renderFilterTab(); return; }
|
||
|
||
const schema = TAB_SCHEMAS[currentTab], conf = spiderConfig[currentTab];
|
||
UI.reuseBar.style.display = 'none';
|
||
|
||
if (currentTab === 'category') {
|
||
UI.reuseBar.style.display = 'flex';
|
||
UI.reuseBar.innerHTML = `<label class="sp_chk_label"><input type="checkbox" id="chk_reuse_home" ${conf._reuse === 'home' ? 'checked' : ''}> 复用推荐配置</label>`;
|
||
shadow.querySelector('#chk_reuse_home').onchange = (e) => { if (e.target.checked) { conf._reuse = 'home'; copyConfig('home', 'category'); } else { conf._reuse = null; clearConfig('category'); saveConfig(); renderTaskList(); } };
|
||
} else if (currentTab === 'search') {
|
||
UI.reuseBar.style.display = 'flex';
|
||
UI.reuseBar.innerHTML = `<label class="sp_chk_label" style="margin-right:12px;"><input type="checkbox" id="chk_reuse_home_s" ${conf._reuse === 'home' ? 'checked' : ''}> 复用推荐</label><label class="sp_chk_label"><input type="checkbox" id="chk_reuse_cat_s" ${conf._reuse === 'category' ? 'checked' : ''}> 复用分类</label>`;
|
||
shadow.querySelector('#chk_reuse_home_s').onchange = (e) => { if (e.target.checked) { conf._reuse = 'home'; shadow.querySelector('#chk_reuse_cat_s').checked = false; copyConfig('home', 'search'); } else { conf._reuse = null; clearConfig('search'); saveConfig(); renderTaskList(); } };
|
||
shadow.querySelector('#chk_reuse_cat_s').onchange = (e) => { if (e.target.checked) { conf._reuse = 'category'; shadow.querySelector('#chk_reuse_home_s').checked = false; copyConfig('category', 'search'); } else { conf._reuse = null; clearConfig('search'); saveConfig(); renderTaskList(); } };
|
||
}
|
||
|
||
let htmlList = schema.isList ? '<div class="sp_group_title">列表字段</div><div class="sp_task_grid">' : '<div class="sp_group_title">基础字段</div><div class="sp_task_grid">';
|
||
let htmlGlobal = schema.isList ? '<div class="sp_group_title" style="margin-top:12px;">全局字段</div><div class="sp_task_grid">' : '';
|
||
let htmlPlay = !schema.isList && currentTab === 'detail' ? '<div class="sp_group_title" style="margin-top:12px;">播放列表</div><div class="sp_task_grid">' : '';
|
||
let hasGlobal = false; let hasPlay = false;
|
||
|
||
schema.fields.forEach(f => {
|
||
let isDone = f.id === 'container' ? !!conf[f.id].sel : !!(conf[f.id] && conf[f.id].extract);
|
||
let activeClass = currentPickingFieldId === f.id ? 'active' : '';
|
||
let itemHtml = `<div class="sp_task_item ${isDone ? 'done' : ''} ${activeClass}" data-id="${f.id}"><div class="sp_task_name">${f.name}</div><div class="sp_task_val">${isDone ? '已配置' : '未配置'}</div></div>`;
|
||
if (f.global) { htmlGlobal += itemHtml; hasGlobal = true; } else if (f.isPlay) { htmlPlay += itemHtml; hasPlay = true; } else { htmlList += itemHtml; }
|
||
});
|
||
UI.taskContainer.innerHTML = htmlList + '</div>' + (hasGlobal ? htmlGlobal + '</div>' : '') + (hasPlay ? htmlPlay + '</div>' : '');
|
||
|
||
shadow.querySelectorAll('.sp_task_item').forEach(item => {
|
||
item.onclick = (e) => {
|
||
e.stopPropagation();
|
||
const fId = item.dataset.id, fSchema = schema.fields.find(x => x.id === fId);
|
||
const isDone = item.classList.contains('done');
|
||
if (schema.isList && fId !== 'container' && !fSchema.global && !conf.container.sel) { alert('[提示] 请先设定【卡片容器】!'); return; }
|
||
currentPickingFieldId = fId; renderTaskList();
|
||
if (isDone) {
|
||
let savedSel = conf[fId].sel || ''; useAbsoluteSelector = savedSel.includes('nth-of-type') || !fSchema.isBatch;
|
||
let targetEl = null;
|
||
if (fId === 'container') { try { targetEl = document.querySelector(savedSel); } catch(e){} }
|
||
else { let base = document; if (schema.isList && !fSchema.global && conf.container.node) base = conf.container.node; try { targetEl = savedSel ? base.querySelector(savedSel) : base; } catch(e){} }
|
||
currentElement = targetEl;
|
||
if (fId === 'container') { tempExtraction = { type: 'container', absSel: savedSel }; }
|
||
else { tempExtraction = { type: conf[fId].extract.type, val: conf[fId].extract.val || '', key: conf[fId].extract.key || '', sel: savedSel, filters: conf[fId].extract.filters ? JSON.parse(JSON.stringify(conf[fId].extract.filters)) : [] }; }
|
||
openDialog(true);
|
||
} else {
|
||
useAbsoluteSelector = !fSchema.isBatch; tempExtraction = null; UI.statusBar.style.display = 'none';
|
||
UI.panelTitle.textContent = `Spider 规则构建 - ${fSchema.name}`; UI.panelTitle.classList.add('picking');
|
||
UI.btnManual.style.display = 'flex'; UI.panel.classList.add('picking-mode'); UI.panel.style.cursor = 'pointer';
|
||
togglePicking(true);
|
||
}
|
||
};
|
||
});
|
||
}
|
||
|
||
UI.btnManual.onclick = (e) => { e.stopPropagation(); togglePicking(false); UI.btnManual.style.display = 'none'; currentElement = null; tempExtraction = { type: 'custom', key: '', val: '', sel: '', filters: [] }; openDialog(true); };
|
||
|
||
UI.tabs.forEach(tab => {
|
||
tab.onclick = () => {
|
||
UI.tabs.forEach(t => t.classList.remove('active')); tab.classList.add('active'); currentTab = tab.dataset.tab;
|
||
currentPickingFieldId = null; currentFilterPicking = null;
|
||
togglePicking(false); UI.statusBar.style.display = 'none'; resetPanelTitle(); saveConfig();
|
||
UI.panel.classList.remove('picking-mode'); UI.panel.style.cursor = 'default';
|
||
if (currentTab === 'filter') renderFilterTab(); else renderTaskList();
|
||
};
|
||
});
|
||
|
||
UI.outTabs.forEach(tab => {
|
||
tab.onclick = () => {
|
||
UI.outTabs.forEach(t => t.classList.remove('active')); tab.classList.add('active'); const targetId = tab.dataset.target;
|
||
UI.testResult.style.display = 'none'; UI.codeOutput.style.display = 'none'; UI.testResult.classList.remove('active'); UI.codeOutput.classList.remove('active');
|
||
const targetPane = shadow.querySelector('#' + targetId); targetPane.style.display = 'block'; setTimeout(() => targetPane.classList.add('active'), 10);
|
||
if (targetId === 'sp_code_output') { UI.actionBtns.style.display = 'flex'; } else { UI.actionBtns.style.display = 'none'; }
|
||
};
|
||
});
|
||
|
||
UI.copyBtn.onclick = () => { const code = UI.codeOutput.value; if(!code) return alert('[提示] 没有可复制的代码!'); if(navigator.clipboard && window.isSecureContext) { navigator.clipboard.writeText(code).then(() => { let oldHTML = UI.copyBtn.innerHTML; UI.copyBtn.innerHTML = '[成功]'; setTimeout(() => UI.copyBtn.innerHTML = oldHTML, 1500); }); } else { UI.codeOutput.select(); document.execCommand('copy'); let oldHTML = UI.copyBtn.innerHTML; UI.copyBtn.innerHTML = '[成功]'; setTimeout(() => UI.copyBtn.innerHTML = oldHTML, 1500); } };
|
||
UI.downloadBtn.onclick = () => { const code = UI.codeOutput.value; if(!code) return alert('[提示] 没有可导出的代码!'); const blob = new Blob([code], { type: 'text/javascript' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'spider_rule.js'; a.click(); URL.revokeObjectURL(url); };
|
||
|
||
// =========================================================================
|
||
// === [BLOCK 5: 核心选择器算法与数据清洗] ===
|
||
// =========================================================================
|
||
const handleEvent = (e) => {
|
||
if (e.composedPath().includes(host)) return;
|
||
if (e.type === 'mouseover' || e.type === 'touchstart') e.target.classList.add('spider-highlight');
|
||
else if (e.type === 'mouseout' || e.type === 'touchend') e.target.classList.remove('spider-highlight');
|
||
else if (e.type === 'click') {
|
||
e.preventDefault(); e.stopPropagation();
|
||
|
||
// [新增] 🎯 批量嗅探维度的捕获逻辑
|
||
if (currentBatchSniff) {
|
||
let cIdx = currentBatchSniff.classIdx;
|
||
let absPath = getSelector(e.target, null, true); // 获取点击元素的纯绝对路径
|
||
let parts = absPath.split(' > ');
|
||
|
||
// 核心:寻找倒数第一个可以泛化的结构层 (如 ul, dl, div.row 的 nth-of-type)
|
||
let genIdx = -1;
|
||
for(let i = 0; i < parts.length; i++) {
|
||
if(parts[i].includes(':nth-of-type')) { genIdx = i; break; }
|
||
}
|
||
|
||
if (genIdx === -1) {
|
||
alert('[提示] 未找到平级的列表结构规律,请手动添加维度。');
|
||
togglePicking(false); currentBatchSniff = null; resetPanelTitle();
|
||
UI.panel.classList.remove('picking-mode'); UI.panel.style.cursor = 'default';
|
||
renderFilterTab(); return;
|
||
}
|
||
|
||
// 剥离 nth-of-type 实现全域匹配
|
||
let genTag = parts[genIdx].split(':')[0];
|
||
parts[genIdx] = genTag;
|
||
let genSel = parts.join(' > ');
|
||
|
||
let nodes = document.querySelectorAll(genSel);
|
||
let batchId = Date.now().toString(); // 为这批兄弟维度打上共同烙印
|
||
let newItems = [];
|
||
|
||
nodes.forEach((node, i) => {
|
||
let text = node.textContent.replace(/[::\s]/g, '').trim();
|
||
if(!text) return;
|
||
|
||
// 智能倒推 Key
|
||
let inferredKey = 'key_' + i;
|
||
if (text.includes('类') || text.includes('型')) inferredKey = 'class';
|
||
else if (text.includes('剧')) inferredKey = 'cate';
|
||
else if (text.includes('区')) inferredKey = 'area';
|
||
else if (text.includes('年')) inferredKey = 'year';
|
||
else if (text.includes('语')) inferredKey = 'lang';
|
||
else if (text.includes('字')) inferredKey = 'letter';
|
||
else if (text.includes('排')) inferredKey = 'by';
|
||
|
||
// 向上回溯,找出这个维度对应的父级绝对基准路径 (用于未来的单点同步)
|
||
let curr = node;
|
||
while(curr && curr.tagName.toLowerCase() !== genTag) { curr = curr.parentElement; }
|
||
let baseSel = curr ? getSelector(curr, null, true) : '';
|
||
|
||
newItems.push({ key: inferredKey, name: text, n: null, v: null, baseSel: baseSel, batchId: batchId });
|
||
});
|
||
|
||
if (newItems.length > 0) {
|
||
spiderConfig.filter.list[cIdx].items = spiderConfig.filter.list[cIdx].items.concat(newItems);
|
||
saveConfig();
|
||
// 这里可以放一个轻提示,但原生 alert 会打断体验,所以静默刷新界面即可
|
||
} else {
|
||
alert('[提示] 未能嗅探到有效的兄弟节点。');
|
||
}
|
||
|
||
togglePicking(false); currentBatchSniff = null; resetPanelTitle();
|
||
UI.panel.classList.remove('picking-mode'); UI.panel.style.cursor = 'default';
|
||
renderFilterTab(); return;
|
||
}
|
||
|
||
let baseNode = document;
|
||
if (!currentFilterPicking && currentTab !== 'filter' && TAB_SCHEMAS[currentTab].isList && currentPickingFieldId !== 'container') {
|
||
let fSchema = TAB_SCHEMAS[currentTab].fields.find(x => x.id === currentPickingFieldId);
|
||
if (!fSchema.global && spiderConfig[currentTab].container && spiderConfig[currentTab].container.node) baseNode = spiderConfig[currentTab].container.node;
|
||
}
|
||
if (baseNode && !baseNode.contains(e.target) && e.target !== baseNode) { alert('[错误] 点击超出了卡片容器范围!'); e.target.classList.remove('spider-highlight'); return; }
|
||
currentElement = e.target; elementHistory = []; openDialog(false);
|
||
}
|
||
};
|
||
|
||
function togglePicking(isOn) {
|
||
['mouseover', 'mouseout', 'click', 'touchstart', 'touchend'].forEach(ev => { if (isOn) document.addEventListener(ev, handleEvent, {capture: true, passive: false}); else document.removeEventListener(ev, handleEvent, {capture: true}); });
|
||
if (!isOn) document.querySelectorAll('.spider-highlight').forEach(el => el.classList.remove('spider-highlight'));
|
||
}
|
||
|
||
UI.panel.addEventListener('click', (e) => {
|
||
if ((currentPickingFieldId !== null || currentFilterPicking !== null || currentBatchSniff !== null) && UI.panel.classList.contains('picking-mode')) {
|
||
if (e.target.closest('.sp_icon_btn') || e.target.closest('.sp_task_item')) return;
|
||
e.preventDefault(); e.stopPropagation(); togglePicking(false); resetPanelTitle();
|
||
UI.panel.classList.remove('picking-mode'); UI.panel.style.cursor = 'default';
|
||
currentPickingFieldId = null; currentFilterPicking = null; currentBatchSniff = null;
|
||
if (currentTab === 'filter') renderFilterTab(); else renderTaskList();
|
||
}
|
||
});
|
||
|
||
UI.btnReselect.onclick = () => {
|
||
UI.dialogBox.classList.remove('show');
|
||
setTimeout(() => {
|
||
UI.dialogOverlay.style.display = 'none';
|
||
UI.dialogBox.classList.remove('collapsed'); UI.btnRestore.style.display = 'none'; UI.dialogTitle.style.display = 'block'; UI.fieldHint.style.display = 'inline-block'; UI.fieldHint.style.margin = '0';
|
||
|
||
if (currentFilterPicking) {
|
||
let name = spiderConfig.filter.list[currentFilterPicking.classIdx].items[currentFilterPicking.filterIdx].name;
|
||
UI.panelTitle.textContent = `重新提取 - ${name} (${currentFilterPicking.type==='n'?'选名':'选值'})`;
|
||
} else {
|
||
const fSchema = TAB_SCHEMAS[currentTab].fields.find(x => x.id === currentPickingFieldId);
|
||
UI.panelTitle.textContent = `Spider 规则构建 - ${fSchema.name}`;
|
||
}
|
||
UI.panelTitle.classList.add('picking'); UI.btnManual.style.display = 'flex'; UI.panel.classList.add('picking-mode'); UI.panel.style.cursor = 'pointer';
|
||
currentElement = null; tempExtraction = null; togglePicking(true);
|
||
}, 300);
|
||
};
|
||
|
||
function openDialog(isEditMode = false) {
|
||
togglePicking(false); UI.btnManual.style.display = 'none';
|
||
UI.dialogBox.classList.remove('collapsed'); UI.btnRestore.style.display = 'none'; UI.dialogTitle.style.display = 'block'; UI.fieldHint.style.display = 'inline-block'; UI.fieldHint.style.margin = '0';
|
||
refreshDialog(isEditMode);
|
||
UI.btnReselect.style.display = isEditMode ? 'block' : 'none';
|
||
UI.dialogOverlay.style.display = 'flex';
|
||
setTimeout(() => UI.dialogBox.classList.add('show'), 10);
|
||
}
|
||
|
||
// [核心提取算法修复:允许截断泛型的最底层级和包裹层级]
|
||
function getSelector(el, stopNode, isAbsolute) {
|
||
if (!el || el === stopNode) return '';
|
||
let path = []; let current = el; let depth = 0;
|
||
let isBatchField = currentFilterPicking ? true : (currentPickingFieldId ? TAB_SCHEMAS[currentTab].fields.find(x => x.id === currentPickingFieldId)?.isBatch : false);
|
||
|
||
while (current && current.nodeType === 1 && current !== stopNode && current.tagName !== 'BODY') {
|
||
let sel = current.tagName.toLowerCase(); let hasId = false;
|
||
|
||
// 是否属于泛型包装层(如 <li> 或 <div>),需剥离 nth-of-type 以实现批量提取同行元素
|
||
let isGenericWrapper = isBatchField && (depth === 0 || (depth === 1 && (!current.className || current.tagName === 'LI')));
|
||
|
||
if (!isGenericWrapper && current.id && !current.id.includes('__spider')) { sel += '#' + CSS.escape(current.id); hasId = true; }
|
||
|
||
let hasClass = false;
|
||
if (!isGenericWrapper && !hasId && current.className) {
|
||
let classStr = typeof current.className === 'string' ? current.className : (current.className.baseVal || '');
|
||
let classes = classStr.trim().split(/\s+/).filter(c => c && !c.includes('spider-highlight') && !/^[a-zA-Z0-9\-_]{8,}$/.test(c));
|
||
if (classes.length > 0) { sel += '.' + CSS.escape(classes[0]); hasClass = true; }
|
||
}
|
||
if (hasId) { path.unshift(sel); break; }
|
||
|
||
let nth = 1, sibling = current;
|
||
while (sibling = sibling.previousElementSibling) { if (sibling.tagName === current.tagName) nth++; }
|
||
|
||
if (isAbsolute || nth > 1 || !hasClass) {
|
||
if (!isGenericWrapper) { sel += `:nth-of-type(${nth})`; }
|
||
}
|
||
path.unshift(sel); current = current.parentNode; depth++;
|
||
}
|
||
return path.join(' > ');
|
||
}
|
||
|
||
function refreshDialog(isEditMode = false) {
|
||
document.querySelectorAll('.spider-highlight').forEach(el => el.classList.remove('spider-highlight'));
|
||
if(currentElement) currentElement.classList.add('spider-highlight');
|
||
UI.chkAbsolute.checked = useAbsoluteSelector; let stopNode = null;
|
||
|
||
if (currentFilterPicking) { UI.fieldHint.textContent = `【树状筛选 - 提取${currentFilterPicking.type==='n'?'名称':'参数值'}】`; }
|
||
else { const fSchema = TAB_SCHEMAS[currentTab].fields.find(x => x.id === currentPickingFieldId); UI.fieldHint.textContent = `【${fSchema.name}】`; if (fSchema && !fSchema.global && spiderConfig[currentTab].container) stopNode = spiderConfig[currentTab].container.node; }
|
||
|
||
if (currentPickingFieldId === 'container' && !currentFilterPicking) {
|
||
UI.pathControls.style.display = 'flex'; UI.btnParentNode.style.display = 'block'; UI.btnChildNode.style.display = 'block';
|
||
let absSel = getSelector(currentElement, null, useAbsoluteSelector);
|
||
if(!isEditMode) tempExtraction = { type: 'container', absSel: absSel };
|
||
UI.cssPath.textContent = '容器: ' + tempExtraction.absSel;
|
||
UI.attrContainer.innerHTML = '<div style="padding:24px; text-align:center; color:#007aff; font-weight:bold; font-size:14px;">已锁定外层框架<br><br><span style="font-size:12px;color:#8e8e93;font-weight:normal;">如不准确可点击扩大或重选</span></div>';
|
||
} else {
|
||
let generatedSel = currentElement ? getSelector(currentElement, stopNode, useAbsoluteSelector) : '';
|
||
if(!isEditMode) {
|
||
if(!currentElement) tempExtraction = { type: 'custom', key: '', val: '', sel: '', filters: [] };
|
||
else tempExtraction = { type: tempExtraction?.type || 'text', key: tempExtraction?.key || '', val: '', sel: generatedSel, filters: tempExtraction?.filters || [] };
|
||
}
|
||
if (!currentElement) { UI.pathControls.style.display = 'none'; UI.cssPath.textContent = '路径: [无 (采用自定义固定值)]'; UI.btnParentNode.style.display = 'none'; UI.btnChildNode.style.display = 'none'; }
|
||
else { UI.pathControls.style.display = 'flex'; UI.cssPath.textContent = tempExtraction.sel ? '路径: ' + tempExtraction.sel : '路径: (当前节点本身)'; UI.btnParentNode.style.display = 'block'; UI.btnChildNode.style.display = 'block'; }
|
||
|
||
let htmlStr = `<label class="sp_attr_item"><div style="display:flex; align-items:center; width:100%;"><input type="radio" name="sp_ex" value="custom" ${tempExtraction.type==='custom'?'checked':''}><span class="sp_attr_name" style="color:#ff9500;">[固定值]</span><input type="text" id="sp_custom_input" placeholder="输入内容..." value="${tempExtraction.val || ''}" style="flex:1; border:1px solid #e5e5ea; border-radius:4px; padding:6px 8px; outline:none; font-size:12px;"></div></label>`;
|
||
if (currentElement) {
|
||
htmlStr += `<label class="sp_attr_item"><input type="radio" name="sp_ex" value="text" ${tempExtraction.type==='text'?'checked':''}><span class="sp_attr_name">[纯文本]</span><span class="sp_attr_val">${currentElement.textContent.trim() || '(无)'}</span></label>`;
|
||
htmlStr += `<label class="sp_attr_item"><input type="radio" name="sp_ex" value="html" ${tempExtraction.type==='html'?'checked':''}><span class="sp_attr_name" style="color:#34c759;">[内部源码]</span><span class="sp_attr_val">innerHTML</span></label>`;
|
||
htmlStr += `<label class="sp_attr_item"><input type="radio" name="sp_ex" value="outerhtml" ${tempExtraction.type==='outerhtml'?'checked':''}><span class="sp_attr_name" style="color:#34c759;">[外部源码]</span><span class="sp_attr_val">outerHTML</span></label>`;
|
||
Array.from(currentElement.attributes).forEach(attr => { let isChecked = (tempExtraction.type==='attr' && tempExtraction.key===attr.name) ? 'checked' : ''; htmlStr += `<label class="sp_attr_item"><input type="radio" name="sp_ex" value="attr:${attr.name}" ${isChecked}><span class="sp_attr_name" style="color:#007aff;">${attr.name}</span><span class="sp_attr_val">${attr.value}</span></label>`; });
|
||
} else { htmlStr += `<div style="padding:16px; color:#8e8e93; text-align:center; font-size:12px;">提示:您选择了手动输入固定值。<br>如需提取请点击重选。</div>`; }
|
||
UI.attrContainer.innerHTML = htmlStr;
|
||
|
||
shadow.querySelectorAll('input[name="sp_ex"]').forEach(r => r.addEventListener('change', (e) => {
|
||
if(e.target.value === 'text') tempExtraction.type = 'text'; else if (e.target.value === 'custom') tempExtraction.type = 'custom'; else if (e.target.value === 'html') tempExtraction.type = 'html'; else if (e.target.value === 'outerhtml') tempExtraction.type = 'outerhtml'; else { tempExtraction.type = 'attr'; tempExtraction.key = e.target.value.split(':')[1]; }
|
||
}));
|
||
const customInput = shadow.querySelector('#sp_custom_input');
|
||
if (customInput) { customInput.onclick = () => { shadow.querySelector('input[value="custom"]').checked = true; tempExtraction.type = 'custom'; }; customInput.oninput = (e) => { tempExtraction.val = e.target.value; if (shadow.querySelector('input[value="custom"]').checked === false) { shadow.querySelector('input[value="custom"]').checked = true; tempExtraction.type = 'custom'; } }; }
|
||
}
|
||
renderFilters();
|
||
}
|
||
|
||
function renderFilters() {
|
||
let isContainer = currentPickingFieldId === 'container';
|
||
if (isContainer && !currentFilterPicking) { UI.filterCard.style.display = 'none'; return; }
|
||
UI.filterCard.style.display = 'flex';
|
||
if (!tempExtraction || !tempExtraction.filters || tempExtraction.filters.length === 0) { UI.filterList.innerHTML = ''; UI.filterList.style.display = 'none'; return; }
|
||
UI.filterList.style.display = 'flex'; let html = ''; let delBtnHtml = `<button class="sp_icon_btn f_del" style="width:24px; height:24px; flex-shrink:0;">${svgIcons.close}</button>`;
|
||
|
||
tempExtraction.filters.forEach((flt, idx) => {
|
||
if (flt.type === 'replace') { html += `<div class="sp_filter_item"><span style="font-size:12px; color:#666; font-weight:bold;">替换</span><input type="text" class="sp_filter_input f_from" data-idx="${idx}" placeholder="被替换词" value="${flt.from || ''}"><span style="color:#999; font-size:12px;">➞</span><input type="text" class="sp_filter_input f_to" data-idx="${idx}" placeholder="替换为" value="${flt.to || ''}">${delBtnHtml.replace('f_del"', `f_del" data-idx="${idx}"`)}</div>`; }
|
||
else if (flt.type === 'split') { let dIdx = (parseInt(flt.index)||0)+1; html += `<div class="sp_filter_item"><span style="font-size:12px; color:#666; font-weight:bold;">分割</span><input type="text" class="sp_filter_input f_sep" data-idx="${idx}" placeholder="分割符" value="${flt.sep || ''}" style="flex:2;"><span style="color:#999; font-size:12px; white-space:nowrap;">取第</span><input type="number" min="1" class="sp_filter_input f_index" data-idx="${idx}" placeholder="1" value="${dIdx}" style="flex:1;"><span style="color:#999; font-size:12px;">段</span>${delBtnHtml.replace('f_del"', `f_del" data-idx="${idx}"`)}</div>`; }
|
||
else if (flt.type === 'between') { html += `<div class="sp_filter_item"><span style="font-size:12px; color:#666; font-weight:bold;">取中间</span><input type="text" class="sp_filter_input f_left" data-idx="${idx}" placeholder="左侧字符" value="${flt.left || ''}" style="flex:1;"><span style="color:#999; font-size:12px;">与</span><input type="text" class="sp_filter_input f_right" data-idx="${idx}" placeholder="右侧字符" value="${flt.right || ''}" style="flex:1;">${delBtnHtml.replace('f_del"', `f_del" data-idx="${idx}"`)}</div>`; }
|
||
else if (flt.type === 'regex') { html += `<div class="sp_filter_item"><span style="font-size:12px; color:#666; font-weight:bold;">正则</span><div style="position:relative; flex:1; display:flex;"><input type="text" class="sp_filter_input f_pattern" data-idx="${idx}" placeholder="匹配规则" value="${flt.pattern || ''}" style="padding-right:26px;"><button class="sp_magic_btn" data-idx="${idx}">${svgIcons.magic}</button><div class="sp_regex_menu" id="rmenu_${idx}"><div class="sp_regex_menu_item" data-idx="${idx}" data-val="\\d+">提取数字</div><div class="sp_regex_menu_item" data-idx="${idx}" data-val="[\\u4e00-\\u9fa5]+">提取中文</div><div class="sp_regex_menu_item" data-idx="${idx}" data-val="[a-zA-Z]+">提取字母</div></div></div>${delBtnHtml.replace('f_del"', `f_del" data-idx="${idx}"`)}</div>`; }
|
||
else if (flt.type === 'urldecode') { html += `<div class="sp_filter_item"><span style="font-size:12px; color:#007aff; font-weight:bold;">URL 解码</span><div style="flex:1;"></div>${delBtnHtml.replace('f_del"', `f_del" data-idx="${idx}"`)}</div>`; }
|
||
else if (flt.type === 'remove_space') { html += `<div class="sp_filter_item"><span style="font-size:12px; color:#ff9500; font-weight:bold;">去空白换行</span><div style="flex:1;"></div>${delBtnHtml.replace('f_del"', `f_del" data-idx="${idx}"`)}</div>`; }
|
||
else if (flt.type === 'prepend') { html += `<div class="sp_filter_item"><span style="font-size:12px; color:#007aff; font-weight:bold;">前缀</span><input type="text" class="sp_filter_input f_text" data-idx="${idx}" placeholder="补充在前面" value="${flt.text || ''}">${delBtnHtml.replace('f_del"', `f_del" data-idx="${idx}"`)}</div>`; }
|
||
else if (flt.type === 'append') { html += `<div class="sp_filter_item"><span style="font-size:12px; color:#007aff; font-weight:bold;">后缀</span><input type="text" class="sp_filter_input f_text" data-idx="${idx}" placeholder="追加在后面" value="${flt.text || ''}">${delBtnHtml.replace('f_del"', `f_del" data-idx="${idx}"`)}</div>`; }
|
||
else if (flt.type === 'remove_html') { html += `<div class="sp_filter_item"><span style="font-size:12px; color:#ff9500; font-weight:bold;">去HTML</span><div style="flex:1;"></div>${delBtnHtml.replace('f_del"', `f_del" data-idx="${idx}"`)}</div>`; }
|
||
});
|
||
UI.filterList.innerHTML = html;
|
||
|
||
shadow.querySelectorAll('.f_from').forEach(el => el.oninput = (e) => tempExtraction.filters[e.target.dataset.idx].from = e.target.value);
|
||
shadow.querySelectorAll('.f_to').forEach(el => el.oninput = (e) => tempExtraction.filters[e.target.dataset.idx].to = e.target.value);
|
||
shadow.querySelectorAll('.f_sep').forEach(el => el.oninput = (e) => tempExtraction.filters[e.target.dataset.idx].sep = e.target.value);
|
||
shadow.querySelectorAll('.f_left').forEach(el => el.oninput = (e) => tempExtraction.filters[e.target.dataset.idx].left = e.target.value);
|
||
shadow.querySelectorAll('.f_right').forEach(el => el.oninput = (e) => tempExtraction.filters[e.target.dataset.idx].right = e.target.value);
|
||
shadow.querySelectorAll('.f_pattern').forEach(el => el.oninput = (e) => tempExtraction.filters[e.target.dataset.idx].pattern = e.target.value);
|
||
shadow.querySelectorAll('.f_text').forEach(el => el.oninput = (e) => tempExtraction.filters[e.target.dataset.idx].text = e.target.value);
|
||
shadow.querySelectorAll('.f_index').forEach(el => el.oninput = (e) => { let h = parseInt(e.target.value); if(isNaN(h)) h=1; tempExtraction.filters[e.target.dataset.idx].index = Math.max(0, h - 1); });
|
||
shadow.querySelectorAll('.f_del').forEach(el => el.onclick = (e) => { tempExtraction.filters.splice(e.target.closest('.f_del').dataset.idx, 1); renderFilters(); });
|
||
|
||
shadow.querySelectorAll('.sp_magic_btn').forEach(btn => {
|
||
btn.onclick = (e) => {
|
||
e.stopPropagation(); const tBtn = e.target.closest('.sp_magic_btn'); const menu = shadow.querySelector('#rmenu_' + tBtn.dataset.idx); const isShow = menu.style.display === 'flex';
|
||
shadow.querySelectorAll('.sp_regex_menu').forEach(m => m.style.display = 'none');
|
||
if (!isShow) { menu.style.display = 'flex'; const cardRect = UI.filterCard.getBoundingClientRect(); const btnRect = tBtn.getBoundingClientRect(); if (cardRect.bottom - btnRect.bottom < 110) { menu.style.top = 'auto'; menu.style.bottom = 'calc(100% + 4px)'; } else { menu.style.top = 'calc(100% + 4px)'; menu.style.bottom = 'auto'; } }
|
||
};
|
||
});
|
||
shadow.querySelectorAll('.sp_regex_menu_item').forEach(item => { item.onclick = (e) => { e.stopPropagation(); tempExtraction.filters[e.target.dataset.idx].pattern = e.target.dataset.val; renderFilters(); }; });
|
||
}
|
||
|
||
shadow.addEventListener('click', () => { shadow.querySelectorAll('.sp_regex_menu').forEach(m => m.style.display = 'none'); });
|
||
shadow.querySelector('#sp_btn_add_replace').onclick = () => { tempExtraction.filters.push({type: 'replace', from:'', to:''}); renderFilters(); };
|
||
shadow.querySelector('#sp_btn_add_split').onclick = () => { tempExtraction.filters.push({type: 'split', sep:'', index:0}); renderFilters(); };
|
||
shadow.querySelector('#sp_btn_add_between').onclick = () => { tempExtraction.filters.push({type: 'between', left:'', right:''}); renderFilters(); };
|
||
shadow.querySelector('#sp_btn_add_regex').onclick = () => { tempExtraction.filters.push({type: 'regex', pattern:''}); renderFilters(); };
|
||
shadow.querySelector('#sp_btn_add_urldecode').onclick = () => { tempExtraction.filters.push({type: 'urldecode'}); renderFilters(); };
|
||
shadow.querySelector('#sp_btn_add_nospace').onclick = () => { tempExtraction.filters.push({type: 'remove_space'}); renderFilters(); };
|
||
shadow.querySelector('#sp_btn_add_prepend').onclick = () => { tempExtraction.filters.push({type: 'prepend', text:''}); renderFilters(); };
|
||
shadow.querySelector('#sp_btn_add_append').onclick = () => { tempExtraction.filters.push({type: 'append', text:''}); renderFilters(); };
|
||
shadow.querySelector('#sp_btn_add_nohtml').onclick = () => { tempExtraction.filters.push({type: 'remove_html'}); renderFilters(); };
|
||
|
||
// [折叠控制]
|
||
UI.btnParentNode.onclick = () => {
|
||
if (!currentElement || currentElement.tagName === 'BODY') return;
|
||
let boundNode = document;
|
||
if (!currentFilterPicking && TAB_SCHEMAS[currentTab].isList && currentPickingFieldId !== 'container') {
|
||
let conf = spiderConfig[currentTab]; let fSchema = TAB_SCHEMAS[currentTab].fields.find(x => x.id === currentPickingFieldId);
|
||
if (!fSchema.global && conf.container && conf.container.node) boundNode = conf.container.node;
|
||
}
|
||
if (currentElement === boundNode) { alert('[提示] 已达到约束边界!'); return; }
|
||
elementHistory.push(currentElement); currentElement = currentElement.parentElement; refreshDialog(false);
|
||
UI.dialogBox.classList.add('collapsed'); UI.btnRestore.style.display = 'block'; UI.dialogTitle.style.display = 'none'; UI.fieldHint.style.display = 'inline-block'; UI.fieldHint.style.margin = '0';
|
||
};
|
||
|
||
UI.btnChildNode.onclick = () => {
|
||
if (elementHistory.length === 0) { alert('[提示] 已经是底层的节点了!'); return; }
|
||
currentElement = elementHistory.pop(); refreshDialog(false);
|
||
UI.dialogBox.classList.add('collapsed'); UI.btnRestore.style.display = 'block'; UI.dialogTitle.style.display = 'none'; UI.fieldHint.style.display = 'inline-block'; UI.fieldHint.style.margin = '0';
|
||
};
|
||
|
||
UI.btnRestore.onclick = () => {
|
||
UI.dialogBox.classList.remove('collapsed'); UI.btnRestore.style.display = 'none'; UI.dialogTitle.style.display = 'block'; UI.fieldHint.style.display = 'inline-block'; UI.fieldHint.style.marginTop = '6px';
|
||
};
|
||
|
||
function closeDialog() {
|
||
if(currentElement) currentElement.classList.remove('spider-highlight');
|
||
UI.dialogBox.classList.remove('show');
|
||
setTimeout(() => {
|
||
UI.dialogOverlay.style.display = 'none'; resetPanelTitle();
|
||
UI.panel.classList.remove('picking-mode'); UI.panel.style.cursor = 'default';
|
||
currentPickingFieldId = null; tempExtraction = null; currentFilterPicking = null;
|
||
UI.dialogBox.classList.remove('collapsed'); // Reset
|
||
if (currentTab === 'filter') renderFilterTab(); else renderTaskList();
|
||
}, 300);
|
||
}
|
||
|
||
shadow.querySelector('#sp_btn_cancel').onclick = closeDialog;
|
||
|
||
// [核心提取保存:全自动全域映射引擎]
|
||
shadow.querySelector('#sp_btn_confirm').onclick = () => {
|
||
let safeFilters = tempExtraction.filters ? JSON.parse(JSON.stringify(tempExtraction.filters)) : [];
|
||
let eData = { type: tempExtraction.type, val: tempExtraction.val, key: tempExtraction.key, filters: safeFilters };
|
||
|
||
if (currentFilterPicking) {
|
||
let cIdx = currentFilterPicking.classIdx; let fIdx = currentFilterPicking.filterIdx; let t = currentFilterPicking.type;
|
||
let currItem = spiderConfig.filter.list[cIdx].items[fIdx];
|
||
let newSel = tempExtraction.type === 'custom' ? '' : tempExtraction.sel;
|
||
currItem[t] = { sel: newSel, extract: eData };
|
||
|
||
// 🎯 核心全域同步映射:如果当前维度属于批量嗅探出来的,且选择器包含基准特征,则强行覆盖其兄弟维度的配置
|
||
if (currItem.batchId && currItem.baseSel && newSel && newSel.startsWith(currItem.baseSel)) {
|
||
let relativeSel = newSel.substring(currItem.baseSel.length); // 切割出相对偏移路径 (例如: > li > a)
|
||
spiderConfig.filter.list[cIdx].items.forEach(sib => {
|
||
// 找到跟自己同属一批被嗅探出来、但 baseSel 不同的兄弟,强行套用相对路径和清洗规则
|
||
if (sib.batchId === currItem.batchId && sib !== currItem && sib.baseSel) {
|
||
sib[t] = { sel: sib.baseSel + relativeSel, extract: JSON.parse(JSON.stringify(eData)) };
|
||
}
|
||
});
|
||
}
|
||
saveConfig(); closeDialog(); return;
|
||
}
|
||
|
||
const conf = spiderConfig[currentTab];
|
||
if (currentPickingFieldId === 'container') { conf[currentPickingFieldId].sel = tempExtraction.absSel; conf[currentPickingFieldId].node = currentElement; }
|
||
else { conf[currentPickingFieldId].sel = tempExtraction.type === 'custom' ? '' : tempExtraction.sel; conf[currentPickingFieldId].extract = eData; }
|
||
if (conf._reuse) conf._reuse = null;
|
||
saveConfig(); closeDialog();
|
||
};
|
||
|
||
// =========================================================================
|
||
// === [BLOCK 6: 终极代码生成与编译引擎] ===
|
||
// =========================================================================
|
||
function applyFilters(val, filters) {
|
||
let res = val || ''; if (typeof res !== 'string') res = String(res);
|
||
if (!filters || filters.length === 0) return res.trim();
|
||
filters.forEach(flt => {
|
||
if (flt.type === 'replace' && flt.from) res = res.split(flt.from).join(flt.to || '');
|
||
else if (flt.type === 'split' && flt.sep) { let idx = parseInt(flt.index)||0; res = res.split(flt.sep)[idx] !== undefined ? res.split(flt.sep)[idx] : ''; }
|
||
else if (flt.type === 'between' && flt.left && flt.right) { if(res.includes(flt.left)){ res = res.split(flt.left)[1].split(flt.right)[0] || ''; } else { res = ''; } }
|
||
else if (flt.type === 'urldecode') { try { res = decodeURIComponent(res); } catch(e){} }
|
||
else if (flt.type === 'regex' && flt.pattern) { try { let m = res.match(new RegExp(flt.pattern)); res = m ? m[0] : ''; } catch(e) {} }
|
||
else if (flt.type === 'remove_space') res = res.replace(/[\s\u3000]+/g, '');
|
||
else if (flt.type === 'prepend') res = (flt.text || '') + res;
|
||
else if (flt.type === 'append') res = res + (flt.text || '');
|
||
else if (flt.type === 'remove_html') res = res.replace(/<[^>]+>/g, '');
|
||
});
|
||
return res.trim();
|
||
}
|
||
|
||
function genFiltersCode(vName, filters, indNum) {
|
||
if (!filters || filters.length === 0) return '';
|
||
let iStr = ' '.repeat(indNum); let c = '';
|
||
filters.forEach(flt => {
|
||
if (flt.type === 'replace' && flt.from) c += `${iStr}${vName} = String(${vName}).split('${flt.from.replace(/'/g, "\\'")}').join('${(flt.to||'').replace(/'/g, "\\'")}');\n`;
|
||
else if (flt.type === 'split' && flt.sep) c += `${iStr}${vName} = String(${vName}).split('${flt.sep.replace(/'/g, "\\'")}')[${parseInt(flt.index)||0}] || '';\n`;
|
||
else if (flt.type === 'between' && flt.left && flt.right) c += `${iStr}if(String(${vName}).includes('${flt.left.replace(/'/g, "\\'")}')){ ${vName} = String(${vName}).split('${flt.left.replace(/'/g, "\\'")}')[1].split('${flt.right.replace(/'/g, "\\'")}')[0] || ''; } else { ${vName} = ''; }\n`;
|
||
else if (flt.type === 'urldecode') c += `${iStr}try { ${vName} = decodeURIComponent(${vName}); } catch(e) {}\n`;
|
||
else if (flt.type === 'regex' && flt.pattern) c += `${iStr}${vName} = String(${vName}).match(/${flt.pattern.replace(/\\/g, "\\\\").replace(/\//g, "\\/")}/)?.[0] || '';\n`;
|
||
else if (flt.type === 'remove_space') c += `${iStr}${vName} = String(${vName}).replace(/[\\s\\u3000]+/g, '');\n`;
|
||
else if (flt.type === 'prepend') c += `${iStr}${vName} = '${(flt.text||'').replace(/'/g, "\\'")}' + String(${vName});\n`;
|
||
else if (flt.type === 'append') c += `${iStr}${vName} = String(${vName}) + '${(flt.text||'').replace(/'/g, "\\'")}';\n`;
|
||
else if (flt.type === 'remove_html') c += `${iStr}${vName} = String(${vName}).replace(/<[^>]+>/g, '');\n`;
|
||
});
|
||
return c;
|
||
}
|
||
|
||
function generateParseListCodeStr(tabName) {
|
||
let conf = spiderConfig[tabName]; let schema = TAB_SCHEMAS[tabName];
|
||
let c = `function parseVideoList() {\n var vod = [];\n var items = document.querySelectorAll('${conf.container.sel}');\n items.forEach(function(item) {\n`;
|
||
let vodKeys = [];
|
||
schema.fields.forEach(f => {
|
||
if (f.id === 'container' || f.global || !conf[f.id].extract) return;
|
||
let data = conf[f.id];
|
||
if (data.extract.type === 'custom') { c += ` var ${f.id} = '${(data.extract.val||'').replace(/'/g, "\\'")}';\n`; }
|
||
else {
|
||
let queryCode = data.sel ? `item.querySelector('${data.sel}')` : `item`;
|
||
let exCode = data.extract.type === 'text' ? `?.textContent` : data.extract.type === 'html' ? `?.innerHTML` : data.extract.type === 'outerhtml' ? `?.outerHTML` : `?.getAttribute('${data.extract.key}')`;
|
||
c += ` var ${f.id} = ${queryCode}${exCode} || '';\n`;
|
||
}
|
||
c += genFiltersCode(f.id, data.extract.filters, 8); c += ` ${f.id} = String(${f.id}).trim();\n`; vodKeys.push(f.id);
|
||
});
|
||
c += ` var _item_data = { ${vodKeys.map(k => `${k}: ${k}`).join(', ')} };\n`;
|
||
c += ` if (Object.values(_item_data).some(function(v) { return v !== ''; })) { vod.push(_item_data); }\n });\n return vod;\n}\n`;
|
||
return c;
|
||
}
|
||
|
||
function buildStaticFilterConfig() {
|
||
let fConf = { class: [], filters: {} };
|
||
spiderConfig.filter.list.forEach(cls => {
|
||
if (!cls.val || cls.val.trim() === '') return;
|
||
let cId = cls.val.trim();
|
||
if (cls.name) fConf.class.push({ type_id: cId, type_name: cls.name.trim() });
|
||
|
||
let fArr = [];
|
||
cls.items.forEach((item, fIdx) => {
|
||
let k = item.key || 'key_' + fIdx; let nm = item.name || '未知';
|
||
let vList = [];
|
||
let nNodes = item.n && item.n.extract && item.n.extract.type !== 'custom' && item.n.sel ? document.querySelectorAll(item.n.sel) : [];
|
||
let vNodes = item.v && item.v.extract && item.v.extract.type !== 'custom' && item.v.sel ? document.querySelectorAll(item.v.sel) : [];
|
||
let mx = Math.max(item.n && item.n.extract && item.n.extract.type === 'custom' ? 1 : nNodes.length, item.v && item.v.extract && item.v.extract.type === 'custom' ? 1 : vNodes.length);
|
||
for(let i=0; i<mx; i++) {
|
||
let nR = '', vR = '';
|
||
if (item.n && item.n.extract) {
|
||
if (item.n.extract.type === 'custom') nR = item.n.extract.val;
|
||
else if (nNodes[i]) { let type = item.n.extract.type; nR = type === 'text' ? nNodes[i].textContent : type === 'html' ? nNodes[i].innerHTML : type === 'outerhtml' ? nNodes[i].outerHTML : nNodes[i].getAttribute(item.n.extract.key); }
|
||
nR = applyFilters(nR, item.n.extract.filters);
|
||
}
|
||
if (item.v && item.v.extract) {
|
||
if (item.v.extract.type === 'custom') vR = item.v.extract.val;
|
||
else if (vNodes[i]) { let type = item.v.extract.type; vR = type === 'text' ? vNodes[i].textContent : type === 'html' ? vNodes[i].innerHTML : type === 'outerhtml' ? vNodes[i].outerHTML : vNodes[i].getAttribute(item.v.extract.key); }
|
||
vR = applyFilters(vR, item.v.extract.filters);
|
||
}
|
||
// 只录入非空的值,避免脏数据写入
|
||
if (nR || vR) vList.push({ n: nR||vR, v: vR||'' });
|
||
}
|
||
if (vList.length > 0) fArr.push({ key: k, name: nm, value: vList });
|
||
});
|
||
if (fArr.length > 0) fConf.filters[cId] = fArr;
|
||
});
|
||
return fConf;
|
||
}
|
||
|
||
shadow.querySelector('#sp_run_test').onclick = () => {
|
||
UI.outHeader.style.display = 'flex'; const codeTabEl = shadow.querySelector('[data-target="sp_code_output"]');
|
||
|
||
if (currentTab === 'global') {
|
||
codeTabEl.textContent = '完整脚本'; let g = spiderConfig.global;
|
||
let fullCode = `/**\n * Spider UI 规则\n * @config\n * debug: true\n * returnType: dom\n */\n\nvar BASE = '${g.base || ''}';\n\nfunction init(cfg) {\n return {};\n}\n\n`;
|
||
fullCode += `// 1. 路由配置\nvar spider = {\n routes: {\n`;
|
||
const fmt = (str) => "'" + str.replace(/'/g, "\\'").replace(/\{BASE\}/g, "' + BASE + '") + "'";
|
||
if (g.home) fullCode += ` homeContent: function() {\n return ${fmt(g.home)};\n },\n homeVideoContent: function() {\n return ${fmt(g.home)};\n },\n`;
|
||
if (g.category) {
|
||
let s = g.category.replace(/'/g, "\\'").replace(/\{BASE\}/g, "' + BASE + '").replace(/\{tid\}/g, "' + tid + '").replace(/\{pg\}/g, "' + pg + '");
|
||
fullCode += ` categoryContent: function(tid, pg, filter, extend) {\n var url = '${s}';\n`;
|
||
if (g.cat_pg1) fullCode += ` if (parseInt(pg) === 1) { url = url.replace(/-1\\.html/g, '.html').replace(/\\/page\\/1/g, ''); }\n`;
|
||
fullCode += ` return url;\n },\n`;
|
||
}
|
||
if (g.detail) {
|
||
let detUrl = g.detail.replace(/'/g, "\\'").replace(/\{BASE\}/g, "' + BASE + '").replace(/\{id\}/g, "' + id + '");
|
||
fullCode += ` detailContent: function(ids) {\n var id = Array.isArray(ids) ? ids[0] || '' : ids || '';\n return '${detUrl}';\n },\n`;
|
||
}
|
||
if (g.search) {
|
||
let sUrl = g.search.replace(/'/g, "\\'").replace(/\{BASE\}/g, "' + BASE + '").replace(/\{wd\}/g, "' + encodeURIComponent(wd) + '").replace(/\{pg\}/g, "' + pg + '");
|
||
fullCode += ` searchContent: function(wd, pg) {\n var url = '${sUrl}';\n`;
|
||
if (g.search_pg1) fullCode += ` if (parseInt(pg) === 1) { url = url.replace(/-1\\.html/g, '.html').replace(/\\/page\\/1/g, ''); }\n`;
|
||
fullCode += ` return url;\n }\n`;
|
||
}
|
||
fullCode += ` }\n};\n\n`; fullCode = fullCode.replace(/,\n \}/g, '\n }');
|
||
|
||
fullCode += `// 2. 数据提取\nfunction homeContent(filter) {\n`;
|
||
let fConfig = buildStaticFilterConfig();
|
||
fullCode += ` var filterConfig = ${JSON.stringify(fConfig, null, 4)};\n return filterConfig;\n}\n\n`;
|
||
|
||
let genF = [];
|
||
function addP(tN) {
|
||
if (!spiderConfig[tN].container || !spiderConfig[tN].container.sel) return null;
|
||
let bd = generateParseListCodeStr(tN).replace(/function parseVideoList\(\) \{\n/, '').replace(/\}$/, '');
|
||
let wrap = `function _temp_name_() {\n${bd}}\n`;
|
||
for(let z of genF) { if (z.body.replace(/_temp_name_/g,'') === wrap.replace(/_temp_name_/g,'')) return z.name; }
|
||
let nm = 'parseVideoList' + (genF.length ? genF.length + 1 : '');
|
||
if(genF.length===0) nm = 'parseVideoList'; genF.push({ name: nm, body: wrap.replace(/_temp_name_/g, nm) }); return nm;
|
||
}
|
||
|
||
let hn = addP('home'); fullCode += `function homeVideoContent() {\n return { list: ${hn ? hn+'()' : '[]'} };\n}\n\n`;
|
||
let cn = addP('category'); fullCode += `function categoryContent(tid, pg, filter, extend) {\n`;
|
||
['pagecount', 'limit', 'total'].forEach(k => {
|
||
if (spiderConfig.category[k] && spiderConfig.category[k].extract) {
|
||
let pd = spiderConfig.category[k];
|
||
if (pd.extract.type === 'custom') fullCode += ` var _${k}_raw = '${(pd.extract.val||'').replace(/'/g, "\\'")}';\n`;
|
||
else {
|
||
let pq = pd.sel ? `document.querySelector('${pd.sel}')` : `document`; let xC = pd.extract.type === 'text' ? `?.textContent` : pd.extract.type === 'html' ? `?.innerHTML` : pd.extract.type === 'outerhtml' ? `?.outerHTML` : `?.getAttribute('${pd.extract.key}')`;
|
||
fullCode += ` var _${k}_raw = ${pq}${xC} || '';\n`;
|
||
}
|
||
fullCode += ` var _${k} = _${k}_raw;\n`; fullCode += genFiltersCode(`_${k}`, pd.extract.filters, 4); fullCode += ` _${k} = parseInt(String(_${k}).match(/\\d+/)?.[0]) || 0;\n\n`;
|
||
}
|
||
});
|
||
fullCode += ` var vod = ${cn ? cn+'()' : '[]'};\n return {\n code: 1,\n msg: "数据列表",\n page: typeof pg !== 'undefined' ? pg : 1,\n pagecount: ${spiderConfig.category.pagecount?.extract ? '_pagecount' : '99'},\n limit: ${spiderConfig.category.limit?.extract ? '_limit' : 'vod.length'},\n total: ${spiderConfig.category.total?.extract ? '_total' : 'vod.length'},\n list: vod\n };\n}\n\n`;
|
||
|
||
fullCode += `function detailContent(ids) {\n var detail = {};\n`;
|
||
TAB_SCHEMAS.detail.fields.forEach(f => {
|
||
if (f.isPlay || !spiderConfig.detail[f.id].extract) return;
|
||
let dt = spiderConfig.detail[f.id];
|
||
if (dt.extract.type === 'custom') fullCode += ` detail.${f.id} = '${(dt.extract.val||'').replace(/'/g, "\\'")}';\n`;
|
||
else { let qc = `document.querySelector('${dt.sel}')`; let xC = dt.extract.type === 'text' ? `?.textContent` : dt.extract.type === 'html' ? `?.innerHTML` : dt.extract.type === 'outerhtml' ? `?.outerHTML` : `?.getAttribute('${dt.extract.key}')`; fullCode += ` detail.${f.id} = ${qc}${xC} || '';\n`; }
|
||
fullCode += genFiltersCode(`detail.${f.id}`, dt.extract.filters, 4); fullCode += ` detail.${f.id} = String(detail.${f.id}).trim();\n\n`;
|
||
});
|
||
let dConf = spiderConfig.detail;
|
||
if (dConf.vod_play_title && dConf.vod_play_url && dConf.vod_play_title.extract && dConf.vod_play_url.extract) {
|
||
fullCode += ` var playFroms = []; var playUrls = [];\n`;
|
||
fullCode += ` var fromNodes = ${dConf.vod_play_from && dConf.vod_play_from.extract && dConf.vod_play_from.extract.type !== 'custom' && dConf.vod_play_from.sel ? `document.querySelectorAll('${dConf.vod_play_from.sel}')` : `[]`};\n`;
|
||
fullCode += ` var titleNodes = ${dConf.vod_play_title.extract.type !== 'custom' && dConf.vod_play_title.sel ? `document.querySelectorAll('${dConf.vod_play_title.sel}')` : `[]`};\n`;
|
||
fullCode += ` var urlNodes = ${dConf.vod_play_url.extract.type !== 'custom' && dConf.vod_play_url.sel ? `document.querySelectorAll('${dConf.vod_play_url.sel}')` : `[]`};\n`;
|
||
fullCode += ` var baseNodes = titleNodes.length > 0 ? titleNodes : urlNodes; var epGroups = []; var currentGroup = []; var lastContainer = null;\n for(var j=0; j<baseNodes.length; j++) { var node = baseNodes[j]; var container = node.closest('ul') || node.closest('div[class*="list"]') || node.closest('.stui-content__playlist') || node.parentElement.parentElement;\n if (lastContainer && container !== lastContainer) { epGroups.push(currentGroup); currentGroup = []; } currentGroup.push(j); lastContainer = container; } if (currentGroup.length > 0) epGroups.push(currentGroup);\n var maxLen = fromNodes.length > 0 ? Math.min(fromNodes.length, epGroups.length) : epGroups.length;\n for(var i=0; i<maxLen; i++) {\n`;
|
||
if (dConf.vod_play_from && dConf.vod_play_from.extract) {
|
||
if (dConf.vod_play_from.extract.type === 'custom') fullCode += ` var fromName = '${(dConf.vod_play_from.extract.val||'').replace(/'/g, "\\'")}';\n`;
|
||
else { let fEx = dConf.vod_play_from.extract.type === 'text' ? `?.textContent` : dConf.vod_play_from.extract.type === 'html' ? `?.innerHTML` : dConf.vod_play_from.extract.type === 'outerhtml' ? `?.outerHTML` : `?.getAttribute('${dConf.vod_play_from.extract.key}')`; fullCode += ` var fromName = fromNodes[i]${fEx} || '未知线路';\n`; }
|
||
fullCode += genFiltersCode('fromName', dConf.vod_play_from.extract.filters, 8); fullCode += ` fromName = String(fromName).trim() || '未知线路';\n`;
|
||
} else fullCode += ` var fromName = '未知线路';\n`;
|
||
fullCode += ` var epList = []; var groupIndices = epGroups[i] || [];\n for(var k=0; k<groupIndices.length; k++) { var gIdx = groupIndices[k];\n`;
|
||
if (dConf.vod_play_title.extract.type === 'custom') fullCode += ` var epTitle = '${(dConf.vod_play_title.extract.val||'').replace(/'/g, "\\'")}';\n`;
|
||
else { let tEx = dConf.vod_play_title.extract.type === 'text' ? `?.textContent` : dConf.vod_play_title.extract.type === 'html' ? `?.innerHTML` : dConf.vod_play_title.extract.type === 'outerhtml' ? `?.outerHTML` : `?.getAttribute('${dConf.vod_play_title.extract.key}')`; fullCode += ` var epTitle = titleNodes[gIdx]${tEx} || '';\n`; }
|
||
fullCode += genFiltersCode('epTitle', dConf.vod_play_title.extract.filters, 12); fullCode += ` epTitle = String(epTitle).trim();\n`;
|
||
if (dConf.vod_play_url.extract.type === 'custom') fullCode += ` var epUrl = '${(dConf.vod_play_url.extract.val||'').replace(/'/g, "\\'")}';\n`;
|
||
else { let uEx = dConf.vod_play_url.extract.type === 'text' ? `?.textContent` : dConf.vod_play_url.extract.type === 'html' ? `?.innerHTML` : dConf.vod_play_url.extract.type === 'outerhtml' ? `?.outerHTML` : `?.getAttribute('${dConf.vod_play_url.extract.key}')`; fullCode += ` var epUrl = urlNodes[gIdx]${uEx} || '';\n`; }
|
||
fullCode += genFiltersCode('epUrl', dConf.vod_play_url.extract.filters, 12); fullCode += ` epUrl = String(epUrl).trim();\n`;
|
||
fullCode += ` if(epTitle || epUrl) epList.push((epTitle||'未知') + '$' + epUrl);\n } if (epList.length > 0) { playFroms.push(fromName); playUrls.push(epList.join('#')); }\n }\n detail.vod_play_from = playFroms.join('$$$'); detail.vod_play_url = playUrls.join('$$$');\n`;
|
||
}
|
||
fullCode += ` return { list: [detail] };\n}\n\n`;
|
||
|
||
let sn = addP('search');
|
||
fullCode += `function searchContent(wd, pg) {\n`;
|
||
['pagecount', 'limit', 'total'].forEach(k => {
|
||
if (spiderConfig.search[k] && spiderConfig.search[k].extract) {
|
||
let pd = spiderConfig.search[k];
|
||
if (pd.extract.type === 'custom') fullCode += ` var _${k}_raw = '${(pd.extract.val||'').replace(/'/g, "\\'")}';\n`;
|
||
else {
|
||
let pq = pd.sel ? `document.querySelector('${pd.sel}')` : `document`; let xC = pd.extract.type === 'text' ? `?.textContent` : pd.extract.type === 'html' ? `?.innerHTML` : pd.extract.type === 'outerhtml' ? `?.outerHTML` : `?.getAttribute('${pd.extract.key}')`;
|
||
fullCode += ` var _${k}_raw = ${pq}${xC} || '';\n`;
|
||
}
|
||
fullCode += ` var _${k} = _${k}_raw;\n`; fullCode += genFiltersCode(`_${k}`, pd.extract.filters, 4); fullCode += ` _${k} = parseInt(String(_${k}).match(/\\d+/)?.[0]) || 0;\n\n`;
|
||
}
|
||
});
|
||
fullCode += ` var vod = ${sn ? sn+'()' : '[]'};\n return {\n code: 1,\n msg: "搜索结果",\n page: typeof pg !== 'undefined' ? pg : 1,\n pagecount: ${spiderConfig.search.pagecount?.extract ? '_pagecount' : '99'},\n limit: ${spiderConfig.search.limit?.extract ? '_limit' : 'vod.length'},\n total: ${spiderConfig.search.total?.extract ? '_total' : 'vod.length'},\n list: vod\n };\n}\n\n`;
|
||
|
||
if (genF.length > 0) { fullCode += `// 3. 公共解析\n`; genF.forEach(f => { fullCode += `${f.body}\n`; }); }
|
||
|
||
UI.codeOutput.value = fullCode; UI.testResult.textContent = "[成功] 全局编译完成!"; UI.outTabs[1].click(); return;
|
||
}
|
||
|
||
// ==== [局部测试与方法生成] ====
|
||
codeTabEl.textContent = currentTab === 'filter' ? '静态配置' : '方法代码';
|
||
const schema = TAB_SCHEMAS[currentTab], conf = spiderConfig[currentTab]; let tabCode = '';
|
||
try {
|
||
let liveResult = null;
|
||
if (currentTab === 'filter') {
|
||
let sData = buildStaticFilterConfig(); liveResult = sData;
|
||
tabCode = `// 首页静态过滤配置\nfunction homeContent(filter) {\n var filterConfig = ${JSON.stringify(sData, null, 4)};\n return filterConfig;\n}\n`;
|
||
} else if (schema.isList) {
|
||
let resArray = [];
|
||
if (conf.container && conf.container.sel) {
|
||
const items = document.querySelectorAll(conf.container.sel);
|
||
for(let i=0; i<items.length; i++) {
|
||
let item = items[i], obj = {}, _isEmpty = true;
|
||
schema.fields.forEach(f => {
|
||
if (f.id === 'container' || f.global || !conf[f.id].extract) return;
|
||
let rawVal; if (conf[f.id].extract.type === 'custom') rawVal = conf[f.id].extract.val;
|
||
else { let targetEl = conf[f.id].sel ? item.querySelector(conf[f.id].sel) : item; rawVal = conf[f.id].extract.type === 'text' ? targetEl?.textContent : conf[f.id].extract.type === 'html' ? targetEl?.innerHTML : conf[f.id].extract.type === 'outerhtml' ? targetEl?.outerHTML : targetEl?.getAttribute(conf[f.id].extract.key); }
|
||
obj[f.id] = applyFilters(rawVal, conf[f.id].extract.filters); if (obj[f.id] !== '') _isEmpty = false;
|
||
});
|
||
if (!_isEmpty) resArray.push(obj);
|
||
}
|
||
}
|
||
let globalData = {};
|
||
['pagecount', 'limit', 'total'].forEach(k => {
|
||
if (conf[k] && conf[k].extract) {
|
||
let rawVal; if (conf[k].extract.type === 'custom') rawVal = conf[k].extract.val;
|
||
else { let el = conf[k].sel ? document.querySelector(conf[k].sel) : document; rawVal = conf[k].extract.type === 'text' ? el?.textContent : conf[k].extract.type === 'html' ? el?.innerHTML : conf[k].extract.type === 'outerhtml' ? el?.outerHTML : el?.getAttribute(conf[k].extract.key); }
|
||
let finalVal = applyFilters(rawVal, conf[k].extract.filters); let parsedNum = parseInt(String(finalVal).match(/\d+/)?.[0]);
|
||
if (!isNaN(parsedNum)) globalData[k] = parsedNum; else globalData[k] = `[异常] "${finalVal}"`;
|
||
}
|
||
});
|
||
liveResult = (currentTab === 'category' || currentTab === 'search') ? { code: 1, msg: "数据列表", page: "pg (运行时传入)", pagecount: globalData.pagecount || 99, limit: globalData.limit !== undefined ? globalData.limit : resArray.length, total: globalData.total !== undefined ? globalData.total : resArray.length, list: resArray } : { msg: `匹配到 ${resArray.length} 个项目`, list: resArray };
|
||
|
||
if (currentTab === 'home') { tabCode += `function homeVideoContent() {\n var vod = parseVideoList();\n return { list: vod };\n}\n\n`; }
|
||
else if (currentTab === 'category' || currentTab === 'search') {
|
||
let fName = currentTab === 'category' ? 'categoryContent(tid, pg, filter, extend)' : 'searchContent(wd, pg)';
|
||
tabCode += `function ${fName} {\n`;
|
||
['pagecount', 'limit', 'total'].forEach(k => {
|
||
if (conf[k] && conf[k].extract) {
|
||
let pd = conf[k];
|
||
if (pd.extract.type === 'custom') { tabCode += ` var _${k}_raw = '${(pd.extract.val||'').replace(/'/g, "\\'")}';\n`; }
|
||
else {
|
||
let pq = pd.sel ? `document.querySelector('${pd.sel}')` : `document`;
|
||
let pExCode = pd.extract.type === 'text' ? `?.textContent` : pd.extract.type === 'html' ? `?.innerHTML` : pd.extract.type === 'outerhtml' ? `?.outerHTML` : `?.getAttribute('${pd.extract.key}')`;
|
||
tabCode += ` var _${k}_raw = ${pq}${pExCode} || '';\n`;
|
||
}
|
||
tabCode += ` var _${k} = _${k}_raw;\n`; tabCode += genFiltersCode(`_${k}`, pd.extract.filters, 4);
|
||
tabCode += ` _${k} = parseInt(String(_${k}).match(/\\d+/)?.[0]) || 0;\n\n`;
|
||
}
|
||
});
|
||
tabCode += ` var vod = parseVideoList();\n return {\n code: 1,\n msg: "列表",\n page: typeof pg !== 'undefined' ? pg : 1,\n pagecount: ${conf.pagecount?.extract ? '_pagecount' : '99'},\n limit: ${conf.limit?.extract ? '_limit' : 'vod.length'},\n total: ${conf.total?.extract ? '_total' : 'vod.length'},\n list: vod\n };\n}\n\n`;
|
||
}
|
||
if (conf.container && conf.container.sel) tabCode += generateParseListCodeStr(currentTab);
|
||
|
||
} else {
|
||
let detailObj = {};
|
||
schema.fields.forEach(f => {
|
||
if (f.isPlay || !conf[f.id].extract) return;
|
||
let rawVal; if (conf[f.id].extract.type === 'custom') rawVal = conf[f.id].extract.val;
|
||
else { let el = document.querySelector(conf[f.id].sel); rawVal = conf[f.id].extract.type === 'text' ? el?.textContent : conf[f.id].extract.type === 'html' ? el?.innerHTML : conf[f.id].extract.type === 'outerhtml' ? el?.outerHTML : el?.getAttribute(conf[f.id].extract.key); }
|
||
detailObj[f.id] = applyFilters(rawVal, conf[f.id].extract.filters);
|
||
});
|
||
if (conf.vod_play_title && conf.vod_play_url && conf.vod_play_title.extract && conf.vod_play_url.extract) {
|
||
let playFroms = []; let playUrls = [];
|
||
let fromNodes = conf.vod_play_from?.extract?.type !== 'custom' && conf.vod_play_from?.sel ? document.querySelectorAll(conf.vod_play_from.sel) : [];
|
||
let titleNodes = conf.vod_play_title.extract.type !== 'custom' && conf.vod_play_title.sel ? document.querySelectorAll(conf.vod_play_title.sel) : [];
|
||
let urlNodes = conf.vod_play_url.extract.type !== 'custom' && conf.vod_play_url.sel ? document.querySelectorAll(conf.vod_play_url.sel) : [];
|
||
let baseNodes = titleNodes.length > 0 ? titleNodes : urlNodes; let epGroups = []; let currentGroup = []; let lastContainer = null;
|
||
for(let j=0; j<baseNodes.length; j++) { let node = baseNodes[j]; let container = node.closest('ul') || node.closest('div[class*="list"]') || node.closest('.stui-content__playlist') || node.parentElement.parentElement; if (lastContainer && container !== lastContainer) { epGroups.push(currentGroup); currentGroup = []; } currentGroup.push(j); lastContainer = container; } if (currentGroup.length > 0) epGroups.push(currentGroup);
|
||
let maxLen = fromNodes.length > 0 ? Math.min(fromNodes.length, epGroups.length) : epGroups.length;
|
||
for(let i=0; i<maxLen; i++) {
|
||
let fromRaw = conf.vod_play_from?.extract?.type === 'custom' ? conf.vod_play_from.extract.val : (conf.vod_play_from?.extract?.type === 'text' ? fromNodes[i]?.textContent : (conf.vod_play_from?.extract?.type === 'html' ? fromNodes[i]?.innerHTML : (conf.vod_play_from?.extract?.type === 'outerhtml' ? fromNodes[i]?.outerHTML : fromNodes[i]?.getAttribute(conf.vod_play_from?.extract?.key))));
|
||
let fromName = applyFilters(fromRaw, conf.vod_play_from?.extract?.filters) || '未知线路';
|
||
let epList = []; let groupIndices = epGroups[i] || [];
|
||
for(let k=0; k<groupIndices.length; k++) {
|
||
let j = groupIndices[k];
|
||
let tRaw = conf.vod_play_title.extract.type === 'custom' ? conf.vod_play_title.extract.val : (conf.vod_play_title.extract.type === 'text' ? titleNodes[j]?.textContent : (conf.vod_play_title.extract.type === 'html' ? titleNodes[j]?.innerHTML : (conf.vod_play_title.extract.type === 'outerhtml' ? titleNodes[j]?.outerHTML : titleNodes[j]?.getAttribute(conf.vod_play_title.extract.key)))); let title = applyFilters(tRaw, conf.vod_play_title.extract.filters);
|
||
let uRaw = conf.vod_play_url.extract.type === 'custom' ? conf.vod_play_url.extract.val : (conf.vod_play_url.extract.type === 'text' ? urlNodes[j]?.textContent : (conf.vod_play_url.extract.type === 'html' ? urlNodes[j]?.innerHTML : (conf.vod_play_url.extract.type === 'outerhtml' ? urlNodes[j]?.outerHTML : urlNodes[j]?.getAttribute(conf.vod_play_url.extract.key)))); let url = applyFilters(uRaw, conf.vod_play_url.extract.filters);
|
||
if(title || url) epList.push((title||'未知') + '$' + (url||''));
|
||
}
|
||
if (epList.length > 0) { playFroms.push(fromName); playUrls.push(epList.join('#')); }
|
||
}
|
||
detailObj.vod_play_from = playFroms.join('$$$'); detailObj.vod_play_url = playUrls.join('$$$');
|
||
}
|
||
liveResult = { msg: '详情页解析成功', list: [detailObj] };
|
||
|
||
tabCode += `function detailContent(ids) {\n var detail = {};\n`;
|
||
schema.fields.forEach(f => {
|
||
if (f.isPlay || !conf[f.id].extract) return;
|
||
let dt = conf[f.id];
|
||
if (dt.extract.type === 'custom') { tabCode += ` detail.${f.id} = '${(dt.extract.val||'').replace(/'/g, "\\'")}';\n`; }
|
||
else { let qc = `document.querySelector('${dt.sel}')`; let exCode = dt.extract.type === 'text' ? `?.textContent` : dt.extract.type === 'html' ? `?.innerHTML` : dt.extract.type === 'outerhtml' ? `?.outerHTML` : `?.getAttribute('${dt.extract.key}')`; tabCode += ` detail.${f.id} = ${qc}${exCode} || '';\n`; }
|
||
tabCode += genFiltersCode(`detail.${f.id}`, dt.extract.filters, 4); tabCode += ` detail.${f.id} = String(detail.${f.id}).trim();\n\n`;
|
||
});
|
||
if (conf.vod_play_title && conf.vod_play_url && conf.vod_play_title.extract && conf.vod_play_url.extract) {
|
||
tabCode += ` var playFroms = []; var playUrls = [];\n var fromNodes = ${conf.vod_play_from && conf.vod_play_from.extract && conf.vod_play_from.extract.type !== 'custom' && conf.vod_play_from.sel ? `document.querySelectorAll('${conf.vod_play_from.sel}')` : `[]`};\n var titleNodes = ${conf.vod_play_title.extract.type !== 'custom' && conf.vod_play_title.sel ? `document.querySelectorAll('${conf.vod_play_title.sel}')` : `[]`};\n var urlNodes = ${conf.vod_play_url.extract.type !== 'custom' && conf.vod_play_url.sel ? `document.querySelectorAll('${conf.vod_play_url.sel}')` : `[]`};\n var baseNodes = titleNodes.length > 0 ? titleNodes : urlNodes; var epGroups = []; var currentGroup = []; var lastContainer = null;\n for(var j=0; j<baseNodes.length; j++) { var node = baseNodes[j]; var container = node.closest('ul') || node.closest('div[class*="list"]') || node.closest('.stui-content__playlist') || node.parentElement.parentElement;\n if (lastContainer && container !== lastContainer) { epGroups.push(currentGroup); currentGroup = []; } currentGroup.push(j); lastContainer = container; } if (currentGroup.length > 0) epGroups.push(currentGroup);\n var maxLen = fromNodes.length > 0 ? Math.min(fromNodes.length, epGroups.length) : epGroups.length;\n for(var i=0; i<maxLen; i++) {\n`;
|
||
if (conf.vod_play_from && conf.vod_play_from.extract) {
|
||
if (conf.vod_play_from.extract.type === 'custom') { tabCode += ` var fromName = '${(conf.vod_play_from.extract.val||'').replace(/'/g, "\\'")}';\n`; }
|
||
else { let fEx = conf.vod_play_from.extract.type === 'text' ? `?.textContent` : conf.vod_play_from.extract.type === 'html' ? `?.innerHTML` : conf.vod_play_from.extract.type === 'outerhtml' ? `?.outerHTML` : `?.getAttribute('${conf.vod_play_from.extract.key}')`; tabCode += ` var fromName = fromNodes[i]${fEx} || '未知线路';\n`; }
|
||
tabCode += genFiltersCode('fromName', conf.vod_play_from.extract.filters, 8); tabCode += ` fromName = String(fromName).trim() || '未知线路';\n\n`;
|
||
} else { tabCode += ` var fromName = '未知线路';\n\n`; }
|
||
tabCode += ` var epList = []; var groupIndices = epGroups[i] || [];\n for(var k=0; k<groupIndices.length; k++) { var gIdx = groupIndices[k];\n`;
|
||
if (conf.vod_play_title.extract.type === 'custom') { tabCode += ` var epTitle = '${(conf.vod_play_title.extract.val||'').replace(/'/g, "\\'")}';\n`; }
|
||
else { let tEx = conf.vod_play_title.extract.type === 'text' ? `?.textContent` : conf.vod_play_title.extract.type === 'html' ? `?.innerHTML` : conf.vod_play_title.extract.type === 'outerhtml' ? `?.outerHTML` : `?.getAttribute('${conf.vod_play_title.extract.key}')`; tabCode += ` var epTitle = titleNodes[gIdx]${tEx} || '';\n`; }
|
||
tabCode += genFiltersCode('epTitle', conf.vod_play_title.extract.filters, 12); tabCode += ` epTitle = String(epTitle).trim();\n\n`;
|
||
if (conf.vod_play_url.extract.type === 'custom') { tabCode += ` var epUrl = '${(conf.vod_play_url.extract.val||'').replace(/'/g, "\\'")}';\n`; }
|
||
else { let uEx = conf.vod_play_url.extract.type === 'text' ? `?.textContent` : conf.vod_play_url.extract.type === 'html' ? `?.innerHTML` : conf.vod_play_url.extract.type === 'outerhtml' ? `?.outerHTML` : `?.getAttribute('${conf.vod_play_url.extract.key}')`; tabCode += ` var epUrl = urlNodes[gIdx]${uEx} || '';\n`; }
|
||
tabCode += genFiltersCode('epUrl', conf.vod_play_url.extract.filters, 12); tabCode += ` epUrl = String(epUrl).trim();\n\n`;
|
||
tabCode += ` if(epTitle || epUrl) epList.push((epTitle||'未知') + '$' + epUrl);\n }\n if (epList.length > 0) { playFroms.push(fromName); playUrls.push(epList.join('#')); }\n }\n detail.vod_play_from = playFroms.join('$$$');\n detail.vod_play_url = playUrls.join('$$$');\n\n`;
|
||
}
|
||
tabCode += ` return { list: [detail] };\n}\n`;
|
||
}
|
||
UI.testResult.textContent = typeof liveResult === 'string' ? liveResult : JSON.stringify(liveResult, null, 2);
|
||
UI.codeOutput.value = tabCode; UI.outTabs[0].click();
|
||
} catch(e) {
|
||
UI.testResult.textContent = '[错误] 提取出错: ' + e.message; UI.outTabs[0].click();
|
||
}
|
||
};
|
||
|
||
// =========================================================================
|
||
// === [BLOCK 7: 面板开关与系统启动] ===
|
||
// =========================================================================
|
||
UI.fab.onclick = () => {
|
||
UI.panel.style.display = 'flex';
|
||
setTimeout(() => UI.panel.classList.add('show'), 10);
|
||
UI.fab.style.display = 'none';
|
||
if (currentTab === 'filter') renderFilterTab(); else renderTaskList();
|
||
};
|
||
|
||
shadow.querySelector('#sp_close_panel').onclick = () => {
|
||
UI.panel.classList.remove('show');
|
||
setTimeout(() => {
|
||
UI.panel.style.display = 'none';
|
||
UI.fab.style.display = 'flex';
|
||
togglePicking(false);
|
||
resetPanelTitle();
|
||
UI.panel.classList.remove('picking-mode');
|
||
UI.panel.style.cursor = 'default';
|
||
currentPickingFieldId = null;
|
||
currentFilterPicking = null;
|
||
}, 300);
|
||
};
|
||
|
||
console.log("[成功] Spider-UI V44 启动完毕 (局部热更 + 完美筛选树状版)。");
|
||
})(); |