Files
BlackFruit-UI/plugins/addon/theme_configurator/template/admin/index.html.stable
yiqiu d741e9c693
All checks were successful
continuous-integration/drone/push Build is passing
修复插件
2025-12-28 15:46:13 +08:00

592 lines
22 KiB
Plaintext
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>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">顶部和底部导航较复杂,建议使用JSON编辑器编辑</p>
</div>
<div class="section-body">
<div class="alert alert-info">
由于导航结构包含多级嵌套,建议切换到"JSON编辑器"Tab进行编辑
</div>
</div>
</div>
</section>
<!-- 其他配置 -->
<section id="tab-other" class="config-section">
<div class="section-card">
<div class="section-header">
<h2>其他配置</h2>
<p class="section-desc">友情链接、反馈类型等其他配置项</p>
</div>
<div class="section-body">
<div class="alert alert-info">
这些配置项建议使用JSON编辑器编辑,格式请参考完整JSON数据
</div>
</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() {
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 || []);
}
// 获取嵌套属性值
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();
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);
};
// 文件上传
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;
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); // 调试日志
axios.post(apiBase, data, {
headers: { Authorization: `Bearer ${token}` }
}).then(res => {
alert(res.data.msg || '保存成功');
// 使用服务器返回的数据更新config
config = res.data.data || data;
// 重新渲染界面
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);
// 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>