feat: switch to table layout and add image URL field

This commit is contained in:
2026-07-18 20:53:35 +08:00
parent d95f4fa4bf
commit e5214420fc
2 changed files with 263 additions and 335 deletions
+238 -310
View File
@@ -15,7 +15,7 @@ body {
} }
.container { .container {
max-width: 960px; max-width: 1100px;
margin: 0 auto; margin: 0 auto;
padding: 20px; padding: 20px;
} }
@@ -56,29 +56,13 @@ body {
font-weight: 500; font-weight: 500;
} }
.btn-primary { .btn-primary { background: #1677ff; color: #fff; }
background: #1677ff;
color: #fff;
}
.btn-primary:hover { background: #4096ff; } .btn-primary:hover { background: #4096ff; }
.btn-default { background: #fff; color: #333; border: 1px solid #d9d9d9; }
.btn-default {
background: #fff;
color: #333;
border: 1px solid #d9d9d9;
}
.btn-default:hover { border-color: #1677ff; color: #1677ff; } .btn-default:hover { border-color: #1677ff; color: #1677ff; }
.btn-danger { background: #ff4d4f; color: #fff; }
.btn-danger {
background: #ff4d4f;
color: #fff;
}
.btn-danger:hover { background: #ff7875; } .btn-danger:hover { background: #ff7875; }
.btn-sm { padding: 4px 10px; font-size: 12px; }
.btn-sm {
padding: 4px 10px;
font-size: 12px;
}
.mode-toggle { .mode-toggle {
background: #f0f0f0; background: #f0f0f0;
@@ -104,48 +88,140 @@ body {
box-shadow: 0 1px 3px rgba(0,0,0,0.1); box-shadow: 0 1px 3px rgba(0,0,0,0.1);
} }
/* Component Cards */ /* Table */
.component-list { .config-table-wrap {
display: flex; background: #fff;
flex-direction: column; border-radius: 12px;
gap: 12px; box-shadow: 0 2px 8px rgba(0,0,0,0.06);
overflow: hidden;
margin-bottom: 80px; margin-bottom: 80px;
} }
.component-card { .config-table {
background: #fff; width: 100%;
border-radius: 12px; border-collapse: collapse;
padding: 20px 24px; font-size: 14px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
transition: box-shadow 0.2s;
} }
.component-card:hover { .config-table thead {
box-shadow: 0 4px 16px rgba(0,0,0,0.1); background: #fafafa;
} }
.card-header { .config-table th {
padding: 12px 16px;
text-align: left;
font-weight: 600;
color: #666;
font-size: 13px;
border-bottom: 1px solid #f0f0f0;
white-space: nowrap;
}
.config-table td {
padding: 12px 16px;
border-bottom: 1px solid #f0f0f0;
vertical-align: middle;
}
.config-table tbody tr:last-child td {
border-bottom: none;
}
.config-table tbody tr:hover {
background: #fafafa;
}
/* Image cell */
.cell-img {
width: 64px;
height: 64px;
border-radius: 8px;
object-fit: contain;
background: #f5f5f5;
border: 1px solid #eee;
}
.cell-img-placeholder {
width: 64px;
height: 64px;
border-radius: 8px;
background: #f5f5f5;
border: 1px solid #eee;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: center;
margin-bottom: 12px; color: #ccc;
font-size: 20px;
} }
.component-type { /* Type badge */
.type-badge {
font-size: 13px; font-size: 13px;
font-weight: 600; font-weight: 600;
color: #1677ff; color: #1677ff;
background: #e6f4ff; background: #e6f4ff;
padding: 4px 12px; padding: 4px 10px;
border-radius: 6px; border-radius: 6px;
display: inline-block;
} }
.card-actions { /* Price cell */
.price-cell {
font-weight: 700;
color: #ff4d4f;
white-space: nowrap;
}
.price-cell .currency {
font-size: 12px;
font-weight: 500;
}
/* Link cell */
.link-cell a {
color: #1677ff;
text-decoration: none;
font-size: 13px;
}
.link-cell a:hover { text-decoration: underline; }
/* Alt selector */
.alt-selector {
margin-top: 4px;
}
.alt-label {
font-size: 11px;
color: #999;
margin-bottom: 4px;
}
.alt-options {
display: flex; display: flex;
gap: 6px; gap: 4px;
flex-wrap: wrap;
} }
.card-actions button { .alt-pill {
padding: 3px 8px;
border: 1px solid #e8e8e8;
border-radius: 4px;
font-size: 11px;
cursor: pointer;
background: #fff;
color: #666;
transition: all 0.2s;
}
.alt-pill:hover { border-color: #1677ff; color: #1677ff; }
.alt-pill.active { border-color: #1677ff; background: #e6f4ff; color: #1677ff; font-weight: 500; }
/* Action cells */
.action-cell {
white-space: nowrap;
}
.action-cell button {
background: none; background: none;
border: 1px solid #e8e8e8; border: 1px solid #e8e8e8;
border-radius: 6px; border-radius: 6px;
@@ -154,107 +230,17 @@ body {
font-size: 12px; font-size: 12px;
color: #666; color: #666;
transition: all 0.2s; transition: all 0.2s;
margin-left: 4px;
} }
.card-actions button:hover { .action-cell button:first-child { margin-left: 0; }
border-color: #1677ff; .action-cell button:hover { border-color: #1677ff; color: #1677ff; }
color: #1677ff; .action-cell button.delete-btn:hover { border-color: #ff4d4f; color: #ff4d4f; }
}
.card-actions button.delete-btn:hover { /* Add button row */
border-color: #ff4d4f; .add-row {
color: #ff4d4f; padding: 16px;
} text-align: center;
.card-body {
display: grid;
grid-template-columns: 1fr auto;
gap: 8px 20px;
align-items: start;
}
.card-info {
display: flex;
flex-direction: column;
gap: 6px;
}
.info-row {
display: flex;
align-items: baseline;
gap: 8px;
}
.info-label {
font-size: 12px;
color: #999;
min-width: 42px;
}
.info-value {
font-size: 14px;
color: #333;
}
.info-value a {
color: #1677ff;
text-decoration: none;
}
.info-value a:hover { text-decoration: underline; }
.card-price {
font-size: 22px;
font-weight: 700;
color: #ff4d4f;
white-space: nowrap;
}
.card-price .currency {
font-size: 14px;
font-weight: 500;
}
/* Alternative Selector */
.alt-selector {
grid-column: 1 / -1;
margin-top: 8px;
padding-top: 12px;
border-top: 1px solid #f0f0f0;
}
.alt-label {
font-size: 12px;
color: #999;
margin-bottom: 6px;
}
.alt-options {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.alt-pill {
padding: 5px 12px;
border: 1px solid #e8e8e8;
border-radius: 6px;
font-size: 12px;
cursor: pointer;
background: #fff;
color: #666;
transition: all 0.2s;
}
.alt-pill:hover {
border-color: #1677ff;
color: #1677ff;
}
.alt-pill.active {
border-color: #1677ff;
background: #e6f4ff;
color: #1677ff;
font-weight: 500;
} }
/* Footer / Total */ /* Footer / Total */
@@ -270,17 +256,14 @@ body {
} }
.footer-inner { .footer-inner {
max-width: 960px; max-width: 1100px;
margin: 0 auto; margin: 0 auto;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
} }
.footer-label { .footer-label { font-size: 14px; color: #666; }
font-size: 14px;
color: #666;
}
.footer-total { .footer-total {
font-size: 28px; font-size: 28px;
@@ -288,9 +271,7 @@ body {
color: #ff4d4f; color: #ff4d4f;
} }
.footer-total .currency { .footer-total .currency { font-size: 16px; }
font-size: 16px;
}
/* Modal */ /* Modal */
.modal-overlay { .modal-overlay {
@@ -303,14 +284,12 @@ body {
align-items: center; align-items: center;
} }
.modal-overlay.active { .modal-overlay.active { display: flex; }
display: flex;
}
.modal { .modal {
background: #fff; background: #fff;
border-radius: 12px; border-radius: 12px;
width: 560px; width: 600px;
max-width: 90vw; max-width: 90vw;
max-height: 85vh; max-height: 85vh;
overflow-y: auto; overflow-y: auto;
@@ -324,10 +303,7 @@ body {
justify-content: space-between; justify-content: space-between;
} }
.modal-header h2 { .modal-header h2 { font-size: 18px; font-weight: 600; }
font-size: 18px;
font-weight: 600;
}
.modal-close { .modal-close {
background: none; background: none;
@@ -339,13 +315,9 @@ body {
} }
.modal-close:hover { color: #333; } .modal-close:hover { color: #333; }
.modal-body { .modal-body { padding: 20px 24px; }
padding: 20px 24px;
}
.form-group { .form-group { margin-bottom: 16px; }
margin-bottom: 16px;
}
.form-group label { .form-group label {
display: block; display: block;
@@ -356,8 +328,7 @@ body {
} }
.form-group input, .form-group input,
.form-group select, .form-group select {
.form-group textarea {
width: 100%; width: 100%;
padding: 8px 12px; padding: 8px 12px;
border: 1px solid #d9d9d9; border: 1px solid #d9d9d9;
@@ -368,8 +339,7 @@ body {
} }
.form-group input:focus, .form-group input:focus,
.form-group select:focus, .form-group select:focus {
.form-group textarea:focus {
outline: none; outline: none;
border-color: #1677ff; border-color: #1677ff;
box-shadow: 0 0 0 2px rgba(22,119,255,0.1); box-shadow: 0 0 0 2px rgba(22,119,255,0.1);
@@ -395,17 +365,13 @@ body {
margin-bottom: 12px; margin-bottom: 12px;
} }
.alt-section-header h3 { .alt-section-header h3 { font-size: 14px; font-weight: 600; }
font-size: 14px;
font-weight: 600;
}
.alt-item { .alt-item {
background: #fafafa; background: #fafafa;
border-radius: 8px; border-radius: 8px;
padding: 12px; padding: 12px;
margin-bottom: 8px; margin-bottom: 8px;
position: relative;
} }
.alt-item-header { .alt-item-header {
@@ -415,11 +381,7 @@ body {
margin-bottom: 8px; margin-bottom: 8px;
} }
.alt-item-label { .alt-item-label { font-size: 12px; font-weight: 600; color: #1677ff; }
font-size: 12px;
font-weight: 600;
color: #1677ff;
}
.alt-item-remove { .alt-item-remove {
background: none; background: none;
@@ -444,10 +406,7 @@ body {
color: #999; color: #999;
} }
.empty-state p { .empty-state p { margin-bottom: 16px; font-size: 15px; }
margin-bottom: 16px;
font-size: 15px;
}
/* Import Overlay */ /* Import Overlay */
.import-overlay { .import-overlay {
@@ -460,9 +419,7 @@ body {
align-items: center; align-items: center;
} }
.import-overlay.active { .import-overlay.active { display: flex; }
display: flex;
}
.import-box { .import-box {
background: #fff; background: #fff;
@@ -472,18 +429,14 @@ body {
box-shadow: 0 8px 32px rgba(0,0,0,0.2); box-shadow: 0 8px 32px rgba(0,0,0,0.2);
} }
.import-box p { .import-box p { margin-bottom: 16px; color: #666; }
margin-bottom: 16px;
color: #666;
}
/* Responsive */ /* Responsive */
@media (max-width: 640px) { @media (max-width: 768px) {
.container { padding: 12px; } .container { padding: 12px; }
.header { padding: 16px; } .header { padding: 16px; }
.component-card { padding: 16px; } .config-table-wrap { overflow-x: auto; }
.card-body { grid-template-columns: 1fr; } .config-table { min-width: 700px; }
.card-price { justify-self: start; }
.form-row { grid-template-columns: 1fr; } .form-row { grid-template-columns: 1fr; }
} }
</style> </style>
@@ -491,7 +444,6 @@ body {
<body> <body>
<div class="container"> <div class="container">
<!-- Header -->
<div class="header"> <div class="header">
<h1 id="configName">PC装机配置表</h1> <h1 id="configName">PC装机配置表</h1>
<div class="header-actions"> <div class="header-actions">
@@ -504,12 +456,11 @@ body {
</div> </div>
</div> </div>
<!-- Component List --> <div id="contentArea"></div>
<div class="component-list" id="componentList"></div>
</div> </div>
<!-- Footer Total --> <!-- Footer Total -->
<div class="footer" id="footer"> <div class="footer">
<div class="footer-inner"> <div class="footer-inner">
<span class="footer-label">总计</span> <span class="footer-label">总计</span>
<span class="footer-total"><span class="currency">¥</span><span id="totalPrice">0</span></span> <span class="footer-total"><span class="currency">¥</span><span id="totalPrice">0</span></span>
@@ -525,7 +476,6 @@ body {
</div> </div>
<div class="modal-body"> <div class="modal-body">
<input type="hidden" id="editComponentIndex" value="-1"> <input type="hidden" id="editComponentIndex" value="-1">
<input type="hidden" id="editAltIndex" value="-1">
<div class="form-group"> <div class="form-group">
<label>部件类型</label> <label>部件类型</label>
@@ -545,6 +495,11 @@ body {
<input type="text" id="formTypeCustom" style="display:none;margin-top:6px" placeholder="输入自定义类型名称"> <input type="text" id="formTypeCustom" style="display:none;margin-top:6px" placeholder="输入自定义类型名称">
</div> </div>
<div class="form-group">
<label>图片链接</label>
<input type="url" id="formImage" placeholder="https://example.com/image.jpg">
</div>
<div class="form-row"> <div class="form-row">
<div class="form-group"> <div class="form-group">
<label>品牌</label> <label>品牌</label>
@@ -619,34 +574,21 @@ body {
// ========== State ========== // ========== State ==========
let config = { configName: 'PC装机配置表', components: [] }; let config = { configName: 'PC装机配置表', components: [] };
let currentMode = 'view'; let currentMode = 'view';
let tempAlts = []; // temporary alt items in modal let tempAlts = [];
// ========== Preset Types ==========
const PRESET_TYPES = ['CPU', '主板', '显卡', '内存', '硬盘', '电源', '机箱', '散热器', '显示器']; const PRESET_TYPES = ['CPU', '主板', '显卡', '内存', '硬盘', '电源', '机箱', '散热器', '显示器'];
// ========== Init ========== // ========== Init ==========
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', loadFromURL);
loadFromURL();
});
function loadFromURL() { function loadFromURL() {
const params = new URLSearchParams(window.location.search); const params = new URLSearchParams(window.location.search);
const configURL = params.get('config'); const configURL = params.get('config');
if (configURL) { if (configURL) {
fetch(configURL) fetch(configURL)
.then(r => { .then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
if (!r.ok) throw new Error(`HTTP ${r.status}`); .then(data => { config = data; document.getElementById('configName').textContent = config.configName || 'PC装机配置表'; render(); })
return r.json(); .catch(err => { console.error('加载配置失败:', err); render(); });
})
.then(data => {
config = data;
document.getElementById('configName').textContent = config.configName || 'PC装机配置表';
render();
})
.catch(err => {
console.error('加载配置失败:', err);
render();
});
} else { } else {
render(); render();
} }
@@ -663,10 +605,10 @@ function setMode(mode) {
// ========== Render ========== // ========== Render ==========
function render() { function render() {
const list = document.getElementById('componentList'); const area = document.getElementById('contentArea');
if (!config.components || config.components.length === 0) { if (!config.components || config.components.length === 0) {
list.innerHTML = ` area.innerHTML = `
<div class="empty-state"> <div class="empty-state">
<p>暂无配置数据</p> <p>暂无配置数据</p>
<button class="btn btn-primary" onclick="loadSample()">加载示例配置</button> <button class="btn btn-primary" onclick="loadSample()">加载示例配置</button>
@@ -676,62 +618,79 @@ function render() {
return; return;
} }
list.innerHTML = config.components.map((comp, i) => renderCard(comp, i)).join(''); const editHeaders = currentMode === 'edit' ? '<th style="width:100px">操作</th>' : '';
updateTotal();
}
function renderCard(comp, index) { let rows = config.components.map((comp, i) => {
const selectedIdx = comp.selected || 0; const selectedIdx = comp.selected || 0;
const opt = comp.options[selectedIdx]; const opt = comp.options[selectedIdx];
if (!opt) return ''; if (!opt) return '';
const editActions = currentMode === 'edit' ? ` const imgCell = opt.image
<div class="card-actions"> ? `<img class="cell-img" src="${opt.image}" alt="${opt.model}" onerror="this.outerHTML='<div class=\\'cell-img-placeholder\\'> </div>'">`
<button onclick="editComponent(${index})">编辑</button> : '<div class="cell-img-placeholder"> </div>';
<button class="delete-btn" onclick="confirmDelete(${index})">删除</button>
</div>` : '';
const altSelector = comp.options.length > 1 ? ` const linkCell = opt.link
? `<a href="${opt.link}" target="_blank" rel="noopener">查看商品</a>`
: '-';
const altCell = comp.options.length > 1 ? `
<div class="alt-selector"> <div class="alt-selector">
<div class="alt-label">备选方案(${comp.options.length}个可选)</div> <div class="alt-label">备选 (${comp.options.length})</div>
<div class="alt-options"> <div class="alt-options">
${comp.options.map((o, j) => ` ${comp.options.map((o, j) => `
<button class="alt-pill ${j === selectedIdx ? 'active' : ''}" onclick="selectAlt(${index}, ${j})"> <button class="alt-pill ${j === selectedIdx ? 'active' : ''}" onclick="selectAlt(${i}, ${j})">
${o.brand} ${o.model} ¥${o.price} ${o.brand} ${o.model} ¥${o.price}
</button> </button>
`).join('')} `).join('')}
</div> </div>
</div>` : ''; </div>` : '';
const actionCell = currentMode === 'edit' ? `
<td class="action-cell">
<button onclick="editComponent(${i})">编辑</button>
<button class="delete-btn" onclick="confirmDelete(${i})">删除</button>
</td>` : '';
return ` return `
<div class="component-card"> <tr>
<div class="card-header"> <td>${imgCell}</td>
<span class="component-type">${comp.type}</span> <td><span class="type-badge">${comp.type}</span></td>
${editActions} <td>${opt.brand || '-'}</td>
</div> <td>${opt.model || '-'}</td>
<div class="card-body"> <td>${opt.specs || '-'}</td>
<div class="card-info"> <td class="price-cell"><span class="currency">¥</span>${(opt.price || 0).toLocaleString()}</td>
<div class="info-row"> <td class="link-cell">${linkCell}</td>
<span class="info-label">品牌</span> <td>${altCell}</td>
<span class="info-value">${opt.brand || '-'}</span> ${actionCell}
</div> </tr>`;
<div class="info-row"> }).join('');
<span class="info-label">型号</span>
<span class="info-value">${opt.model || '-'}</span> const addRow = currentMode === 'edit'
</div> ? `<div class="add-row"><button class="btn btn-primary" onclick="addComponent()">+ 添加部件</button></div>`
<div class="info-row"> : '';
<span class="info-label">参数</span>
<span class="info-value">${opt.specs || '-'}</span> area.innerHTML = `
</div> <div class="config-table-wrap">
<div class="info-row"> <table class="config-table">
<span class="info-label">链接</span> <thead>
<span class="info-value">${opt.link ? `<a href="${opt.link}" target="_blank" rel="noopener">查看商品</a>` : '-'}</span> <tr>
</div> <th style="width:80px">图片</th>
</div> <th style="width:80px">类型</th>
<div class="card-price"><span class="currency">¥</span>${opt.price || 0}</div> <th style="width:80px">品牌</th>
${altSelector} <th style="width:120px">型号</th>
</div> <th>参数</th>
<th style="width:100px">价格</th>
<th style="width:80px">链接</th>
<th style="width:200px">备选</th>
${editHeaders}
</tr>
</thead>
<tbody>${rows}</tbody>
</table>
${addRow}
</div>`; </div>`;
updateTotal();
} }
// ========== Total Price ========== // ========== Total Price ==========
@@ -762,12 +721,10 @@ function closeImport() {
function handleFileImport(event) { function handleFileImport(event) {
const file = event.target.files[0]; const file = event.target.files[0];
if (!file) return; if (!file) return;
const reader = new FileReader(); const reader = new FileReader();
reader.onload = (e) => { reader.onload = (e) => {
try { try {
const data = JSON.parse(e.target.result); config = JSON.parse(e.target.result);
config = data;
document.getElementById('configName').textContent = config.configName || 'PC装机配置表'; document.getElementById('configName').textContent = config.configName || 'PC装机配置表';
closeImport(); closeImport();
render(); render();
@@ -781,11 +738,7 @@ function handleFileImport(event) {
function loadSample() { function loadSample() {
fetch('sample_config.json') fetch('sample_config.json')
.then(r => r.json()) .then(r => r.json())
.then(data => { .then(data => { config = data; document.getElementById('configName').textContent = config.configName || 'PC装机配置表'; render(); })
config = data;
document.getElementById('configName').textContent = config.configName || 'PC装机配置表';
render();
})
.catch(err => alert('加载示例失败: ' + err.message)); .catch(err => alert('加载示例失败: ' + err.message));
} }
@@ -801,7 +754,7 @@ function exportJSON() {
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
} }
// ========== Edit Modal ========== // ========== Modal ==========
function openModal(title) { function openModal(title) {
document.getElementById('modalTitle').textContent = title; document.getElementById('modalTitle').textContent = title;
document.getElementById('editModal').classList.add('active'); document.getElementById('editModal').classList.add('active');
@@ -814,16 +767,12 @@ function closeModal() {
function onTypeChange() { function onTypeChange() {
const sel = document.getElementById('formType'); const sel = document.getElementById('formType');
const custom = document.getElementById('formTypeCustom'); document.getElementById('formTypeCustom').style.display = sel.value === '__custom__' ? '' : 'none';
custom.style.display = sel.value === '__custom__' ? '' : 'none';
} }
function getFormType() { function getFormType() {
const sel = document.getElementById('formType'); const sel = document.getElementById('formType');
if (sel.value === '__custom__') { return sel.value === '__custom__' ? document.getElementById('formTypeCustom').value.trim() : sel.value;
return document.getElementById('formTypeCustom').value.trim();
}
return sel.value;
} }
function setFormType(type) { function setFormType(type) {
@@ -841,6 +790,7 @@ function setFormType(type) {
function getFormData() { function getFormData() {
return { return {
image: document.getElementById('formImage').value.trim(),
brand: document.getElementById('formBrand').value.trim(), brand: document.getElementById('formBrand').value.trim(),
model: document.getElementById('formModel').value.trim(), model: document.getElementById('formModel').value.trim(),
specs: document.getElementById('formSpecs').value.trim(), specs: document.getElementById('formSpecs').value.trim(),
@@ -850,6 +800,7 @@ function getFormData() {
} }
function setFormData(opt) { function setFormData(opt) {
document.getElementById('formImage').value = opt.image || '';
document.getElementById('formBrand').value = opt.brand || ''; document.getElementById('formBrand').value = opt.brand || '';
document.getElementById('formModel').value = opt.model || ''; document.getElementById('formModel').value = opt.model || '';
document.getElementById('formSpecs').value = opt.specs || ''; document.getElementById('formSpecs').value = opt.specs || '';
@@ -861,16 +812,14 @@ function clearForm() {
document.getElementById('formType').value = ''; document.getElementById('formType').value = '';
document.getElementById('formTypeCustom').value = ''; document.getElementById('formTypeCustom').value = '';
document.getElementById('formTypeCustom').style.display = 'none'; document.getElementById('formTypeCustom').style.display = 'none';
document.getElementById('formBrand').value = ''; ['formImage', 'formBrand', 'formModel', 'formSpecs', 'formPrice', 'formLink'].forEach(id => {
document.getElementById('formModel').value = ''; document.getElementById(id).value = '';
document.getElementById('formSpecs').value = ''; });
document.getElementById('formPrice').value = '';
document.getElementById('formLink').value = '';
tempAlts = []; tempAlts = [];
renderAltList(); renderAltList();
} }
// ========== Add Component ========== // ========== Add / Edit Component ==========
function addComponent() { function addComponent() {
clearForm(); clearForm();
document.getElementById('editComponentIndex').value = -1; document.getElementById('editComponentIndex').value = -1;
@@ -878,28 +827,20 @@ function addComponent() {
openModal('添加部件'); openModal('添加部件');
} }
// ========== Edit Component ==========
function editComponent(index) { function editComponent(index) {
const comp = config.components[index]; const comp = config.components[index];
document.getElementById('editComponentIndex').value = index; document.getElementById('editComponentIndex').value = index;
setFormType(comp.type); setFormType(comp.type);
setFormData(comp.options[0] || {});
// Load first option as main
const opt = comp.options[0] || {};
setFormData(opt);
// Load alternatives (skip first)
tempAlts = comp.options.slice(1).map(o => ({...o})); tempAlts = comp.options.slice(1).map(o => ({...o}));
document.getElementById('altSection').style.display = tempAlts.length > 0 || comp.options.length > 0 ? '' : 'none'; document.getElementById('altSection').style.display = tempAlts.length > 0 || comp.options.length > 0 ? '' : 'none';
renderAltList(); renderAltList();
openModal('编辑部件'); openModal('编辑部件');
} }
// ========== Alternatives in Modal ========== // ========== Alternatives in Modal ==========
function addAltItem() { function addAltItem() {
tempAlts.push({ brand: '', model: '', specs: '', price: 0, link: '' }); tempAlts.push({ image: '', brand: '', model: '', specs: '', price: 0, link: '' });
renderAltList(); renderAltList();
} }
@@ -914,13 +855,16 @@ function renderAltList() {
container.innerHTML = '<p style="font-size:12px;color:#999">暂无备选配置,点击上方按钮添加</p>'; container.innerHTML = '<p style="font-size:12px;color:#999">暂无备选配置,点击上方按钮添加</p>';
return; return;
} }
container.innerHTML = tempAlts.map((alt, i) => ` container.innerHTML = tempAlts.map((alt, i) => `
<div class="alt-item"> <div class="alt-item">
<div class="alt-item-header"> <div class="alt-item-header">
<span class="alt-item-label">备选 ${i + 1}</span> <span class="alt-item-label">备选 ${i + 1}</span>
<button class="alt-item-remove" onclick="removeAltItem(${i})">删除</button> <button class="alt-item-remove" onclick="removeAltItem(${i})">删除</button>
</div> </div>
<div class="form-group">
<label>图片链接</label>
<input type="url" value="${alt.image || ''}" onchange="tempAlts[${i}].image=this.value" placeholder="https://example.com/image.jpg">
</div>
<div class="form-row"> <div class="form-row">
<div class="form-group"> <div class="form-group">
<label>品牌</label> <label>品牌</label>
@@ -952,39 +896,27 @@ function renderAltList() {
// ========== Save Component ========== // ========== Save Component ==========
function saveComponent() { function saveComponent() {
const type = getFormType(); const type = getFormType();
if (!type) { if (!type) { alert('请选择或输入部件类型'); return; }
alert('请选择或输入部件类型');
return;
}
const mainOpt = getFormData(); const mainOpt = getFormData();
const options = [mainOpt, ...tempAlts]; const options = [mainOpt, ...tempAlts];
const index = parseInt(document.getElementById('editComponentIndex').value); const index = parseInt(document.getElementById('editComponentIndex').value);
if (index >= 0) { if (index >= 0) {
// Edit existing
config.components[index].type = type; config.components[index].type = type;
config.components[index].options = options; config.components[index].options = options;
if (config.components[index].selected >= options.length) { if (config.components[index].selected >= options.length) config.components[index].selected = 0;
config.components[index].selected = 0;
}
} else { } else {
// Add new config.components.push({ type, selected: 0, options });
config.components.push({
type: type,
selected: 0,
options: options
});
} }
closeModal(); closeModal();
render(); render();
} }
// ========== Delete Component ========== // ========== Delete ==========
function confirmDelete(index) { function confirmDelete(index) {
document.getElementById('confirmMessage').textContent = document.getElementById('confirmMessage').textContent = `确定要删除「${config.components[index].type}」吗?`;
`确定要删除「${config.components[index].type}」吗?`;
document.getElementById('confirmDeleteBtn').onclick = () => { document.getElementById('confirmDeleteBtn').onclick = () => {
config.components.splice(index, 1); config.components.splice(index, 1);
closeConfirm(); closeConfirm();
@@ -997,13 +929,9 @@ function closeConfirm() {
document.getElementById('confirmModal').classList.remove('active'); document.getElementById('confirmModal').classList.remove('active');
} }
// ========== Keyboard Shortcuts ========== // ========== Keyboard ==========
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') { if (e.key === 'Escape') { closeModal(); closeImport(); closeConfirm(); }
closeModal();
closeImport();
closeConfirm();
}
}); });
</script> </script>
+14 -14
View File
@@ -5,62 +5,62 @@
"type": "CPU", "type": "CPU",
"selected": 0, "selected": 0,
"options": [ "options": [
{ "brand": "Intel", "model": "i7-14700K", "specs": "20核28线程 / 3.4GHz", "price": 2499, "link": "https://item.jd.com/100060726782.html" }, { "image": "", "brand": "Intel", "model": "i7-14700K", "specs": "20核28线程 / 3.4GHz", "price": 2499, "link": "https://item.jd.com/100060726782.html" },
{ "brand": "AMD", "model": "R7 7800X3D", "specs": "8核16线程 / 4.2GHz", "price": 2199, "link": "https://item.jd.com/100057602578.html" } { "image": "", "brand": "AMD", "model": "R7 7800X3D", "specs": "8核16线程 / 4.2GHz", "price": 2199, "link": "https://item.jd.com/100057602578.html" }
] ]
}, },
{ {
"type": "主板", "type": "主板",
"selected": 0, "selected": 0,
"options": [ "options": [
{ "brand": "华硕", "model": "ROG STRIX Z790-E", "specs": "ATX / DDR5 / WiFi", "price": 2899, "link": "https://item.jd.com/100056938678.html" }, { "image": "", "brand": "华硕", "model": "ROG STRIX Z790-E", "specs": "ATX / DDR5 / WiFi", "price": 2899, "link": "https://item.jd.com/100056938678.html" },
{ "brand": "微星", "model": "MPG Z790 EDGE", "specs": "ATX / DDR5 / WiFi", "price": 1999, "link": "https://item.jd.com/100058365432.html" } { "image": "", "brand": "微星", "model": "MPG Z790 EDGE", "specs": "ATX / DDR5 / WiFi", "price": 1999, "link": "https://item.jd.com/100058365432.html" }
] ]
}, },
{ {
"type": "显卡", "type": "显卡",
"selected": 0, "selected": 0,
"options": [ "options": [
{ "brand": "NVIDIA", "model": "RTX 4070 Ti Super", "specs": "16GB GDDR6X", "price": 6499, "link": "https://item.jd.com/100080746782.html" }, { "image": "", "brand": "NVIDIA", "model": "RTX 4070 Ti Super", "specs": "16GB GDDR6X", "price": 6499, "link": "https://item.jd.com/100080746782.html" },
{ "brand": "AMD", "model": "RX 7900 XT", "specs": "20GB GDDR6", "price": 5299, "link": "https://item.jd.com/100060987654.html" } { "image": "", "brand": "AMD", "model": "RX 7900 XT", "specs": "20GB GDDR6", "price": 5299, "link": "https://item.jd.com/100060987654.html" }
] ]
}, },
{ {
"type": "内存", "type": "内存",
"selected": 0, "selected": 0,
"options": [ "options": [
{ "brand": "金士顿", "model": "FURY Beast DDR5", "specs": "32GB(16G×2) / 6000MHz", "price": 699, "link": "https://item.jd.com/100041234567.html" }, { "image": "", "brand": "金士顿", "model": "FURY Beast DDR5", "specs": "32GB(16G×2) / 6000MHz", "price": 699, "link": "https://item.jd.com/100041234567.html" },
{ "brand": "芝奇", "model": "Trident Z5 DDR5", "specs": "32GB(16G×2) / 6400MHz", "price": 899, "link": "https://item.jd.com/100039876543.html" } { "image": "", "brand": "芝奇", "model": "Trident Z5 DDR5", "specs": "32GB(16G×2) / 6400MHz", "price": 899, "link": "https://item.jd.com/100039876543.html" }
] ]
}, },
{ {
"type": "硬盘", "type": "硬盘",
"selected": 0, "selected": 0,
"options": [ "options": [
{ "brand": "三星", "model": "990 PRO", "specs": "1TB / NVMe PCIe 4.0", "price": 699, "link": "https://item.jd.com/100038765432.html" }, { "image": "", "brand": "三星", "model": "990 PRO", "specs": "1TB / NVMe PCIe 4.0", "price": 699, "link": "https://item.jd.com/100038765432.html" },
{ "brand": "西部数据", "model": "SN850X", "specs": "1TB / NVMe PCIe 4.0", "price": 599, "link": "https://item.jd.com/100037654321.html" } { "image": "", "brand": "西部数据", "model": "SN850X", "specs": "1TB / NVMe PCIe 4.0", "price": 599, "link": "https://item.jd.com/100037654321.html" }
] ]
}, },
{ {
"type": "电源", "type": "电源",
"selected": 0, "selected": 0,
"options": [ "options": [
{ "brand": "海韵", "model": "FOCUS GX-850", "specs": "850W / 80Plus金牌 / 全模组", "price": 899, "link": "https://item.jd.com/100036543210.html" } { "image": "", "brand": "海韵", "model": "FOCUS GX-850", "specs": "850W / 80Plus金牌 / 全模组", "price": 899, "link": "https://item.jd.com/100036543210.html" }
] ]
}, },
{ {
"type": "机箱", "type": "机箱",
"selected": 0, "selected": 0,
"options": [ "options": [
{ "brand": "联力", "model": "O11 Dynamic EVO", "specs": "中塔 / 玻璃侧透", "price": 999, "link": "https://item.jd.com/100035432109.html" } { "image": "", "brand": "联力", "model": "O11 Dynamic EVO", "specs": "中塔 / 玻璃侧透", "price": 999, "link": "https://item.jd.com/100035432109.html" }
] ]
}, },
{ {
"type": "散热器", "type": "散热器",
"selected": 0, "selected": 0,
"options": [ "options": [
{ "brand": "利民", "model": "Frozen Magic 360", "specs": "360mm一体式水冷", "price": 499, "link": "https://item.jd.com/100034321098.html" }, { "image": "", "brand": "利民", "model": "Frozen Magic 360", "specs": "360mm一体式水冷", "price": 499, "link": "https://item.jd.com/100034321098.html" },
{ "brand": "猫头鹰", "model": "NH-D15", "specs": "双塔风冷 / 150W+", "price": 699, "link": "https://item.jd.com/100033210987.html" } { "image": "", "brand": "猫头鹰", "model": "NH-D15", "specs": "双塔风冷 / 150W+", "price": 699, "link": "https://item.jd.com/100033210987.html" }
] ]
} }
] ]