Files
changan-manual/assets/js/app.js
T

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();
}
})();