Files
BlackFruit-UI/plugins/addon/theme_configurator/template/admin/index.html
yiqiu 123750e904
All checks were successful
continuous-integration/drone/push Build is passing
三级菜单地区图片
2026-01-11 11:51:48 +08:00

1251 lines
50 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<link rel="stylesheet" href="/plugins/addon/theme_configurator/template/admin/theme.css" />
<div id="theme-config-app" class="admin-container">
<!-- 顶部工具栏 -->
<header class="admin-header">
<div class="admin-logo">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M12 2L2 7L12 12L22 7L12 2Z" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" />
<path d="M2 17L12 22L22 17" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" />
<path d="M2 12L12 17L22 12" stroke="currentColor" stroke-width="2" stroke-linecap="round"
stroke-linejoin="round" />
</svg>
<span>黑果云模板控制器</span>
</div>
<button class="btn btn-primary btn-lg" id="saveBtn">
<svg width="16" height="16" viewBox="0 0 16 16" fill="none">
<path d="M13.5 2.5H2.5V13.5H13.5V2.5Z" stroke="currentColor" stroke-width="1.5" />
<path d="M10.5 2.5V6.5H5.5V2.5" stroke="currentColor" stroke-width="1.5" />
<path d="M5.5 9.5H10.5V13.5H5.5V9.5Z" stroke="currentColor" stroke-width="1.5" />
</svg>
<span>保存全部配置</span>
</button>
</header>
<!-- Tab导航 -->
<nav class="admin-tabs">
<a class="tab-item active" data-tab="basic">
<svg viewBox="0 0 24 24" fill="none">
<rect x="3" y="3" width="18" height="18" rx="2" stroke="currentColor" stroke-width="2" />
<line x1="3" y1="9" x2="21" y2="9" stroke="currentColor" stroke-width="2" />
</svg>
基础配置
</a>
<a class="tab-item" data-tab="seo">
<svg viewBox="0 0 24 24" fill="none">
<circle cx="11" cy="11" r="8" stroke="currentColor" stroke-width="2" />
<path d="M21 21L16.65 16.65" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
</svg>
SEO设置
</a>
<a class="tab-item" data-tab="home">
<svg viewBox="0 0 24 24" fill="none">
<path
d="M3 9L12 2L21 9V20C21 20.5304 20.7893 21.0391 20.4142 21.4142C20.0391 21.7893 19.5304 22 19 22H5C4.46957 22 3.96086 21.7893 3.58579 21.4142C3.21071 21.0391 3 20.5304 3 20V9Z"
stroke="currentColor" stroke-width="2" />
</svg>
首页内容
</a>
<a class="tab-item" data-tab="nav">
<svg viewBox="0 0 24 24" fill="none">
<line x1="3" y1="12" x2="21" y2="12" stroke="currentColor" stroke-width="2" />
<line x1="3" y1="6" x2="21" y2="6" stroke="currentColor" stroke-width="2" />
<line x1="3" y1="18" x2="21" y2="18" stroke="currentColor" stroke-width="2" />
</svg>
导航配置
</a>
<a class="tab-item" data-tab="other">
<svg viewBox="0 0 24 24" fill="none">
<circle cx="12" cy="12" r="3" stroke="currentColor" stroke-width="2" />
<circle cx="12" cy="5" r="1" fill="currentColor" />
<circle cx="12" cy="19" r="1" fill="currentColor" />
<circle cx="5" cy="12" r="1" fill="currentColor" />
<circle cx="19" cy="12" r="1" fill="currentColor" />
</svg>
其他配置
</a>
<a class="tab-item" data-tab="json">
<svg viewBox="0 0 24 24" fill="none">
<rect x="3" y="3" width="18" height="18" rx="2" stroke="currentColor" stroke-width="2" />
<path d="M8 8L12 12L8 16" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
<path d="M12 8L16 12L12 16" stroke="currentColor" stroke-width="2" stroke-linecap="round" />
</svg>
JSON编辑器
</a>
</nav>
<!-- 主内容区 -->
<main class="admin-main">
<!-- 基础配置 -->
<section id="tab-basic" class="config-section active">
<div class="section-card">
<div class="section-header">
<h2>企业信息</h2>
<p class="section-desc">配置企业基础联系信息</p>
</div>
<div class="section-body">
<div class="form-fields">
<div class="form-item">
<label>企业名称</label>
<input type="text" class="form-control" name="site_config.enterprise_name" placeholder="主题云">
</div>
<div class="form-item">
<label>联系电话</label>
<input type="text" class="form-control" name="site_config.enterprise_telephone"
placeholder="400-000-0000">
</div>
<div class="form-item">
<label>联系邮箱</label>
<input type="text" class="form-control" name="site_config.enterprise_mailbox"
placeholder="support@example.com">
</div>
<div class="form-item">
<label>网站Logo</label>
<div class="upload-control">
<input type="text" class="form-control" name="site_config.official_website_logo"
placeholder="/upload/logo.png">
<button class="btn btn-secondary upload-btn" data-target="site_config.official_website_logo">
<svg width="14" height="14" viewBox="0 0 16 16" fill="none">
<path
d="M14 10V12.6667C14 13.0203 13.8595 13.3594 13.6095 13.6095C13.3594 13.8595 13.0203 14 12.6667 14H3.33333C2.97971 14 2.64057 13.8595 2.39052 13.6095C2.14048 13.3594 2 13.0203 2 12.6667V10"
stroke="currentColor" stroke-width="1.5" />
<path d="M11.3333 5.33333L8 2L4.66667 5.33333" stroke="currentColor" stroke-width="1.5" />
<path d="M8 2V10" stroke="currentColor" stroke-width="1.5" />
</svg>
选择文件
</button>
</div>
<div class="form-hint">建议尺寸: 200×60 像素</div>
</div>
<div class="form-item">
<label>Logo链接地址</label>
<input type="text" class="form-control" name="site_config.logo_link" placeholder="index.html">
<div class="form-hint">点击Logo跳转的链接地址默认为首页</div>
</div>
<div class="form-item">
<label>ICP备案号</label>
<input type="text" class="form-control" name="site_config.icp_info" placeholder="京ICP备XXXX号">
</div>
<div class="form-item">
<label>版权信息</label>
<input type="text" class="form-control" name="site_config.copyright_info" placeholder="© 2025 主题云">
</div>
</div>
</div>
</div>
</section>
<!-- SEO设置 -->
<section id="tab-seo" class="config-section">
<div class="section-card">
<div class="section-header">
<h2>SEO设置</h2>
</div>
<div class="section-body">
<div class="form-fields">
<div class="form-item">
<label>站点标题</label>
<input type="text" class="form-control" name="seo.title" placeholder="首页标题">
</div>
<div class="form-item">
<label>关键词</label>
<input type="text" class="form-control" name="seo.keywords" placeholder="关键词,逗号分隔">
</div>
<div class="form-item">
<label>描述</label>
<textarea class="form-control" name="seo.description" rows="3" placeholder="站点描述"></textarea>
</div>
</div>
</div>
</div>
</section>
<!-- 首页内容 -->
<section id="tab-home" class="config-section">
<div class="section-card">
<div class="section-header">
<h2>首页轮播</h2>
</div>
<div class="section-body">
<div id="bannerList"></div>
<button class="btn btn-secondary" id="addBannerBtn">+ 添加轮播</button>
</div>
</div>
<div class="section-card">
<div class="section-header">
<h2>企业荣誉</h2>
</div>
<div class="section-body">
<div id="honorList"></div>
<button class="btn btn-secondary" id="addHonorBtn">+ 添加荣誉</button>
</div>
</div>
</section>
<!-- 导航配置 -->
<section id="tab-nav" class="config-section">
<div class="section-card">
<div class="section-header">
<h2>顶部导航</h2>
<p class="section-desc">配置网站顶部导航栏菜单项支持添加子菜单。注意Logo链接已在"基础配置"中独立设置,所有导航项都会正常显示。</p>
</div>
<div class="section-body">
<div id="headerNavList"></div>
<button class="btn btn-secondary" id="addHeaderNavBtn">+ 添加导航</button>
</div>
</div>
<div class="section-card">
<div class="section-header">
<h2>底部导航</h2>
</div>
<div class="section-body">
<div id="footerNavList"></div>
<button class="btn btn-secondary" id="addFooterNavBtn">+ 添加栏目</button>
</div>
</div>
</section>
<!-- 其他配置 -->
<section id="tab-other" class="config-section">
<div class="section-card">
<div class="section-header">
<h2>友情链接</h2>
</div>
<div class="section-body">
<div id="friendlyLinkList"></div>
<button class="btn btn-secondary" id="addFriendlyLinkBtn">+ 添加</button>
</div>
</div>
<div class="section-card">
<div class="section-header">
<h2>侧边浮窗</h2>
</div>
<div class="section-body">
<div id="sideList"></div>
<button class="btn btn-secondary" id="addSideBtn">+ 添加</button>
</div>
</div>
<div class="section-card">
<div class="section-header">
<h2>反馈类型</h2>
</div>
<div class="section-body">
<div id="feedbackTypeList"></div>
<button class="btn btn-secondary" id="addFeedbackTypeBtn">+ 添加</button>
</div>
</div>
</section>
<!-- JSON编辑器 -->
<section id="tab-json" class="config-section">
<div class="section-card">
<div class="section-header">
<h2>JSON配置编辑器</h2>
<p class="section-desc">直接编辑完整JSON配置</p>
</div>
<div class="section-body">
<div class="alert alert-info" style="margin-bottom: 16px;">
<strong>使用说明:</strong> 点击"同步"按钮将表单数据转为JSON,编辑后点击"应用"更新表单,最后点击页面顶部"保存全部配置"
</div>
<textarea id="jsonEditor" class="json-editor" rows="20" placeholder='点击"同步配置"按钮加载当前数据...'></textarea>
<div style="margin-top: 16px; display: flex; gap: 12px;">
<button class="btn btn-secondary" id="syncJsonBtn">同步配置</button>
<button class="btn btn-primary" id="applyJsonBtn">应用JSON</button>
<button class="btn btn-secondary" id="copyJsonBtn">复制JSON</button>
</div>
</div>
</div>
</section>
</main>
</div>
<!-- 隐藏的文件上传input -->
<input type="file" id="fileInput" accept="image/*" style="display:none">
<script src="/plugins/addon/theme_configurator/template/admin/js/axios.min.js"></script>
<script>
(function () {
const host = location.origin;
const adminPath = location.pathname.split("/")[1];
const apiBase = `${host}/${adminPath}/v1/theme/config`;
const uploadUrl = `${host}/${adminPath}/v1/upload`;
const token = localStorage.getItem("backJwt");
let config = {};
let currentUploadTarget = null;
// Tab切换
document.querySelectorAll('.tab-item').forEach(tab => {
tab.addEventListener('click', (e) => {
e.preventDefault();
const tabName = tab.dataset.tab;
// 更新Tab active状态
document.querySelectorAll('.tab-item').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
// 更新section显示
document.querySelectorAll('.config-section').forEach(s => s.classList.remove('active'));
document.getElementById(`tab-${tabName}`).classList.add('active');
});
});
// 加载配置
function loadConfig() {
console.log('开始加载配置...');
axios.get(apiBase, {
headers: { Authorization: `Bearer ${token}` }
}).then(res => {
config = res.data.data || {};
fillForm(config);
}).catch(err => {
alert('加载配置失败: ' + (err.message || '未知错误'));
});
}
// 填充表单
function fillForm(data) {
// 填充简单字段
document.querySelectorAll('input[name], textarea[name]').forEach(input => {
const name = input.getAttribute('name');
const value = getNestedValue(data, name);
if (value !== undefined) {
input.value = value;
}
});
// 渲染轮播列表
renderBanners(data.banner || []);
// 渲染荣誉列表
renderHonors(data.honor || []);
// 渲染导航
renderHeaderNav(data.header_nav || []);
renderFooterNav(data.footer_nav || []);
// 渲染其他配置
renderFriendlyLinks(data.friendly_link || []);
renderSides(data.side || []);
renderFeedbackTypes(data.feedback_type || []);
}
// 获取嵌套属性值
function getNestedValue(obj, path) {
return path.split('.').reduce((val, key) => val?.[key], obj);
}
// 设置嵌套属性值
function setNestedValue(obj, path, value) {
const keys = path.split('.');
const last = keys.pop();
const target = keys.reduce((o, k) => o[k] = o[k] || {}, obj);
target[last] = value;
}
// 收集表单数据
function collectFormData() {
const data = JSON.parse(JSON.stringify(config)); // 深拷贝
document.querySelectorAll('input[name], textarea[name]').forEach(input => {
const name = input.getAttribute('name');
setNestedValue(data, name, input.value);
});
// 收集轮播数据
data.banner = collectBanners();
// 收集荣誉数据
data.honor = collectHonors();
data.friendly_link = collectFriendlyLinks();
data.side = collectSides();
data.feedback_type = collectFeedbackTypes();
data.header_nav = collectHeaderNav();
data.footer_nav = collectFooterNav();
return data;
}
// 渲染轮播列表
function renderBanners(banners) {
const container = document.getElementById('bannerList');
container.innerHTML = '';
banners.forEach((banner, index) => {
const item = document.createElement('div');
item.className = 'config-item';
item.innerHTML = `
<div class="config-item__header">
<h4>轮播 ${index + 1}</h4>
<button class="btn-icon btn-icon-danger" onclick="removeBanner(${index})">×</button>
</div>
<div class="config-item__body">
<div class="form-fields">
<div class="form-item">
<label>标题</label>
<input type="text" class="form-control" data-banner="${index}" data-field="title" value="${banner.title || ''}" placeholder="弹性算力">
</div>
<div class="form-item">
<label>描述</label>
<input type="text" class="form-control" data-banner="${index}" data-field="description" value="${banner.description || ''}" placeholder="宣传语">
</div>
<div class="form-item">
<label>图片地址</label>
<div class="upload-control">
<input type="text" class="form-control" data-banner="${index}" data-field="img" value="${banner.img || ''}" placeholder="/upload/banner.png">
<button class="btn btn-secondary upload-btn" data-target-banner="${index}.img">选择文件</button>
</div>
</div>
</div>
</div>
`;
container.appendChild(item);
});
}
// 收集轮播数据
function collectBanners() {
const banners = [];
document.querySelectorAll('[data-banner]').forEach(input => {
const index = parseInt(input.dataset.banner);
const field = input.dataset.field;
if (!banners[index]) {
banners[index] = {};
}
banners[index][field] = input.value;
});
return banners.filter(b => b); // 移除空项
}
// 添加轮播
window.addBanner = function () {
const banners = collectBanners();
banners.push({ title: '', description: '', img: '' });
renderBanners(banners);
};
// 删除轮播
window.removeBanner = function (index) {
const banners = collectBanners();
banners.splice(index, 1);
renderBanners(banners);
};
// 渲染荣誉列表
function renderHonors(honors) {
const container = document.getElementById('honorList');
container.innerHTML = '';
honors.forEach((honor, index) => {
const item = document.createElement('div');
item.className = 'config-item';
item.innerHTML = `
<div class="config-item__header">
<h4>荣誉 ${index + 1}</h4>
<button class="btn-icon btn-icon-danger" onclick="removeHonor(${index})">×</button>
</div>
<div class="config-item__body">
<div class="form-fields">
<div class="form-item">
<label>名称</label>
<input type="text" class="form-control" data-honor="${index}" data-field="name" value="${honor.name || ''}" placeholder="高新技术企业">
</div>
<div class="form-item">
<label>图片地址</label>
<div class="upload-control">
<input type="text" class="form-control" data-honor="${index}" data-field="img" value="${honor.img || ''}" placeholder="/upload/honor.png">
<button class="btn btn-secondary upload-btn" data-target-honor="${index}.img">选择文件</button>
</div>
</div>
</div>
</div>
`;
container.appendChild(item);
});
}
// 收集荣誉数据
function collectHonors() {
const honors = [];
document.querySelectorAll('[data-honor]').forEach(input => {
const index = parseInt(input.dataset.honor);
const field = input.dataset.field;
if (!honors[index]) {
honors[index] = {};
}
honors[index][field] = input.value;
});
return honors.filter(h => h);
}
// 添加荣誉
window.addHonor = function () {
const honors = collectHonors();
honors.push({ name: '', img: '' });
renderHonors(honors);
};
// 删除荣誉
window.removeHonor = function (index) {
const honors = collectHonors();
honors.splice(index, 1);
renderHonors(honors);
};
// ========== 友情链接 ==========
function renderFriendlyLinks(links) {
const container = document.getElementById('friendlyLinkList');
if (!container) return;
container.innerHTML = '';
links.forEach((link, index) => {
const item = document.createElement('div');
item.className = 'config-item';
item.innerHTML = `
<div class="config-item__header">
<h4>链接 ${index + 1}</h4>
<button class="btn-icon btn-icon-danger" onclick="removeFriendlyLink(${index})">×</button>
</div>
<div class="config-item__body">
<div class="form-fields">
<div class="form-item">
<label>名称</label>
<input type="text" class="form-control" data-friendly="${index}" data-field="name" value="${link.name || ''}" placeholder="网站名称">
</div>
<div class="form-item">
<label>链接地址</label>
<input type="text" class="form-control" data-friendly="${index}" data-field="url" value="${link.url || ''}" placeholder="https://example.com">
</div>
</div>
</div>
`;
container.appendChild(item);
});
}
function collectFriendlyLinks() {
const links = [];
document.querySelectorAll('[data-friendly]').forEach(input => {
const index = parseInt(input.dataset.friendly);
const field = input.dataset.field;
if (!links[index]) links[index] = {};
links[index][field] = input.value;
});
return links.filter(l => l);
}
window.addFriendlyLink = function () {
const links = collectFriendlyLinks();
links.push({ name: '', url: '' });
renderFriendlyLinks(links);
};
window.removeFriendlyLink = function (index) {
const links = collectFriendlyLinks();
links.splice(index, 1);
renderFriendlyLinks(links);
};
// ========== 侧边浮窗 ==========
function renderSides(sides) {
const container = document.getElementById('sideList');
if (!container) return;
container.innerHTML = '';
sides.forEach((side, index) => {
const item = document.createElement('div');
item.className = 'config-item';
item.innerHTML = `
<div class="config-item__header">
<h4>浮窗 ${index + 1}</h4>
<button class="btn-icon btn-icon-danger" onclick="removeSide(${index})">×</button>
</div>
<div class="config-item__body">
<div class="form-fields">
<div class="form-item">
<label>名称</label>
<input type="text" class="form-control" data-side="${index}" data-field="name" value="${side.name || ''}" placeholder="电话咨询">
</div>
<div class="form-item">
<label>图标地址</label>
<div class="upload-control">
<input type="text" class="form-control" data-side="${index}" data-field="icon" value="${side.icon || ''}" placeholder="/upload/icon.png">
<button class="btn btn-secondary upload-btn" data-target-side="${index}.icon">选择文件</button>
</div>
</div>
<div class="form-item">
<label>内容HTML</label>
<textarea class="form-control" data-side="${index}" data-field="content" rows="2" placeholder="HTML内容">${side.content || ''}</textarea>
</div>
</div>
</div>
`;
container.appendChild(item);
});
}
function collectSides() {
const sides = [];
document.querySelectorAll('[data-side]').forEach(input => {
const index = parseInt(input.dataset.side);
const field = input.dataset.field;
if (!sides[index]) sides[index] = {};
sides[index][field] = input.value;
});
return sides.filter(s => s);
}
window.addSide = function () {
const sides = collectSides();
sides.push({ name: '', icon: '', content: '' });
renderSides(sides);
};
window.removeSide = function (index) {
const sides = collectSides();
sides.splice(index, 1);
renderSides(sides);
};
// ========== 反馈类型 ==========
function renderFeedbackTypes(types) {
const container = document.getElementById('feedbackTypeList');
if (!container) return;
container.innerHTML = '';
types.forEach((type, index) => {
const item = document.createElement('div');
item.className = 'config-item';
item.innerHTML = `
<div class="config-item__header">
<h4>类型 ${index + 1}</h4>
<button class="btn-icon btn-icon-danger" onclick="removeFeedbackType(${index})">×</button>
</div>
<div class="config-item__body">
<div class="form-fields">
<div class="form-item">
<label>ID</label>
<input type="text" class="form-control" data-feedback="${index}" data-field="id" value="${type.id || ''}" placeholder="1">
</div>
<div class="form-item">
<label>名称</label>
<input type="text" class="form-control" data-feedback="${index}" data-field="name" value="${type.name || ''}" placeholder="产品建议">
</div>
<div class="form-item">
<label>描述</label>
<input type="text" class="form-control" data-feedback="${index}" data-field="description" value="${type.description || ''}" placeholder="用于产品体验反馈">
</div>
</div>
</div>
`;
container.appendChild(item);
});
}
function collectFeedbackTypes() {
const types = [];
document.querySelectorAll('[data-feedback]').forEach(input => {
const index = parseInt(input.dataset.feedback);
const field = input.dataset.field;
if (!types[index]) types[index] = {};
types[index][field] = input.value;
});
return types.filter(t => t);
}
window.addFeedbackType = function () {
const types = collectFeedbackTypes();
types.push({ id: '', name: '', description: '' });
renderFeedbackTypes(types);
};
window.removeFeedbackType = function (index) {
const types = collectFeedbackTypes();
types.splice(index, 1);
renderFeedbackTypes(types);
};
// ========== 顶部导航(完整版 - 支持子菜单) ==========
function renderHeaderNav(navs) {
const container = document.getElementById('headerNavList');
if (!container) return;
container.innerHTML = '';
navs.forEach((nav, index) => {
const hasChildren = Array.isArray(nav.children) && nav.children.length > 0;
const item = document.createElement('div');
item.className = 'config-item';
item.style.borderLeft = '3px solid #52c41a'; // 绿色标识
item.innerHTML = `
<div class="config-item__header" style="background:#f6ffed;">
<h4 style="color:#52c41a;">导航 ${index + 1}: ${nav.name || '(未命名)'}</h4>
<div style="display: flex; gap: 4px;">
<button class="btn btn-success btn-sm" id="toggle-nav-${index}" onclick="toggleHeaderNavChildren(${index})" style="font-size:12px;">${hasChildren && nav.children.length > 0 ? '收起子菜单' : '展开子菜单'}</button>
<button class="btn-icon btn-icon-danger" onclick="removeHeaderNav(${index})">×</button>
</div>
</div>
<div class="config-item__body">
<div class="form-fields">
<div class="form-item">
<label>名称</label>
<input type="text" class="form-control" data-hnav="${index}" data-field="name" value="${nav.name || ''}" placeholder="产品中心">
</div>
<div class="form-item">
<label>链接</label>
<input type="text" class="form-control" data-hnav="${index}" data-field="file_address" value="${nav.file_address || ''}" placeholder="/products.html">
</div>
</div>
<div id="header-nav-children-${index}" style="display:${hasChildren ? 'block' : 'none'}; margin-top:12px; padding-top:12px; border-top:1px solid #eee;">
<h5 style="margin:0 0 8px; font-size:13px;">子菜单</h5>
<div id="header-nav-children-list-${index}"></div>
<button class="btn btn-secondary btn-sm" onclick="addHeaderNavChild(${index})" style="margin-top:8px;">+ 添加子菜单</button>
</div>
</div>
`;
container.appendChild(item);
if (hasChildren) {
renderHeaderNavChildren(index, nav.children);
}
});
}
function renderHeaderNavChildren(navIndex, children) {
const container = document.getElementById(`header-nav-children-list-${navIndex}`);
if (!container) return;
container.innerHTML = '';
children.forEach((child, childIndex) => {
const hasGrandChildren = Array.isArray(child.children) && child.children.length > 0;
const item = document.createElement('div');
item.style.cssText = 'padding:12px; margin-bottom:12px; background:#f9f9f9; border-radius:4px; border-left:3px solid #1890ff;';
item.innerHTML = `
<div style="margin-bottom:8px; display:flex; justify-content:space-between; align-items:center;">
<strong style="color:#1890ff;">二级菜单 ${childIndex + 1}</strong>
<div style="display:flex; gap:4px;">
<button class="btn btn-info btn-sm" onclick="toggleThirdLevel(${navIndex}, ${childIndex})" style="font-size:11px;">
${hasGrandChildren ? '收起三级' : '展开三级'}
</button>
<button class="btn-icon btn-icon-danger btn-sm" onclick="removeHeaderNavChild(${navIndex}, ${childIndex})">×</button>
</div>
</div>
<div style="display:grid; gap:8px;">
<input type="text" class="form-control" data-hnav-child="${navIndex}.${childIndex}" data-field="name" value="${child.name || ''}" placeholder="二级菜单名称SAS轻量云服务器">
<input type="text" class="form-control" data-hnav-child="${navIndex}.${childIndex}" data-field="file_address" value="${child.file_address || ''}" placeholder="链接地址(可选)">
<input type="text" class="form-control" data-hnav-child="${navIndex}.${childIndex}" data-field="icon" value="${child.icon || ''}" placeholder="图标URL可选">
<input type="text" class="form-control" data-hnav-child="${navIndex}.${childIndex}" data-field="description" value="${child.description || ''}" placeholder="描述文字高性能SSD云服务器">
<label style="font-size:12px;"><input type="checkbox" data-hnav-child="${navIndex}.${childIndex}" data-field="blank" ${child.blank ? 'checked' : ''}> 新窗口打开</label>
</div>
<div id="third-level-${navIndex}-${childIndex}" style="display:${hasGrandChildren ? 'block' : 'none'}; margin-top:12px; padding:12px; background:#fff; border-radius:4px; border:1px dashed #d9d9d9;">
<h6 style="margin:0 0 8px; font-size:12px; color:#666;">三级菜单</h6>
<div id="third-level-list-${navIndex}-${childIndex}"></div>
<button class="btn btn-secondary btn-sm" onclick="addThirdLevelItem(${navIndex}, ${childIndex})" style="margin-top:8px; font-size:11px;">+ 添加三级菜单</button>
</div>
`;
container.appendChild(item);
// 渲染三级菜单
if (hasGrandChildren) {
renderThirdLevel(navIndex, childIndex, child.children);
}
});
}
// 渲染三级菜单
function renderThirdLevel(navIndex, childIndex, grandChildren) {
const container = document.getElementById(`third-level-list-${navIndex}-${childIndex}`);
if (!container) return;
container.innerHTML = '';
grandChildren.forEach((grandChild, grandIndex) => {
const item = document.createElement('div');
item.style.cssText = 'padding:8px; margin-bottom:6px; background:#fafafa; border-radius:3px; border-left:2px solid #52c41a;';
// 生成地区选项(只使用方形图标,排除-y后缀的圆形图标
const countryOptions = getCountryOptions();
const selectedCountry = grandChild.country_code || '';
const countrySelectHTML = `
<select class="form-control form-control-sm" data-hnav-grandchild="${navIndex}.${childIndex}.${grandIndex}" data-field="country_code" style="font-size:12px;">
<option value="">无地区标识</option>
${countryOptions.map(code => `<option value="${code}" ${selectedCountry === code ? 'selected' : ''}>${code}</option>`).join('')}
</select>
`;
item.innerHTML = `
<div style="display:flex; gap:6px; align-items:start;">
<div style="flex:1; display:grid; gap:6px;">
<input type="text" class="form-control form-control-sm" data-hnav-grandchild="${navIndex}.${childIndex}.${grandIndex}" data-field="name" value="${grandChild.name || ''}" placeholder="三级菜单名称香港SAS" style="font-size:12px;">
<input type="text" class="form-control form-control-sm" data-hnav-grandchild="${navIndex}.${childIndex}.${grandIndex}" data-field="file_address" value="${grandChild.file_address || ''}" placeholder="链接地址" style="font-size:12px;">
<input type="text" class="form-control form-control-sm" data-hnav-grandchild="${navIndex}.${childIndex}.${grandIndex}" data-field="description" value="${grandChild.description || ''}" placeholder="描述(可选)" style="font-size:12px;">
<div style="display:flex; gap:8px; align-items:center;">
<label style="font-size:11px; margin:0;"><input type="checkbox" data-hnav-grandchild="${navIndex}.${childIndex}.${grandIndex}" data-field="blank" ${grandChild.blank ? 'checked' : ''}> 新窗口</label>
<div style="flex:1;">${countrySelectHTML}</div>
</div>
</div>
<button class="btn-icon btn-icon-danger btn-sm" onclick="removeThirdLevelItem(${navIndex}, ${childIndex}, ${grandIndex})" style="font-size:11px;">×</button>
</div>
`;
container.appendChild(item);
});
}
// 获取国家/地区代码列表(只使用方形图标)
function getCountryOptions() {
// 常用国家/地区代码列表(方形图标)
return [
'CN', 'HK', 'TW', 'MO', // 中国及特别行政区
'US', 'JP', 'KR', 'SG', 'GB', // 常用国家
'DE', 'FR', 'CA', 'AU', 'IN', // 其他常用
'TH', 'MY', 'ID', 'PH', 'VN', // 东南亚
'AE', 'RU', 'BR', 'IT', 'ES', 'NL', 'CH', 'SE' // 其他
];
}
// 切换三级菜单显示
window.toggleThirdLevel = function (navIndex, childIndex) {
const container = document.getElementById(`third-level-${navIndex}-${childIndex}`);
const btn = event.target;
if (container) {
const isHidden = container.style.display === 'none';
container.style.display = isHidden ? 'block' : 'none';
btn.textContent = isHidden ? '收起三级' : '展开三级';
}
};
// 添加三级菜单项
window.addThirdLevelItem = function (navIndex, childIndex) {
const navs = collectHeaderNav();
if (!navs[navIndex]) return;
if (!navs[navIndex].children[childIndex]) return;
if (!navs[navIndex].children[childIndex].children) {
navs[navIndex].children[childIndex].children = [];
}
navs[navIndex].children[childIndex].children.push({
name: '',
file_address: '',
description: '',
blank: false,
country_code: '' // 新增地区代码字段
});
renderHeaderNav(navs);
// 确保展开
document.getElementById(`third-level-${navIndex}-${childIndex}`).style.display = 'block';
};
// 删除三级菜单项
window.removeThirdLevelItem = function (navIndex, childIndex, grandIndex) {
const navs = collectHeaderNav();
if (navs[navIndex] && navs[navIndex].children[childIndex] && navs[navIndex].children[childIndex].children) {
navs[navIndex].children[childIndex].children.splice(grandIndex, 1);
renderHeaderNav(navs);
}
};
function collectHeaderNav() {
const navs = [];
// 收集主导航
document.querySelectorAll('[data-hnav]').forEach(input => {
const index = parseInt(input.dataset.hnav);
const field = input.dataset.field;
if (!navs[index]) navs[index] = { children: [] };
navs[index][field] = input.value;
});
// 收集子菜单
document.querySelectorAll('[data-hnav-child]').forEach(input => {
const [navIndex, childIndex] = input.dataset.hnavChild.split('.').map(Number);
const field = input.dataset.field;
if (!navs[navIndex]) navs[navIndex] = { children: [] };
if (!navs[navIndex].children[childIndex]) {
navs[navIndex].children[childIndex] = { children: [] };
}
if (input.type === 'checkbox') {
navs[navIndex].children[childIndex][field] = input.checked;
} else {
navs[navIndex].children[childIndex][field] = input.value;
}
});
// 收集三级菜单
document.querySelectorAll('[data-hnav-grandchild]').forEach(input => {
const [navIndex, childIndex, grandIndex] = input.dataset.hnavGrandchild.split('.').map(Number);
const field = input.dataset.field;
if (!navs[navIndex]) navs[navIndex] = { children: [] };
if (!navs[navIndex].children[childIndex]) {
navs[navIndex].children[childIndex] = { children: [] };
}
if (!navs[navIndex].children[childIndex].children) {
navs[navIndex].children[childIndex].children = [];
}
if (!navs[navIndex].children[childIndex].children[grandIndex]) {
navs[navIndex].children[childIndex].children[grandIndex] = {};
}
if (input.type === 'checkbox') {
navs[navIndex].children[childIndex].children[grandIndex][field] = input.checked;
} else {
navs[navIndex].children[childIndex].children[grandIndex][field] = input.value;
}
});
// 清理空的children数组
navs.forEach(nav => {
if (nav && nav.children) {
nav.children = nav.children.filter(child => child);
nav.children.forEach(child => {
if (child && child.children) {
child.children = child.children.filter(gc => gc);
// 如果三级菜单为空删除children属性
if (child.children.length === 0) {
delete child.children;
}
}
});
}
});
return navs.filter(n => n);
}
window.addHeaderNav = function () {
const navs = collectHeaderNav();
navs.push({ name: '', file_address: '', children: [] });
renderHeaderNav(navs);
};
window.removeHeaderNav = function (index) {
const navs = collectHeaderNav();
navs.splice(index, 1);
renderHeaderNav(navs);
};
window.toggleHeaderNavChildren = function (index) {
const container = document.getElementById(`header-nav-children-${index}`);
const toggleBtn = document.getElementById(`toggle-nav-${index}`);
if (container && toggleBtn) {
const isHidden = container.style.display === 'none';
container.style.display = isHidden ? 'block' : 'none';
toggleBtn.textContent = isHidden ? '收起子菜单' : '展开子菜单';
}
};
window.addHeaderNavChild = function (navIndex) {
const navs = collectHeaderNav();
if (!navs[navIndex]) navs[navIndex] = { children: [] };
if (!navs[navIndex].children) navs[navIndex].children = [];
navs[navIndex].children.push({
name: '',
file_address: '',
icon: '',
description: '',
blank: false
});
renderHeaderNav(navs);
// 确保展开
document.getElementById(`header-nav-children-${navIndex}`).style.display = 'block';
};
window.removeHeaderNavChild = function (navIndex, childIndex) {
const navs = collectHeaderNav();
if (navs[navIndex] && navs[navIndex].children) {
navs[navIndex].children.splice(childIndex, 1);
renderHeaderNav(navs);
}
};
// ========== 底部导航(完整版 - 支持子菜单) ==========
function renderFooterNav(navs) {
const container = document.getElementById('footerNavList');
if (!container) return;
container.innerHTML = '';
navs.forEach((col, index) => {
const hasChildren = Array.isArray(col.children) && col.children.length > 0;
const item = document.createElement('div');
item.className = 'config-item';
item.style.borderLeft = '3px solid #1890ff'; // 蓝色标识
item.innerHTML = `
<div class="config-item__header" style="background:#e6f7ff;">
<h4 style="color:#1890ff;">栏目 ${index + 1}: ${col.name || '(未命名)'}</h4>
<div style="display: flex; gap: 4px;">
<button class="btn btn-primary btn-sm" id="toggle-footer-${index}" onclick="toggleFooterNavChildren(${index})" style="font-size:12px;">${hasChildren && col.children.length > 0 ? '收起链接列表' : '展开链接列表'}</button>
<button class="btn-icon btn-icon-danger" onclick="removeFooterNav(${index})">×</button>
</div>
</div>
<div class="config-item__body">
<div class="form-item">
<label>栏目名称</label>
<input type="text" class="form-control" data-fnav="${index}" data-field="name" value="${col.name || ''}" placeholder="热门云产品">
</div>
<div id="footer-nav-children-${index}" style="display:${hasChildren ? 'block' : 'none'}; margin-top:12px; padding-top:12px; border-top:1px solid #d9d9d9;">
<h5 style="margin:0 0 8px; font-size:13px; color:#1890ff;">链接列表</h5>
<div id="footer-nav-children-list-${index}"></div>
<button class="btn btn-primary btn-sm" onclick="addFooterNavChild(${index})" style="margin-top:8px;">+ 添加链接</button>
</div>
</div>
`;
container.appendChild(item);
if (hasChildren) {
renderFooterNavChildren(index, col.children);
}
});
}
function renderFooterNavChildren(navIndex, children) {
const container = document.getElementById(`footer-nav-children-list-${navIndex}`);
if (!container) return;
container.innerHTML = '';
children.forEach((child, childIndex) => {
const item = document.createElement('div');
item.style.cssText = 'padding:8px; margin-bottom:8px; background:#f0f8ff; border-radius:4px; border-left:2px solid #1890ff;';
item.innerHTML = `
<div style="display:flex; justify-content:space-between; align-items:start;">
<div style="flex:1; display:grid; gap:8px;">
<input type="text" class="form-control" data-fnav-child="${navIndex}.${childIndex}" data-field="name" value="${child.name || ''}" placeholder="链接名称">
<input type="text" class="form-control" data-fnav-child="${navIndex}.${childIndex}" data-field="url" value="${child.url || ''}" placeholder="链接地址">
<label style="font-size:12px;"><input type="checkbox" data-fnav-child="${navIndex}.${childIndex}" data-field="blank" ${child.blank ? 'checked' : ''}> 新窗口打开</label>
</div>
<button class="btn-icon btn-icon-danger" onclick="removeFooterNavChild(${navIndex}, ${childIndex})" style="margin-left:8px;">×</button>
</div>
`;
container.appendChild(item);
});
}
function collectFooterNav() {
const navs = [];
// 收集栏目名称
document.querySelectorAll('[data-fnav]').forEach(input => {
const index = parseInt(input.dataset.fnav);
if (!navs[index]) navs[index] = { children: [] };
navs[index][input.dataset.field] = input.value;
});
// 收集链接列表
document.querySelectorAll('[data-fnav-child]').forEach(input => {
const [navIndex, childIndex] = input.dataset.fnavChild.split('.').map(Number);
const field = input.dataset.field;
if (!navs[navIndex]) navs[navIndex] = { children: [] };
if (!navs[navIndex].children[childIndex]) {
navs[navIndex].children[childIndex] = {};
}
if (input.type === 'checkbox') {
navs[navIndex].children[childIndex][field] = input.checked;
} else {
navs[navIndex].children[childIndex][field] = input.value;
}
});
return navs.filter(n => n);
}
window.addFooterNav = function () {
const navs = collectFooterNav();
navs.push({ name: '', children: [] });
renderFooterNav(navs);
};
window.removeFooterNav = function (index) {
const navs = collectFooterNav();
navs.splice(index, 1);
renderFooterNav(navs);
};
window.toggleFooterNavChildren = function (index) {
const container = document.getElementById(`footer-nav-children-${index}`);
const toggleBtn = document.getElementById(`toggle-footer-${index}`);
if (container && toggleBtn) {
const isHidden = container.style.display === 'none';
container.style.display = isHidden ? 'block' : 'none';
toggleBtn.textContent = isHidden ? '收起链接列表' : '展开链接列表';
}
};
window.addFooterNavChild = function (navIndex) {
const navs = collectFooterNav();
if (!navs[navIndex]) navs[navIndex] = { children: [] };
if (!navs[navIndex].children) navs[navIndex].children = [];
navs[navIndex].children.push({
name: '',
url: '',
blank: false
});
renderFooterNav(navs);
// 确保展开
document.getElementById(`footer-nav-children-${navIndex}`).style.display = 'block';
};
window.removeFooterNavChild = function (navIndex, childIndex) {
const navs = collectFooterNav();
if (navs[navIndex] && navs[navIndex].children) {
navs[navIndex].children.splice(childIndex, 1);
renderFooterNav(navs);
}
};
// 文件上传
document.addEventListener('click', (e) => {
if (e.target.closest('.upload-btn')) {
e.preventDefault();
const btn = e.target.closest('.upload-btn');
currentUploadTarget = btn.dataset.target || btn.dataset.targetBanner || btn.dataset.targetHonor || btn.dataset.targetSide;
document.getElementById('fileInput').click();
}
});
document.getElementById('fileInput').addEventListener('change', function (e) {
const file = e.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('file', file);
const saveBtn = document.getElementById('saveBtn');
saveBtn.disabled = true;
saveBtn.querySelector('span').textContent = '上传中...';
axios.post(uploadUrl, formData, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'multipart/form-data'
}
}).then(res => {
const data = res.data.data || {};
const url = data.image_url || data.url || data.save_name || '';
if (url) {
// 找到目标input并设置值
if (currentUploadTarget.includes('.')) {
// 动态列表项
const input = document.querySelector(`[data-target-banner="${currentUploadTarget}"], [data-target-honor="${currentUploadTarget}"]`)
?.closest('.upload-control')
?.querySelector('input');
if (input) input.value = url.startsWith('/') ? url : '/' + url;
} else {
// 普通字段
const input = document.querySelector(`input[name="${currentUploadTarget}"]`);
if (input) input.value = url.startsWith('/') ? url : '/' + url;
}
alert('上传成功');
}
}).catch(err => {
alert('上传失败: ' + (err.response?.data?.msg || err.message));
}).finally(() => {
saveBtn.disabled = false;
saveBtn.querySelector('span').textContent = '保存全部配置';
e.target.value = ''; // 清空文件选择
});
});
// 保存配置
document.getElementById('saveBtn').addEventListener('click', () => {
const btn = document.getElementById('saveBtn');
btn.disabled = true;
btn.querySelector('span').textContent = '保存中...';
const data = collectFormData();
console.log('保存的数据:', data);
console.log('保存的数据JSON:', JSON.stringify(data, null, 2)); // 调试日志
axios.post(apiBase, data, {
headers: { Authorization: `Bearer ${token}` }
}).then(res => {
console.log('服务器响应:', res.data);
alert(res.data.msg || '保存成功');
// 使用服务器返回的数据更新config
config = res.data.data || data;
// 清除前端缓存,确保下次访问时重新加载最新数据
sessionStorage.removeItem('commentData');
// 重新渲染界面
fillForm(config);
}).catch(err => {
alert('保存失败: ' + (err.response?.data?.msg || err.message));
}).finally(() => {
btn.disabled = false;
btn.querySelector('span').textContent = '保存全部配置';
});
});
// 添加按钮事件
document.getElementById('addBannerBtn').addEventListener('click', addBanner);
document.getElementById('addHonorBtn').addEventListener('click', addHonor);
document.getElementById('addFriendlyLinkBtn').addEventListener('click', addFriendlyLink);
document.getElementById('addSideBtn').addEventListener('click', addSide);
document.getElementById('addFeedbackTypeBtn').addEventListener('click', addFeedbackType);
document.getElementById('addHeaderNavBtn').addEventListener('click', addHeaderNav);
document.getElementById('addFooterNavBtn').addEventListener('click', addFooterNav);
// JSON编辑器功能
document.getElementById('syncJsonBtn').addEventListener('click', () => {
const data = collectFormData();
document.getElementById('jsonEditor').value = JSON.stringify(data, null, 2);
alert('✓ 已同步当前配置到JSON编辑器');
});
document.getElementById('applyJsonBtn').addEventListener('click', () => {
const jsonText = document.getElementById('jsonEditor').value.trim();
if (!jsonText) {
alert('⚠ JSON内容为空');
return;
}
try {
const data = JSON.parse(jsonText);
config = data;
fillForm(data);
alert('✓ JSON已应用到表单,请切换到其他Tab查看');
} catch (err) {
alert('✗ JSON解析失败:\n' + err.message);
}
});
document.getElementById('copyJsonBtn').addEventListener('click', () => {
const jsonText = document.getElementById('jsonEditor').value;
if (!jsonText) {
alert('⚠ JSON内容为空');
return;
}
navigator.clipboard.writeText(jsonText).then(() => {
alert('✓ JSON已复制到剪贴板');
}).catch(err => {
// 兼容方案
const textarea = document.createElement('textarea');
textarea.value = jsonText;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
alert('✓ JSON已复制到剪贴板');
});
});
// 初始化
loadConfig();
})();
</script>
</html>