1100 lines
43 KiB
JavaScript
1100 lines
43 KiB
JavaScript
/**
|
|
* CHANGAN CS55 PLUS 4TH GEN OWNER'S MANUAL - MULTILINGUAL SPA CONTROLLER
|
|
* Fluent Multilingual & RTL Support: English (en), Persian (fa / فارسی), and Chinese (zh / 中文)
|
|
*/
|
|
|
|
(function () {
|
|
'use strict';
|
|
|
|
// I18N Localization Dictionary
|
|
const I18N = {
|
|
en: {
|
|
searchPlaceholder: "Search manual (e.g., adaptive cruise, start engine, fuses, TPMS)...",
|
|
searchModalPlaceholder: "Search topics, keywords, or switches...",
|
|
searchEmpty: "Type keywords to search across all manual topics...",
|
|
searchNoResults: (q) => `No results found for "${q}".`,
|
|
tabManual: "📖 Manual",
|
|
tabSwitches: "🎛️ Switches",
|
|
tabVisual: "🖼️ Visual",
|
|
breadcrumbManual: "Manual",
|
|
prevTopic: "← Previous Topic",
|
|
nextTopic: "Next Topic →",
|
|
topicNotFound: "Topic Not Found",
|
|
topicNotFoundDesc: "The requested topic could not be loaded.",
|
|
returnHome: "Return to Home",
|
|
visualTitle: "Visual Diagram Index",
|
|
visualSubtitle: "Interactive Exterior & Interior Overview Diagrams",
|
|
switchesTitle: "Vehicle Switches & Buttons",
|
|
switchesSubtitle: "Interactive guide to key fob, stalks, shifter, center console & AC",
|
|
clusterTitle: "Instrument Cluster",
|
|
clusterSubtitle: "Digital speedometer, tachometer, menus and drive modes",
|
|
lampsTitle: "Warning & Indicator Lamps",
|
|
lampsSubtitle: "Meanings of all 34 dashboard warning lamps and indicators",
|
|
drivingTitle: "Driving the Vehicle",
|
|
drivingSubtitle: "Keyless engine start, electronic shifter and EPB auto hold",
|
|
adasTitle: "Assisted Driving (ADAS)",
|
|
adasSubtitle: "ACC, lane centering, 360 camera & APA auto parking",
|
|
emergencyTitle: "Emergency Situations",
|
|
emergencySubtitle: "Towing, battery jump start, fuse box and roadside emergency",
|
|
backHomeLabel: "Car Models",
|
|
searchScope: "Searching inside CS55 PLUS Manual"
|
|
},
|
|
fa: {
|
|
searchPlaceholder: "جستجو در راهنمای CS55 پلاس (مانند: کروز کنترل، استارت، فیوزها، باد تایر)...",
|
|
searchModalPlaceholder: "جستجوی مباحث یا کلیدهای CS55 پلاس...",
|
|
searchEmpty: "کلمه مورد نظر را برای جستجو در راهنمای CS55 پلاس وارد نمایید...",
|
|
searchNoResults: (q) => `هیچ نتیجهای در راهنمای CS55 پلاس برای «${q}» یافت نشد.`,
|
|
tabManual: "📖 راهنما",
|
|
tabSwitches: "🎛️ کلیدها",
|
|
tabVisual: "🖼️ تصاویر",
|
|
breadcrumbManual: "راهنما",
|
|
prevTopic: "مبحث قبلی →",
|
|
nextTopic: "← مبحث بعدی",
|
|
topicNotFound: "مبحث مورد نظر یافت نشد",
|
|
topicNotFoundDesc: "محتوای بخش درخواستی بارگذاری نشد.",
|
|
returnHome: "بازگشت به صفحه اصلی",
|
|
visualTitle: "راهنمای تصویری قطعات خودرو",
|
|
visualSubtitle: "نقشه و نمودارهای تصویری تعاملی از تجهیزات بیرونی و داخلی خودرو",
|
|
switchesTitle: "کلیدها و ادوات کنترلی خودرو",
|
|
switchesSubtitle: "راهنمای تعاملی ریموت، اهرمها، شیفتر دنده، کنسول وسط و تهویه",
|
|
clusterTitle: "صفحه کیلومتر و آمپر",
|
|
clusterSubtitle: "کیلومترشمار دیجیتال، دور موتور، منوها و حالتهای رانندگی",
|
|
lampsTitle: "چراغهای هشدار و راهنما",
|
|
lampsSubtitle: "تفسیر کامل ۳۴ چراغ اخطار و نشانگر صفحه آمپر",
|
|
drivingTitle: "رانندگی با خودرو",
|
|
drivingSubtitle: "استارت بدون کلید، شیفتر دنده الکترونیکی و توقف خودکار (Auto Hold)",
|
|
adasTitle: "سامانههای کمکی راننده (ADAS)",
|
|
adasSubtitle: "کروز کنترل تطبیقی، رادار بین خطوط، دوربین ۳۶۰ و پارک خودکار",
|
|
emergencyTitle: "شرایط اضطراری و امداد",
|
|
emergencySubtitle: "بکسل، باتریبهباتری، جعبه فیوزها و ابزار اضطراری",
|
|
backHomeLabel: "مدلهای خودرو",
|
|
searchScope: "جستجو در راهنمای اختصاصی CS55 پلاس"
|
|
},
|
|
zh: {
|
|
searchPlaceholder: "搜索 CS55 PLUS 手册(如:自适应巡航、启动发动机、保险丝、胎压)...",
|
|
searchModalPlaceholder: "搜索 CS55 PLUS 章节、关键词或开关按键...",
|
|
searchEmpty: "输入关键词搜索 CS55 PLUS 手册内容...",
|
|
searchNoResults: (q) => `未在 CS55 PLUS 手册中找到包含 "${q}" 的结果。`,
|
|
tabManual: "📖 指南",
|
|
tabSwitches: "🎛️ 按键",
|
|
tabVisual: "🖼️ 图解",
|
|
breadcrumbManual: "驾驶指南",
|
|
prevTopic: "← 上一节",
|
|
nextTopic: "下一节 →",
|
|
topicNotFound: "内容未找到",
|
|
topicNotFoundDesc: "请求的章节内容无法加载。",
|
|
returnHome: "返回首页",
|
|
visualTitle: "图片索引",
|
|
visualSubtitle: "整车外饰与内饰示意图",
|
|
switchesTitle: "全车开关按键",
|
|
switchesSubtitle: "钥匙、控制手柄、怀挡、中控与空调按键",
|
|
clusterTitle: "仪表界面",
|
|
clusterSubtitle: "全液晶仪表界面与行车电脑设置",
|
|
lampsTitle: "警示灯和指示灯",
|
|
lampsSubtitle: "34种仪表指示灯和故障警示灯图解说明",
|
|
drivingTitle: "驾驶车辆",
|
|
drivingSubtitle: "自动挡启动、电子怀挡与自动驻车使用",
|
|
adasTitle: "辅助驾驶",
|
|
adasSubtitle: "自适应巡航、360全景影像与泊车辅助",
|
|
emergencyTitle: "紧急情况",
|
|
emergencySubtitle: "搭电启动、应急拖车与随车工具",
|
|
backHomeLabel: "所有车型",
|
|
searchScope: "正在检索 CS55 PLUS 专属车主手册"
|
|
}
|
|
};
|
|
|
|
// Global State
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
const urlLang = urlParams.get('lang');
|
|
const storedLang = localStorage.getItem('cs55_manual_lang') || 'en';
|
|
let initialLang = (urlLang && ['en', 'fa', 'zh'].includes(urlLang)) ? urlLang : storedLang;
|
|
if (!['en', 'fa', 'zh'].includes(initialLang)) initialLang = 'en';
|
|
|
|
if (urlLang && ['en', 'fa', 'zh'].includes(urlLang)) {
|
|
localStorage.setItem('changan_user_lang_selected', 'true');
|
|
localStorage.setItem('cs55_manual_lang', urlLang);
|
|
}
|
|
|
|
const carParam = (urlParams.get('car') || 'cs55plus').toLowerCase();
|
|
const isX5 = (carParam === 'x5plus' || carParam === 'x5plus_2026');
|
|
const currentCarKey = isX5 ? 'x5plus' : 'cs55plus';
|
|
|
|
const state = {
|
|
currentCar: currentCarKey,
|
|
currentCarName: isX5 ? 'Changan X5 PLUS 2026' : 'Changan CS55 PLUS',
|
|
currentLang: initialLang, // 'en' | 'fa' | 'zh'
|
|
currentView: 'welcome', // 'welcome' | 'topic' | 'switches' | 'visual'
|
|
currentTopicId: null,
|
|
currentPlateId: null,
|
|
currentSwitchId: isX5 ? 'PG770285' : 'PG725359',
|
|
plates: {},
|
|
welcome: {},
|
|
contents: {},
|
|
searchIndex: [],
|
|
allTopicsList: [],
|
|
dataCache: { en: null, fa: null, zh: null }
|
|
};
|
|
|
|
// DOM Elements
|
|
const dom = {
|
|
sidebarScroll: document.getElementById('sidebarScroll'),
|
|
mainContent: document.getElementById('mainContent'),
|
|
langToggleBtn: document.getElementById('langToggleBtn'),
|
|
currentLangIcon: document.getElementById('currentLangIcon'),
|
|
currentLangLabel: document.getElementById('currentLangLabel'),
|
|
langMenu: document.getElementById('langMenu'),
|
|
langDropdownWrapper: document.querySelector('.lang-dropdown-wrapper'),
|
|
menuToggleBtn: document.getElementById('menuToggleBtn'),
|
|
sidebar: document.getElementById('appSidebar'),
|
|
headerSearchInput: document.getElementById('headerSearchInput'),
|
|
searchModal: document.getElementById('searchModal'),
|
|
searchModalInput: document.getElementById('searchModalInput'),
|
|
searchModalClose: document.getElementById('searchModalClose'),
|
|
searchModalResults: document.getElementById('searchModalResults'),
|
|
lightboxModal: document.getElementById('lightboxModal'),
|
|
lightboxImg: document.getElementById('lightboxImg'),
|
|
lightboxClose: document.getElementById('lightboxClose'),
|
|
navTabManual: document.getElementById('navTabManual'),
|
|
navTabSwitches: document.getElementById('navTabSwitches'),
|
|
navTabVisual: document.getElementById('navTabVisual'),
|
|
firstVisitModal: document.getElementById('firstVisitModal'),
|
|
btnBackHome: document.getElementById('btnBackHome'),
|
|
backHomeLabel: document.getElementById('backHomeLabel'),
|
|
carModelBadge: document.getElementById('carModelBadge'),
|
|
carModelSub: document.getElementById('carModelSub')
|
|
};
|
|
|
|
// Initialize App
|
|
async function init() {
|
|
setupEventListeners();
|
|
setupFirstVisitModal();
|
|
applyLanguageDirection(state.currentLang);
|
|
updateLanguageButtonUI(state.currentLang);
|
|
updateLocalizedUI(state.currentLang);
|
|
await loadDataForLanguage(state.currentLang);
|
|
handleRoute();
|
|
window.addEventListener('hashchange', handleRoute);
|
|
}
|
|
|
|
// Setup First-Time Visitor Language Modal
|
|
function setupFirstVisitModal() {
|
|
const hasChosenLang = localStorage.getItem('changan_user_lang_selected');
|
|
if (!hasChosenLang && dom.firstVisitModal) {
|
|
setTimeout(() => {
|
|
dom.firstVisitModal.classList.add('open');
|
|
}, 350);
|
|
}
|
|
|
|
document.querySelectorAll('.lang-choice-btn').forEach(btn => {
|
|
btn.addEventListener('click', () => {
|
|
const selected = btn.getAttribute('data-lang');
|
|
if (['en', 'fa', 'zh'].includes(selected)) {
|
|
localStorage.setItem('changan_user_lang_selected', 'true');
|
|
setLanguage(selected);
|
|
if (dom.firstVisitModal) dom.firstVisitModal.classList.remove('open');
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
// Set Language and re-render
|
|
async function setLanguage(lang) {
|
|
if (!['en', 'fa', 'zh'].includes(lang)) return;
|
|
state.currentLang = lang;
|
|
localStorage.setItem('cs55_manual_lang', lang);
|
|
|
|
applyLanguageDirection(lang);
|
|
updateLanguageButtonUI(lang);
|
|
updateLocalizedUI(lang);
|
|
await loadDataForLanguage(lang);
|
|
|
|
buildFlatTopicsList();
|
|
renderSidebarNavigation();
|
|
handleRoute();
|
|
}
|
|
|
|
// RTL / LTR Direction
|
|
function applyLanguageDirection(lang) {
|
|
const isRtl = (lang === 'fa');
|
|
document.documentElement.setAttribute('dir', isRtl ? 'rtl' : 'ltr');
|
|
document.documentElement.setAttribute('lang', lang);
|
|
}
|
|
|
|
// Update Header Button UI
|
|
function updateLanguageButtonUI(lang) {
|
|
const icons = { en: '🇬🇧', fa: '🇮🇷', zh: '🇨🇳' };
|
|
const labels = { en: 'English', fa: 'فارسی', zh: '中文' };
|
|
|
|
if (dom.currentLangIcon) dom.currentLangIcon.textContent = icons[lang] || '🌐';
|
|
if (dom.currentLangLabel) dom.currentLangLabel.textContent = labels[lang] || lang;
|
|
|
|
// Update active highlight in dropdown
|
|
document.querySelectorAll('.lang-option').forEach(btn => {
|
|
btn.classList.toggle('active', btn.getAttribute('data-lang') === lang);
|
|
});
|
|
}
|
|
|
|
// Update Static Localized UI Strings
|
|
function updateLocalizedUI(lang = state.currentLang) {
|
|
const t = I18N[lang] || I18N.en;
|
|
const isX5 = (state.currentCar === 'x5plus');
|
|
|
|
// Scoped car badges and subheadings
|
|
if (dom.carModelBadge) {
|
|
dom.carModelBadge.textContent = isX5 ? 'X5 PLUS' : 'CS55 PLUS';
|
|
}
|
|
if (dom.carModelSub) {
|
|
if (isX5) {
|
|
dom.carModelSub.textContent = (lang === 'fa') ? 'راهنمای کاربری نسل دوم ۲۰۲۶' : (lang === 'zh' ? '第二代官方车主指南' : "2nd Gen Sport Driver's Guide");
|
|
} else {
|
|
dom.carModelSub.textContent = (lang === 'fa') ? 'راهنمای کاربری نسل چهارم' : (lang === 'zh' ? '第四代官方车主指南' : "4th Gen Owner's Guide");
|
|
}
|
|
}
|
|
|
|
// Scoped Search Placeholder per car
|
|
if (isX5) {
|
|
if (lang === 'fa') {
|
|
if (dom.headerSearchInput) dom.headerSearchInput.placeholder = 'جستجو در راهنمای اختصاصی X5 پلاس (مانند: کروز کنترل، استارت، فیوزها)...';
|
|
if (dom.searchModalInput) dom.searchModalInput.placeholder = 'جستجوی مباحث یا کلیدهای چانگان X5 پلاس...';
|
|
} else if (lang === 'zh') {
|
|
if (dom.headerSearchInput) dom.headerSearchInput.placeholder = '搜索 X5 PLUS 手册(如:自适应巡航、启动发动机、保险丝、胎压)...';
|
|
if (dom.searchModalInput) dom.searchModalInput.placeholder = '搜索 X5 PLUS 章节、关键词或开关按键...';
|
|
} else {
|
|
if (dom.headerSearchInput) dom.headerSearchInput.placeholder = 'Search inside X5 PLUS Manual (e.g., adaptive cruise, start engine, fuses)...';
|
|
if (dom.searchModalInput) dom.searchModalInput.placeholder = 'Search topics or switches in Changan X5 PLUS...';
|
|
}
|
|
} else {
|
|
if (dom.headerSearchInput) dom.headerSearchInput.placeholder = t.searchPlaceholder;
|
|
if (dom.searchModalInput) dom.searchModalInput.placeholder = t.searchModalPlaceholder;
|
|
}
|
|
|
|
if (dom.navTabManual) dom.navTabManual.textContent = t.tabManual;
|
|
if (dom.navTabSwitches) dom.navTabSwitches.textContent = t.tabSwitches;
|
|
if (dom.navTabVisual) dom.navTabVisual.textContent = t.tabVisual;
|
|
if (dom.backHomeLabel) dom.backHomeLabel.textContent = t.backHomeLabel;
|
|
}
|
|
|
|
// Load Data for Specific Language
|
|
async function loadDataForLanguage(lang) {
|
|
if (state.dataCache[lang]) {
|
|
const cached = state.dataCache[lang];
|
|
state.plates = cached.plates;
|
|
state.welcome = cached.welcome;
|
|
state.contents = cached.contents;
|
|
state.searchIndex = cached.searchIndex;
|
|
buildFlatTopicsList();
|
|
renderSidebarNavigation();
|
|
return;
|
|
}
|
|
|
|
try {
|
|
let platesRes, welcomeRes, quickentryRes, manualRes, searchIndexRes;
|
|
const isX5 = (state.currentCar === 'x5plus');
|
|
const basePath = isX5 ? 'data/x5plus/' : 'data/';
|
|
|
|
if (lang === 'fa') {
|
|
// Persian Data
|
|
[platesRes, welcomeRes, quickentryRes] = await Promise.all([
|
|
fetch(`${basePath}plates_fa.json`).then(r => r.json()).catch(() => fetch(`${basePath}plates_en.json`).then(r => r.json()).catch(() => ({}))),
|
|
fetch(`${basePath}welcome_fa.json`).then(r => r.json()).catch(() => fetch(`${basePath}welcome_en.json`).then(r => r.json()).catch(() => ({}))),
|
|
fetch(`${basePath}quickentry_fa.json`).then(r => r.json()).catch(() => fetch(`${basePath}quickentry_en.json`).then(r => r.json()).catch(() => ({})))
|
|
]);
|
|
|
|
try {
|
|
manualRes = await fetch(`${basePath}manual_fa.json`).then(r => r.json());
|
|
} catch (e) {
|
|
manualRes = await fetch(`${basePath}manual_en.json`).then(r => r.json());
|
|
}
|
|
|
|
try {
|
|
searchIndexRes = await fetch(`${basePath}search_index_fa.json`).then(r => r.json());
|
|
} catch (e) {
|
|
searchIndexRes = await fetch(`${basePath}search_index.json`).then(r => r.json());
|
|
}
|
|
|
|
} else if (lang === 'zh') {
|
|
// Chinese Data
|
|
manualRes = await fetch(`${basePath}manual_zh.json`).then(r => r.json());
|
|
platesRes = manualRes.allPlate || {};
|
|
welcomeRes = manualRes.welcome || {};
|
|
quickentryRes = {};
|
|
searchIndexRes = await fetch(`${basePath}search_index.json`).then(r => r.json()).catch(() => []);
|
|
|
|
} else {
|
|
// English Data
|
|
[platesRes, welcomeRes, quickentryRes, manualRes, searchIndexRes] = await Promise.all([
|
|
fetch(`${basePath}plates_en.json`).then(r => r.json()).catch(() => ({})),
|
|
fetch(`${basePath}welcome_en.json`).then(r => r.json()).catch(() => ({})),
|
|
fetch(`${basePath}quickentry_en.json`).then(r => r.json()).catch(() => ({})),
|
|
fetch(`${basePath}manual_en.json`).then(r => r.json()),
|
|
fetch(`${basePath}search_index.json`).then(r => r.json()).catch(() => [])
|
|
]);
|
|
}
|
|
|
|
state.plates = platesRes || {};
|
|
state.welcome = welcomeRes || {};
|
|
state.contents = {};
|
|
|
|
if (quickentryRes) Object.assign(state.contents, quickentryRes);
|
|
if (manualRes && manualRes.contents) Object.assign(state.contents, manualRes.contents);
|
|
state.searchIndex = searchIndexRes || [];
|
|
|
|
// Cache data
|
|
state.dataCache[lang] = {
|
|
plates: state.plates,
|
|
welcome: state.welcome,
|
|
contents: state.contents,
|
|
searchIndex: state.searchIndex
|
|
};
|
|
|
|
buildFlatTopicsList();
|
|
renderSidebarNavigation();
|
|
} catch (err) {
|
|
console.error('Error loading manual data for language ' + lang + ':', err);
|
|
}
|
|
}
|
|
|
|
// Build Ordered List of Topics for Next / Prev Navigation
|
|
function buildFlatTopicsList() {
|
|
state.allTopicsList = [];
|
|
for (const plate of Object.values(state.plates)) {
|
|
if (plate.children) {
|
|
for (const cat of plate.children) {
|
|
for (const item of cat.children || []) {
|
|
if (item.id && !state.allTopicsList.some(t => t.id === item.id)) {
|
|
state.allTopicsList.push({
|
|
id: item.id,
|
|
title: item.title,
|
|
title_zh: item.title_zh,
|
|
plateKey: plate.key,
|
|
plateTitle: plate.title,
|
|
category: cat.title
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Event Listeners
|
|
function setupEventListeners() {
|
|
// Mobile Drawer Toggle
|
|
if (dom.menuToggleBtn) {
|
|
dom.menuToggleBtn.addEventListener('click', () => {
|
|
dom.sidebar.classList.toggle('open');
|
|
});
|
|
}
|
|
|
|
// Language Dropdown Toggle
|
|
if (dom.langToggleBtn && dom.langMenu) {
|
|
dom.langToggleBtn.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
const isOpen = dom.langMenu.classList.toggle('open');
|
|
if (dom.langDropdownWrapper) dom.langDropdownWrapper.classList.toggle('open', isOpen);
|
|
dom.langToggleBtn.setAttribute('aria-expanded', isOpen);
|
|
});
|
|
|
|
// Language Option Selection
|
|
document.querySelectorAll('.lang-option').forEach(btn => {
|
|
btn.addEventListener('click', (e) => {
|
|
e.stopPropagation();
|
|
const selectedLang = btn.getAttribute('data-lang');
|
|
if (selectedLang) {
|
|
setLanguage(selectedLang);
|
|
dom.langMenu.classList.remove('open');
|
|
if (dom.langDropdownWrapper) dom.langDropdownWrapper.classList.remove('open');
|
|
dom.langToggleBtn.setAttribute('aria-expanded', 'false');
|
|
}
|
|
});
|
|
});
|
|
|
|
// Close dropdown on click outside
|
|
document.addEventListener('click', () => {
|
|
if (dom.langMenu) dom.langMenu.classList.remove('open');
|
|
if (dom.langDropdownWrapper) dom.langDropdownWrapper.classList.remove('open');
|
|
if (dom.langToggleBtn) dom.langToggleBtn.setAttribute('aria-expanded', 'false');
|
|
});
|
|
}
|
|
|
|
// Nav Tabs
|
|
if (dom.navTabManual) {
|
|
dom.navTabManual.addEventListener('click', () => {
|
|
setActiveNavTab('manual');
|
|
location.hash = '#welcome';
|
|
});
|
|
}
|
|
if (dom.navTabSwitches) {
|
|
dom.navTabSwitches.addEventListener('click', () => {
|
|
setActiveNavTab('switches');
|
|
const defaultSwitch = (state.currentCar === 'x5plus') ? 'PG770285' : 'PG725359';
|
|
location.hash = `#switches-${defaultSwitch}`;
|
|
});
|
|
}
|
|
if (dom.navTabVisual) {
|
|
dom.navTabVisual.addEventListener('click', () => {
|
|
setActiveNavTab('visual');
|
|
location.hash = '#visual';
|
|
});
|
|
}
|
|
|
|
// Header Search
|
|
if (dom.headerSearchInput) {
|
|
dom.headerSearchInput.addEventListener('focus', () => {
|
|
openSearchModal(dom.headerSearchInput.value);
|
|
});
|
|
}
|
|
|
|
// Search Modal
|
|
if (dom.searchModalClose) {
|
|
dom.searchModalClose.addEventListener('click', closeSearchModal);
|
|
}
|
|
if (dom.searchModal) {
|
|
dom.searchModal.addEventListener('click', (e) => {
|
|
if (e.target === dom.searchModal) closeSearchModal();
|
|
});
|
|
}
|
|
if (dom.searchModalInput) {
|
|
dom.searchModalInput.addEventListener('input', (e) => {
|
|
executeSearch(e.target.value);
|
|
});
|
|
}
|
|
|
|
// Lightbox Modal
|
|
if (dom.lightboxClose) {
|
|
dom.lightboxClose.addEventListener('click', closeLightbox);
|
|
}
|
|
if (dom.lightboxModal) {
|
|
dom.lightboxModal.addEventListener('click', (e) => {
|
|
if (e.target === dom.lightboxModal) closeLightbox();
|
|
});
|
|
}
|
|
|
|
// Keyboard ESC
|
|
document.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Escape') {
|
|
closeSearchModal();
|
|
closeLightbox();
|
|
}
|
|
});
|
|
|
|
// Content delegate for image zoom & internal links
|
|
dom.mainContent.addEventListener('click', (e) => {
|
|
// Lightbox on image
|
|
if (e.target.tagName === 'IMG' && !e.target.classList.contains('no-zoom')) {
|
|
openLightbox(e.target.src);
|
|
}
|
|
});
|
|
}
|
|
|
|
function setActiveNavTab(tab) {
|
|
[dom.navTabManual, dom.navTabSwitches, dom.navTabVisual].forEach(el => {
|
|
if (el) el.classList.remove('active');
|
|
});
|
|
if (tab === 'manual' && dom.navTabManual) dom.navTabManual.classList.add('active');
|
|
if (tab === 'switches' && dom.navTabSwitches) dom.navTabSwitches.classList.add('active');
|
|
if (tab === 'visual' && dom.navTabVisual) dom.navTabVisual.classList.add('active');
|
|
}
|
|
|
|
// Routing Handler
|
|
function handleRoute() {
|
|
const hash = location.hash.replace('#', '') || 'welcome';
|
|
|
|
// Close mobile drawer on route change
|
|
if (dom.sidebar) dom.sidebar.classList.remove('open');
|
|
|
|
if (hash === 'welcome' || hash === '' || hash === 'home') {
|
|
state.currentView = 'welcome';
|
|
state.currentTopicId = null;
|
|
setActiveNavTab('manual');
|
|
renderWelcomeView();
|
|
} else if (hash.startsWith('topic-')) {
|
|
const topicId = hash.replace('topic-', '');
|
|
state.currentView = 'topic';
|
|
state.currentTopicId = topicId;
|
|
setActiveNavTab('manual');
|
|
loadAndRenderTopic(topicId);
|
|
} else if (hash.startsWith('switches-')) {
|
|
const defaultSwitch = (state.currentCar === 'x5plus') ? 'PG770285' : 'PG725359';
|
|
const switchId = hash.replace('switches-', '') || defaultSwitch;
|
|
state.currentView = 'switches';
|
|
state.currentSwitchId = switchId;
|
|
setActiveNavTab('switches');
|
|
renderSwitchesView(switchId);
|
|
} else if (hash === 'visual') {
|
|
state.currentView = 'visual';
|
|
setActiveNavTab('visual');
|
|
renderVisualIndexView();
|
|
} else {
|
|
// Direct topic ID fallback
|
|
state.currentView = 'topic';
|
|
state.currentTopicId = hash;
|
|
setActiveNavTab('manual');
|
|
loadAndRenderTopic(hash);
|
|
}
|
|
}
|
|
|
|
// Render Sidebar
|
|
function renderSidebarNavigation() {
|
|
if (!dom.sidebarScroll) return;
|
|
const lang = state.currentLang;
|
|
let html = '';
|
|
|
|
for (const [plateKey, plate] of Object.entries(state.plates)) {
|
|
if (plate.plateType === 'dynamicPlate' || plateKey === 'homeslides' || plate.plateType === 'replenish') {
|
|
continue;
|
|
}
|
|
|
|
const title = plate.title || plate.title_zh || '';
|
|
const desc = plate.desc || plate.desc_zh || '';
|
|
const iconSrc = plate.bg || 'assets/images/ui/da3cc931-0dcf-4556-a205-3ae8b59ceaea.png';
|
|
|
|
html += `
|
|
<div class="plate-accordion" data-plate="${plateKey}">
|
|
<div class="plate-header" onclick="window.app.toggleAccordion('${plateKey}')">
|
|
<div class="plate-header-left">
|
|
<img src="${iconSrc}" class="plate-icon no-zoom" alt="${title}" onerror="this.style.display='none'">
|
|
<div class="plate-title-group">
|
|
<div class="plate-title">${title}</div>
|
|
<div class="plate-subtitle">${desc || ''}</div>
|
|
</div>
|
|
</div>
|
|
<div class="plate-chevron">▶</div>
|
|
</div>
|
|
<div class="plate-body" id="plate-body-${plateKey}">
|
|
`;
|
|
|
|
if (plate.children) {
|
|
for (const cat of plate.children) {
|
|
const catTitle = cat.title || cat.title_zh || '';
|
|
html += `
|
|
<div class="category-group">
|
|
<div class="category-title">${catTitle}</div>
|
|
`;
|
|
for (const item of cat.children || []) {
|
|
const itemTitle = item.title || item.title_zh || '';
|
|
const activeCls = state.currentTopicId === item.id ? 'active' : '';
|
|
html += `
|
|
<a href="#topic-${item.id}" class="topic-list-item ${activeCls}" data-id="${item.id}">
|
|
${itemTitle}
|
|
</a>
|
|
`;
|
|
}
|
|
html += `</div>`;
|
|
}
|
|
}
|
|
|
|
html += `</div></div>`;
|
|
}
|
|
|
|
dom.sidebarScroll.innerHTML = html;
|
|
}
|
|
|
|
// Toggle Accordion in Sidebar
|
|
function toggleAccordion(plateKey) {
|
|
const body = document.getElementById(`plate-body-${plateKey}`);
|
|
const header = body ? body.previousElementSibling : null;
|
|
if (body && header) {
|
|
body.classList.toggle('open');
|
|
header.classList.toggle('open');
|
|
}
|
|
}
|
|
|
|
// Render Welcome Hero
|
|
function renderWelcomeView() {
|
|
const lang = state.currentLang;
|
|
const t = I18N[lang] || I18N.en;
|
|
const welcome = state.welcome || {};
|
|
const isX5 = (state.currentCar === 'x5plus');
|
|
|
|
const defaultTitle = isX5
|
|
? (lang === 'fa' ? 'راهنمای کاربری چانگان X5 پلاس ۲۰۲۶' : (lang === 'zh' ? '长安第二代 X5 PLUS 驾驶指南' : "Changan X5 PLUS 2026 Owner's Manual"))
|
|
: (lang === 'fa' ? 'راهنمای کاربری چانگان CS55 پلاس' : (lang === 'zh' ? '长安第四代 CS55 PLUS 驾驶指南' : "Changan CS55 PLUS Owner's Manual"));
|
|
|
|
const defaultSubtitle = isX5
|
|
? (lang === 'fa' ? 'راهنمای جامع تعاملی، مشخصات فنی و ادوات کنترلی خودرو' : (lang === 'zh' ? '全功能互动车主指南与车辆技术规范' : "Comprehensive Driver, Controls & Operating Guide"))
|
|
: (lang === 'fa' ? 'راهنمای جامع کاربری، مشخصات فنی و سرویسهای دورهای خودرو' : (lang === 'zh' ? '第四代全功能互动车主指南' : "Comprehensive Driver & Vehicle Operating Guide"));
|
|
|
|
const title = welcome.title || defaultTitle;
|
|
const subtitle = welcome.subtitle || defaultSubtitle;
|
|
const defaultCarImg = isX5 ? 'assets/images/ui/x5plus_car_card.jpg' : 'assets/images/ui/cs55plus_car_card.jpg';
|
|
let carImg = welcome.img || defaultCarImg;
|
|
if (carImg.startsWith('./static/content/images/')) {
|
|
carImg = 'assets/images/ui/' + carImg.split('/').pop();
|
|
}
|
|
|
|
const switchLink = isX5 ? '#switches-PG770285' : '#switches-PG725359';
|
|
const clusterLink = isX5 ? '#topic-PG770036' : '#topic-TT493534';
|
|
const warningLink = isX5 ? '#switches-PG770285' : '#switches-PG725437';
|
|
const drivingLink = isX5 ? '#topic-PG770071' : '#topic-TT482690';
|
|
const adasLink = isX5 ? '#topic-PG770128' : '#topic-TT482806';
|
|
const emergencyLink = isX5 ? '#topic-PG770238' : '#topic-TT482834';
|
|
|
|
dom.mainContent.innerHTML = `
|
|
<div class="content-wrapper">
|
|
<div class="welcome-hero">
|
|
<img src="${carImg}" alt="${isX5 ? 'Changan X5 PLUS 2026' : 'Changan CS55 PLUS'}" class="welcome-car-img no-zoom">
|
|
<h1 class="welcome-title">${title}</h1>
|
|
<p class="welcome-subtitle">${subtitle}</p>
|
|
</div>
|
|
|
|
<div class="welcome-quick-grid">
|
|
<a href="${switchLink}" class="quick-card">
|
|
<div class="quick-card-icon">🎛️</div>
|
|
<div class="quick-card-title">${t.switchesTitle}</div>
|
|
<div class="quick-card-desc">${t.switchesSubtitle}</div>
|
|
</a>
|
|
<a href="${clusterLink}" class="quick-card">
|
|
<div class="quick-card-icon">🖥️</div>
|
|
<div class="quick-card-title">${t.clusterTitle}</div>
|
|
<div class="quick-card-desc">${t.clusterSubtitle}</div>
|
|
</a>
|
|
<a href="${warningLink}" class="quick-card">
|
|
<div class="quick-card-icon">⚠️</div>
|
|
<div class="quick-card-title">${t.lampsTitle}</div>
|
|
<div class="quick-card-desc">${t.lampsSubtitle}</div>
|
|
</a>
|
|
<a href="${drivingLink}" class="quick-card">
|
|
<div class="quick-card-icon">🚗</div>
|
|
<div class="quick-card-title">${t.drivingTitle}</div>
|
|
<div class="quick-card-desc">${t.drivingSubtitle}</div>
|
|
</a>
|
|
<a href="${adasLink}" class="quick-card">
|
|
<div class="quick-card-icon">🛡️</div>
|
|
<div class="quick-card-title">${t.adasTitle}</div>
|
|
<div class="quick-card-desc">${t.adasSubtitle}</div>
|
|
</a>
|
|
<a href="${emergencyLink}" class="quick-card">
|
|
<div class="quick-card-icon">🆘</div>
|
|
<div class="quick-card-title">${t.emergencyTitle}</div>
|
|
<div class="quick-card-desc">${t.emergencySubtitle}</div>
|
|
</a>
|
|
</div>
|
|
|
|
<div class="topic-container" style="margin-top: 32px; padding: 24px; background: #f8fafc; border-radius: 8px;">
|
|
${welcome.reminder || ''}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
dom.mainContent.scrollTop = 0;
|
|
}
|
|
|
|
// Load and render a topic
|
|
async function loadAndRenderTopic(topicId) {
|
|
const lang = state.currentLang;
|
|
const t = I18N[lang] || I18N.en;
|
|
let topicData = state.contents[topicId];
|
|
|
|
if (!topicData) {
|
|
dom.mainContent.innerHTML = `
|
|
<div class="content-wrapper">
|
|
<div class="topic-page-header">
|
|
<h1 class="main-topic-title">${t.topicNotFound}</h1>
|
|
</div>
|
|
<p>${t.topicNotFoundDesc}</p>
|
|
<a href="#welcome" class="manual-link">${t.returnHome}</a>
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
// Identify surrounding topics for Next / Prev
|
|
const currentIndex = state.allTopicsList.findIndex(item => item.id === topicId);
|
|
const prevTopic = currentIndex > 0 ? state.allTopicsList[currentIndex - 1] : null;
|
|
const nextTopic = currentIndex >= 0 && currentIndex < state.allTopicsList.length - 1 ? state.allTopicsList[currentIndex + 1] : null;
|
|
|
|
const title = topicData.title || topicData.title_zh || '';
|
|
const subtitle = (lang !== 'zh' && topicData.title_zh) ? topicData.title_zh : '';
|
|
const cat = topicData.cat || topicData.cat_zh || '';
|
|
const htmlContent = (lang === 'zh') ? (topicData.html_zh || topicData.html) : (topicData.html || topicData.html_zh);
|
|
|
|
// Expand parent plate in sidebar
|
|
highlightActiveTopicInSidebar(topicId);
|
|
|
|
dom.mainContent.innerHTML = `
|
|
<div class="content-wrapper">
|
|
<div class="breadcrumbs">
|
|
<a href="#welcome">${t.breadcrumbManual}</a>
|
|
<span class="breadcrumb-sep">/</span>
|
|
<span>${cat || ''}</span>
|
|
<span class="breadcrumb-sep">/</span>
|
|
<span style="color: var(--text-main); font-weight: 600;">${title}</span>
|
|
</div>
|
|
|
|
<div class="topic-page-header">
|
|
<div class="topic-badges">
|
|
${cat ? `<span class="badge-tag">${cat}</span>` : ''}
|
|
<span class="badge-id">${topicId}</span>
|
|
</div>
|
|
<h1 class="main-topic-title">${title}</h1>
|
|
${subtitle ? `<div class="topic-subtitle-zh">${subtitle}</div>` : ''}
|
|
</div>
|
|
|
|
<div class="topic-container">
|
|
${htmlContent || ''}
|
|
</div>
|
|
|
|
<div class="topic-nav-footer">
|
|
${prevTopic ? `
|
|
<a href="#topic-${prevTopic.id}" class="nav-page-btn prev">
|
|
<span class="nav-btn-sub">${t.prevTopic}</span>
|
|
<span class="nav-btn-title">${prevTopic.title}</span>
|
|
</a>
|
|
` : `<div></div>`}
|
|
|
|
${nextTopic ? `
|
|
<a href="#topic-${nextTopic.id}" class="nav-page-btn next">
|
|
<span class="nav-btn-sub">${t.nextTopic}</span>
|
|
<span class="nav-btn-title">${nextTopic.title}</span>
|
|
</a>
|
|
` : `<div></div>`}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
dom.mainContent.scrollTop = 0;
|
|
}
|
|
|
|
// Highlight active topic in sidebar
|
|
function highlightActiveTopicInSidebar(topicId) {
|
|
document.querySelectorAll('.topic-list-item').forEach(el => {
|
|
el.classList.toggle('active', el.getAttribute('data-id') === topicId);
|
|
});
|
|
|
|
const activeLink = document.querySelector(`.topic-list-item[data-id="${topicId}"]`);
|
|
if (activeLink) {
|
|
const plateBody = activeLink.closest('.plate-body');
|
|
if (plateBody && !plateBody.classList.contains('open')) {
|
|
plateBody.classList.add('open');
|
|
const header = plateBody.previousElementSibling;
|
|
if (header) header.classList.add('open');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Render Switches View
|
|
function renderSwitchesView(switchId) {
|
|
const lang = state.currentLang;
|
|
const t = I18N[lang] || I18N.en;
|
|
|
|
// Discover all switch modules dynamically
|
|
let switchKeys = Object.keys(state.contents).filter(k => Array.isArray(state.contents[k]?.html));
|
|
if (switchKeys.length === 0) {
|
|
switchKeys = (state.currentCar === 'x5plus')
|
|
? ['PG770285', 'PG770339', 'PG770376', 'PG770362', 'PG770370', 'PG770355', 'PG770320', 'PG770332', 'PG770390']
|
|
: ['PG725359', 'PG764631', 'PG764651', 'PG725383', 'PG725399', 'PG725409', 'PG725425', 'PG725437'];
|
|
}
|
|
|
|
const activeSwitchId = (switchId && state.contents[switchId]) ? switchId : switchKeys[0];
|
|
|
|
let tabsHtml = '';
|
|
for (const key of switchKeys) {
|
|
const item = state.contents[key];
|
|
if (!item) continue;
|
|
const title = item.title || item.title_zh || '';
|
|
const activeCls = (key === activeSwitchId) ? 'active' : '';
|
|
tabsHtml += `
|
|
<button class="switch-tab-pill ${activeCls}" onclick="location.hash='#switches-${key}'">
|
|
${title}
|
|
</button>
|
|
`;
|
|
}
|
|
|
|
const currentSwitch = state.contents[activeSwitchId] || {};
|
|
const title = currentSwitch.title || currentSwitch.title_zh || '';
|
|
const cat = currentSwitch.cat || currentSwitch.cat_zh || '';
|
|
const buttons = currentSwitch.html || [];
|
|
|
|
let cardsHtml = '';
|
|
for (const btn of buttons) {
|
|
const btnTitle = btn.title || btn.title_zh || '';
|
|
const btnSub = btn.subTitle || btn.subTitle_zh || '';
|
|
const btnDetail = btn.detail || btn.detail_zh || '';
|
|
const bigImg = btn.bigImg ? `<img src="${btn.bigImg}" class="switch-card-img" alt="${btnTitle}">` : '';
|
|
const subImg = btn.subImg ? `<img src="${btn.subImg}" class="switch-sub-img no-zoom" alt="${btnTitle}">` : '';
|
|
|
|
cardsHtml += `
|
|
<div class="switch-card">
|
|
${bigImg ? `<div class="switch-card-img-wrap">${bigImg}</div>` : ''}
|
|
<div class="switch-card-body">
|
|
<div class="switch-card-header">
|
|
${subImg}
|
|
<div>
|
|
<div class="switch-card-title">${btnTitle}</div>
|
|
${btnSub ? `<div class="switch-card-sub">${btnSub}</div>` : ''}
|
|
</div>
|
|
</div>
|
|
<div class="switch-card-desc">${btnDetail}</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
dom.mainContent.innerHTML = `
|
|
<div class="content-wrapper">
|
|
<div class="breadcrumbs">
|
|
<a href="#welcome">${t.breadcrumbManual}</a>
|
|
<span class="breadcrumb-sep">/</span>
|
|
<span>${t.switchesTitle}</span>
|
|
<span class="breadcrumb-sep">/</span>
|
|
<span style="color: var(--text-main); font-weight: 600;">${title}</span>
|
|
</div>
|
|
|
|
<div class="topic-page-header">
|
|
<div class="topic-badges">
|
|
<span class="badge-tag">${cat || t.switchesTitle}</span>
|
|
<span class="badge-id">${switchId}</span>
|
|
</div>
|
|
<h1 class="main-topic-title">${title}</h1>
|
|
</div>
|
|
|
|
<div class="switches-tabs">
|
|
${tabsHtml}
|
|
</div>
|
|
|
|
<div class="switch-grid">
|
|
${cardsHtml}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
dom.mainContent.scrollTop = 0;
|
|
}
|
|
|
|
// Render Visual Index View
|
|
function renderVisualIndexView() {
|
|
const lang = state.currentLang;
|
|
const t = I18N[lang] || I18N.en;
|
|
const homeslide = state.plates['homeslides'] || {};
|
|
const imgList = homeslide.imgList || [];
|
|
|
|
let sectionsHtml = '';
|
|
for (const sec of imgList) {
|
|
const secTitle = sec.title || sec.title_zh || '';
|
|
const dots = sec.dots || [];
|
|
let dotsHtml = '';
|
|
for (const d of dots) {
|
|
const dotTitle = d.title || d.title_zh || '';
|
|
dotsHtml += `
|
|
<a href="#topic-${d.id}" class="btn-header" style="background: #2787A6; color: #fff; margin: 4px; display: inline-block;">
|
|
📍 ${dotTitle}
|
|
</a>
|
|
`;
|
|
}
|
|
|
|
sectionsHtml += `
|
|
<div style="margin-bottom: 36px; padding: 20px; border: 1px solid var(--border-color); border-radius: 12px; background: #fff;">
|
|
<h2 style="font-size: 20px; font-weight: 700; margin-bottom: 16px;">${secTitle}</h2>
|
|
<img src="${sec.img}" alt="${secTitle}" style="max-width: 100%; border-radius: 8px; margin-bottom: 16px;">
|
|
<div style="display: flex; flex-wrap: wrap; gap: 8px;">
|
|
${dotsHtml}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
dom.mainContent.innerHTML = `
|
|
<div class="content-wrapper" style="max-width: 1000px;">
|
|
<div class="breadcrumbs">
|
|
<a href="#welcome">${t.breadcrumbManual}</a>
|
|
<span class="breadcrumb-sep">/</span>
|
|
<span style="color: var(--text-main); font-weight: 600;">${t.visualTitle}</span>
|
|
</div>
|
|
|
|
<div class="topic-page-header">
|
|
<h1 class="main-topic-title">${t.visualTitle}</h1>
|
|
<div class="topic-subtitle-zh">${t.visualSubtitle}</div>
|
|
</div>
|
|
|
|
<div>
|
|
${sectionsHtml}
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
dom.mainContent.scrollTop = 0;
|
|
}
|
|
|
|
// Real-time Search Modal
|
|
function openSearchModal(initialQuery = '') {
|
|
if (!dom.searchModal) return;
|
|
dom.searchModal.classList.add('open');
|
|
if (dom.searchModalInput) {
|
|
dom.searchModalInput.value = initialQuery;
|
|
dom.searchModalInput.focus();
|
|
executeSearch(initialQuery);
|
|
}
|
|
}
|
|
|
|
function closeSearchModal() {
|
|
if (dom.searchModal) dom.searchModal.classList.remove('open');
|
|
if (dom.headerSearchInput) dom.headerSearchInput.value = '';
|
|
}
|
|
|
|
function executeSearch(query) {
|
|
if (!dom.searchModalResults) return;
|
|
const q = (query || '').trim().toLowerCase();
|
|
const lang = state.currentLang;
|
|
const t = I18N[lang] || I18N.en;
|
|
|
|
if (!q) {
|
|
dom.searchModalResults.innerHTML = `
|
|
<div style="padding: 24px; text-align: center; color: var(--text-muted); font-size: 14px;">
|
|
${t.searchEmpty}
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
const results = [];
|
|
const seenIds = new Set();
|
|
|
|
// 1. Search indexed topics (snippets, keywords, titles)
|
|
if (state.searchIndex && state.searchIndex.length > 0) {
|
|
for (const idxItem of state.searchIndex) {
|
|
const title = (idxItem.title || '').toLowerCase();
|
|
const titleZh = (idxItem.title_zh || '').toLowerCase();
|
|
const keywords = (idxItem.keywords || '').toLowerCase();
|
|
const snippet = (idxItem.snippet || '').toLowerCase();
|
|
const cat = (idxItem.cat || '').toLowerCase();
|
|
|
|
let score = 0;
|
|
if (title === q || titleZh === q) score += 100;
|
|
else if (title.startsWith(q) || titleZh.startsWith(q)) score += 60;
|
|
else if (title.includes(q) || titleZh.includes(q)) score += 40;
|
|
else if (keywords.includes(q)) score += 30;
|
|
else if (cat.includes(q)) score += 20;
|
|
else if (snippet.includes(q)) score += 15;
|
|
|
|
if (score > 0) {
|
|
seenIds.add(idxItem.id);
|
|
results.push({
|
|
id: idxItem.id,
|
|
title: idxItem.title,
|
|
title_zh: idxItem.title_zh,
|
|
category: idxItem.cat,
|
|
snippet: idxItem.snippet,
|
|
score
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. Search allTopicsList fallback
|
|
for (const item of state.allTopicsList) {
|
|
if (seenIds.has(item.id)) continue;
|
|
const title = (item.title || '').toLowerCase();
|
|
const titleZh = (item.title_zh || '').toLowerCase();
|
|
const cat = (item.category || '').toLowerCase();
|
|
|
|
let score = 0;
|
|
if (title === q || titleZh === q) score += 100;
|
|
else if (title.startsWith(q) || titleZh.startsWith(q)) score += 50;
|
|
else if (title.includes(q) || titleZh.includes(q)) score += 30;
|
|
else if (cat.includes(q)) score += 10;
|
|
|
|
if (score > 0) {
|
|
seenIds.add(item.id);
|
|
results.push({ ...item, score });
|
|
}
|
|
}
|
|
|
|
// 3. Search switch buttons
|
|
for (const [qid, item] of Object.entries(state.contents)) {
|
|
if (item.dirname === 'quickentry' && Array.isArray(item.html)) {
|
|
for (const btn of item.html) {
|
|
const bTitle = (btn.title || '').toLowerCase();
|
|
const bTitleZh = (btn.title_zh || '').toLowerCase();
|
|
const bDetail = (btn.detail || '').toLowerCase();
|
|
if (bTitle.includes(q) || bTitleZh.includes(q) || bDetail.includes(q)) {
|
|
results.push({
|
|
id: qid,
|
|
isSwitch: true,
|
|
title: btn.title,
|
|
title_zh: btn.title_zh,
|
|
category: item.title,
|
|
snippet: btn.detail,
|
|
score: 25
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
results.sort((a, b) => b.score - a.score);
|
|
|
|
if (results.length === 0) {
|
|
dom.searchModalResults.innerHTML = `
|
|
<div style="padding: 24px; text-align: center; color: var(--text-muted); font-size: 14px;">
|
|
${t.searchNoResults(query)}
|
|
</div>
|
|
`;
|
|
return;
|
|
}
|
|
|
|
let html = `
|
|
<div style="padding: 10px 16px; background: var(--bg-card-alt); border-bottom: 1px solid var(--border-color); font-size: 13px; font-weight: 600; color: var(--primary); display: flex; align-items: center; justify-content: space-between;">
|
|
<span>🚗 ${t.searchScope}</span>
|
|
<span style="font-size: 12px; color: var(--text-muted); font-weight: normal;">${results.length} results</span>
|
|
</div>
|
|
`;
|
|
for (const r of results.slice(0, 30)) {
|
|
const displayTitle = r.title || r.title_zh || '';
|
|
const subTitle = (lang !== 'zh' && r.title_zh) ? r.title_zh : '';
|
|
const href = r.isSwitch ? `#switches-${r.id}` : `#topic-${r.id}`;
|
|
|
|
html += `
|
|
<a href="${href}" class="search-result-item" onclick="window.app.closeSearchModal()">
|
|
<div class="search-result-meta">${r.category || ''}</div>
|
|
<div class="search-result-title">${highlightQuery(displayTitle, q)} ${subTitle ? `<span style="font-size: 12px; color: var(--text-muted); font-weight: normal;">(${subTitle})</span>` : ''}</div>
|
|
${r.snippet ? `<div class="search-result-snippet">${highlightQuery(r.snippet.slice(0, 160), q)}...</div>` : ''}
|
|
</a>
|
|
`;
|
|
}
|
|
|
|
dom.searchModalResults.innerHTML = html;
|
|
}
|
|
|
|
function highlightQuery(text, query) {
|
|
if (!text || !query) return text || '';
|
|
const escaped = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
const regex = new RegExp(`(${escaped})`, 'gi');
|
|
return text.replace(regex, '<mark>$1</mark>');
|
|
}
|
|
|
|
// Lightbox Modal
|
|
function openLightbox(src) {
|
|
if (!dom.lightboxModal || !dom.lightboxImg) return;
|
|
dom.lightboxImg.src = src;
|
|
dom.lightboxModal.classList.add('open');
|
|
}
|
|
|
|
function closeLightbox() {
|
|
if (!dom.lightboxModal) return;
|
|
dom.lightboxModal.classList.remove('open');
|
|
if (dom.lightboxImg) dom.lightboxImg.src = '';
|
|
}
|
|
|
|
// Expose global methods
|
|
window.app = {
|
|
init,
|
|
setLanguage,
|
|
toggleAccordion,
|
|
closeSearchModal,
|
|
openLightbox,
|
|
closeLightbox,
|
|
navigateToTopic: (tid) => {
|
|
location.hash = `#topic-${tid}`;
|
|
}
|
|
};
|
|
|
|
// Start app on DOMContentLoaded
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', init);
|
|
} else {
|
|
init();
|
|
}
|
|
|
|
})();
|