/**
* --------------------------------------------------------------------
* @description 项目前端核心文件,文件负责处理TVbox规则编辑器的所有前端逻辑,包括表单渲染、数据处理、
* 规则测试、弹窗管理以及与服务器的交互。
* @author https://t.me/CCfork
* @copyright Copyright (c) 2025, https://t.me/CCfork
* --------------------------------------------------------------------
*/
let currentInputEle;
window.globalVariables = {};
let tempDetailPageUrl = '';
let testResultsCache = [];
let isHtmlMode = false;
const MOBILE_UA = 'Mozilla/5.0 (iPhone; CPU iPhone OS 13_2_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.3 Mobile/15E148 Safari/604.1';
const PC_UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36';
const jsoupToggleMap = {
'home': '首页片单是否Jsoup写法',
'category-rules-basic': '分类片单是否Jsoup写法',
'detail': '详情是否Jsoup写法',
'play': '选集标题链接是否Jsoup写法',
'search': '搜索片单是否Jsoup写法'
};
/**
* 编译并渲染 Handlebars 模板。
* @param {string} templateId - 模板的 script 标签ID。
* @param {object} [data={}] - 渲染模板所需的数据对象。
* @returns {string} 渲染后的HTML字符串,如果模板未找到则返回空字符串。
*/
function renderTemplate(templateId, data = {}) {
const source = document.getElementById(templateId)?.innerHTML;
if (!source) {
console.error(`Template with ID '${templateId}' not found.`);
return '';
}
const template = Handlebars.compile(source);
return template(data);
}
/**
* 解析JSON格式的规则内容并填充到表单中。
* @param {string} content - 包含规则的JSON格式字符串。
*/
function parseAndRenderRules(content) {
try {
let cleanedContentLines = content.split('\n').filter(line => !line.trim().startsWith('//') && !line.trim().startsWith('/*'));
let cleanedContent = cleanedContentLines.join('\n');
cleanedContent = cleanedContent.replace(/[\u0000-\u001F\u007F-\u009F]/g, '');
cleanedContent = cleanedContent.replace(/^\s*[\r\n]/gm, '');
let rules;
try {
rules = JSON.parse(cleanedContent);
} catch (err) {
let aggressiveContent = cleanedContent.replace(/,\s*([}\]])/g, '$1');
rules = JSON.parse(aggressiveContent);
}
fillForm(rules);
showToast('规则内容加载成功!', 'success');
} catch (error) {
console.error('解析JSON文件失败:', error);
showToast('规则内容加载失败,请检查文件格式。', 'error');
}
}
/**
* 从表单中收集所有数据并组装成一个JSON对象。
* @returns {object} 包含所有表单数据的对象。
*/
function collectFormDataIntoJson() {
const form = document.getElementById('ruleForm');
const inputs = form.querySelectorAll('input, textarea');
const data = {};
// 基础字段
inputs.forEach(input => {
if (input.id && input.value) {
// 对于筛选数据,尝试解析为JSON对象
if (input.id === '筛选数据') {
try {
data[input.id] = JSON.parse(input.value);
} catch (e) {
data[input.id] = input.value; // 解析失败则存为字符串
}
} else {
data[input.id] = input.value;
}
}
});
return data;
}
/**
* 页面加载完成后的主入口函数。
*/
document.addEventListener('DOMContentLoaded', () => {
/**
* DOM加载完毕后执行的匿名函数,负责初始化页面、实例化组件和绑定事件。
*/
if (typeof formFieldsData === 'undefined') {
console.error('el.js 未能成功加载,无法渲染表单。');
alert('核心数据文件 el.js 加载失败,请检查文件路径或网络连接。');
return;
}
Handlebars.registerHelper('eq', (a, b) => a === b);
const testModal = new Modal({
id: 'testModal',
title: '测试CSS选择器',
content: renderTemplate('test-modal-template'),
footer: ''
});
const variableModal = new Modal({
id: 'variableModal',
title: '设置变量默认值',
content: renderTemplate('variable-modal-template'),
footer: ''
});
const helpModal = new Modal({
id: 'helpModal',
title: 'TVbox规则语法帮助',
content: renderTemplate('help-modal-template')
});
document.getElementById('manualTestBtn').addEventListener('click', manualRunTest);
document.getElementById('applySelectorBtn').addEventListener('click', applySelectorToField);
document.getElementById('toggleSourceBtn').addEventListener('click', () => {
const sourceTextarea = document.getElementById('sourceHtmlInput');
sourceTextarea.style.display = sourceTextarea.style.display === 'none' ? 'block' : 'none';
});
document.getElementById('saveVariablesBtn').addEventListener('click', saveVariables);
document.getElementById('toggleResultModeBtn').addEventListener('click', toggleResultMode);
// document.getElementById('advancedModeBtn').addEventListener('click', toggleAdvancedMode);
document.getElementById('autoTestBtn').addEventListener('click', startAutomatedTest);
document.getElementById('variableBtn').addEventListener('click', () => {
openVariableModal(variableModal);
});
// document.getElementById('helpBtn').addEventListener('click', () => helpModal.open());
document.getElementById('saveBtn').addEventListener('click', () => {
if (!filePathFromServer) {
showToast('文件路径未知,无法保存。', 'error');
return;
}
const jsonData = collectFormDataIntoJson();
const fileContent = JSON.stringify(jsonData, null, 2);
const formData = new FormData();
formData.append('filePath', filePathFromServer);
formData.append('fileContent', fileContent);
showToast('正在保存...', 'info');
fetch('/index.php/Edit/save', {
method: 'POST',
body: formData
})
.then(res => res.json())
.then(result => {
if (result.success) {
showToast(result.message, 'success');
} else {
throw new Error(result.message);
}
})
.catch(err => {
showToast(`保存失败: ${err.message}`, 'error');
});
});
document.getElementById('editBtn').addEventListener('click',
() => {
const urlParams = new URLSearchParams(window.location.search);
const file = urlParams.get('file');
window.open('/index.php/Edit?file=' + file + '&api=editor', '_blank')
}
);
document.addEventListener('input', (event) => {
if (event.target.closest('#ruleForm') || event.target.closest('#testModal') || event.target.closest('#variableModal')) {
if (event.target.id && event.target.id.startsWith('var-')) {
return;
}
saveFormData();
}
});
renderForm();
//loadAdvancedModeState();
loadVariables();
if (typeof fileContentFromServer !== 'undefined' && fileContentFromServer && !fileContentFromServer.startsWith('错误:')) {
parseAndRenderRules(fileContentFromServer);
} else if (typeof fileContentFromServer !== 'undefined') {
alert(fileContentFromServer);
} else {
loadFormData();
}
window.modals = {
testModal,
variableModal,
helpModal
};
});
/**
* 根据 formFieldsData 渲染整个表单结构。
*/
function renderForm() {
const fieldTemplate = Handlebars.compile(document.getElementById('form-field-template').innerHTML);
const renderTabContent = (tabId, fields) => {
const container = document.getElementById(tabId);
if (!container) return;
let html = '';
fields.forEach(field => {
html += fieldTemplate(field);
});
container.innerHTML = html;
// 渲染完成后,为需要复杂交互的按钮绑定事件
fields.forEach(field => {
const formGroup = container.querySelector(`[for="${field.id}"]`)?.parentElement;
if (!formGroup) return;
const buttonContainer = formGroup.querySelector('.input-with-buttons');
if (field.test_btn) {
const testBtn = document.createElement('button');
testBtn.type = 'button';
testBtn.className = 'btn secondary-btn btn-sm';
testBtn.innerText = '测试';
testBtn.onclick = () => openTestModal(field.id);
buttonContainer.appendChild(testBtn);
}
if (field.var_btn) {
const varBtn = document.createElement('button');
varBtn.type = 'button';
varBtn.className = 'btn secondary-btn btn-sm';
varBtn.innerText = '变量';
varBtn.onclick = () => toggleAccordion(field.id, field.var_btn);
buttonContainer.appendChild(varBtn);
}
});
};
for (const tabName in formFieldsData) {
if (tabName === 'category') {
renderTabContent('category-rules-basic', formFieldsData.category.rules);
renderTabContent('category-filter-menu', formFieldsData.category.filters);
} else {
renderTabContent(tabName, formFieldsData[tabName]);
}
}
}
/**
* 将字段渲染到指定的子标签页容器中。
* @param {string} containerId - 容器元素的ID。
* @param {Array