feat: 参数字段管理功能 - 类别和子类别可配置参数列表

This commit is contained in:
2026-04-28 12:50:10 +08:00
parent 146efdf6bd
commit 3f7a5dd5a1
4 changed files with 258 additions and 59 deletions

View File

@@ -2,12 +2,15 @@
{
"name": "比亚迪宋plus dmi 2021款",
"brand": "比亚迪",
"price": "18.87",
"price": 18.87,
"year": "2021",
"category_id": "021dc76d36be",
"id": "3d20dbcd4bdd",
"created_at": "2026-04-09 10:09:56",
"subcategory_id": "sedan"
"subcategory_id": "suv",
"views": 0,
"images": [],
"updated_at": "2026-04-28 12:32:13"
},
{
"name": "秦PLUS",

View File

@@ -3,75 +3,51 @@
"name": "Osmo Pocket 4",
"brand": "DJI",
"price": 2999,
"specs": {
"传感器类型": "1英寸CMOS",
"镜头": "20mm, f/2.0",
"ISO范围": "50-12800",
"视频分辨率": "4K 60fps",
"照片最大分辨率": "5472×3648",
"电池容量": "1545mAh",
"工作温度": "0°C至40°C"
},
"specs": "[object Object]",
"id": "597e29af5937",
"category_id": "71fa2b4d818f",
"created_at": "2026-04-28 00:07:01",
"visible": true,
"raw_text": "",
"images": [
"/static/uploads/1ad784e0b3c6_1777305525.png"
],
"images": [],
"publish_date": "",
"views": 0,
"is_pinned": false
"is_pinned": false,
"subcategory_id": "90ce312b560d",
"updated_at": "2026-04-28 12:32:38"
},
{
"name": "Osmo Pocket 3",
"brand": "DJI",
"price": 2799,
"specs": {
"传感器类型": "1英寸CMOS",
"镜头": "20mm, f/2.0",
"ISO范围": "50-6400",
"视频分辨率": "4K 60fps",
"照片最大分辨率": "5472×3648",
"电池容量": "1300mAh",
"工作温度": "0°C至40°C"
},
"specs": "[object Object]",
"id": "ad10ac80827b",
"category_id": "71fa2b4d818f",
"created_at": "2026-04-28 00:07:01",
"visible": true,
"raw_text": "",
"images": [
"/static/uploads/1ad784e0b3c6_1777305525.png"
],
"images": [],
"publish_date": "",
"views": 0,
"is_pinned": false
"is_pinned": false,
"subcategory_id": "90ce312b560d",
"updated_at": "2026-04-28 12:32:43"
},
{
"name": "DJI Pocket 2",
"brand": "DJI",
"price": 1999,
"specs": {
"传感器类型": "1/1.7英寸CMOS",
"镜头": "20mm, f/1.8",
"ISO范围": "100-3200",
"视频分辨率": "4K 60fps",
"照片最大分辨率": "6272×4680",
"电池容量": "875mAh",
"工作温度": "0°C至40°C"
},
"specs": "[object Object]",
"id": "0fde0f10ad96",
"category_id": "71fa2b4d818f",
"created_at": "2026-04-28 00:07:01",
"visible": true,
"raw_text": "",
"images": [
"/static/uploads/1ad784e0b3c6_1777305525.png"
],
"images": [],
"publish_date": "",
"views": 0,
"is_pinned": false
"is_pinned": false,
"subcategory_id": "90ce312b560d",
"updated_at": "2026-04-28 12:32:50"
}
]

View File

@@ -205,7 +205,7 @@
"license": "Proprietary",
"description": "智谱AI大模型中文能力强",
"created_at": "2024-01-01",
"visible": false,
"visible": true,
"subcategory_id": "chat"
}
]

View File

@@ -141,11 +141,12 @@
<th class="px-4 py-3 text-left text-sm font-medium text-gray-600">ID</th>
<th class="px-4 py-3 text-left text-sm font-medium text-gray-600">名称</th>
<th class="px-4 py-3 text-left text-sm font-medium text-gray-600">类型</th>
<th class="px-4 py-3 text-left text-sm font-medium text-gray-600">参数字段</th>
<th class="px-4 py-3 text-left text-sm font-medium text-gray-600">子类别</th>
<th class="px-4 py-3 text-center text-sm font-medium text-gray-600">操作</th>
</tr>
</thead>
<tbody id="admin-categories-table"><tr><td colspan="6" class="text-center text-gray-400 py-8">加载中...</td></tr></tbody>
<tbody id="admin-categories-table"><tr><td colspan="7" class="text-center text-gray-400 py-8">加载中...</td></tr></tbody>
</table>
</div>
</section>
@@ -392,21 +393,54 @@
<!-- 子类别编辑弹框 -->
<div id="subcategoryModal" class="fixed inset-0 bg-black/50 z-50 hidden flex items-center justify-center">
<div class="bg-white rounded-xl max-w-lg w-full mx-4">
<div class="p-6 border-b flex justify-between items-center">
<div class="bg-white rounded-xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-auto">
<div class="p-6 border-b flex justify-between items-center sticky top-0 bg-white z-10">
<h2 class="text-xl font-bold text-gray-800" id="subcategoryModalTitle"><i class="ri-folder-line mr-2"></i>添加子类别</h2>
<button onclick="closeSubcategoryModal()" class="text-gray-400 hover:text-gray-600"><i class="ri-close-line text-2xl"></i></button>
</div>
<div id="subcategoryModalContent" class="p-6">
<!-- 动态内容 -->
</div>
<div class="p-6 border-t flex justify-end gap-4">
<div class="p-6 border-t flex justify-end gap-4 sticky bottom-0 bg-white">
<button onclick="closeSubcategoryModal()" class="px-4 py-2 bg-gray-200 text-gray-600 rounded-lg hover:bg-gray-300">取消</button>
<button onclick="saveSubcategory()" class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700"><i class="ri-save-line mr-1"></i>保存</button>
</div>
</div>
</div>
<!-- 参数字段编辑弹框 -->
<div id="fieldModal" class="fixed inset-0 bg-black/50 z-50 hidden flex items-center justify-center">
<div class="bg-white rounded-xl max-w-lg w-full mx-4">
<div class="p-6 border-b flex justify-between items-center">
<h2 class="text-xl font-bold text-gray-800" id="fieldModalTitle"><i class="ri-list-settings-line mr-2"></i>添加参数字段</h2>
<button onclick="closeFieldModal()" class="text-gray-400 hover:text-gray-600"><i class="ri-close-line text-2xl"></i></button>
</div>
<div id="fieldModalContent" class="p-6">
<div class="space-y-4">
<div><label class="text-sm text-gray-600 mb-1 block">字段名 *</label><input type="text" id="field_key" class="w-full px-3 py-2 border rounded-lg" placeholder="如context_length"></div>
<div><label class="text-sm text-gray-600 mb-1 block">显示名 *</label><input type="text" id="field_label" class="w-full px-3 py-2 border rounded-lg" placeholder="如:上下文长度"></div>
<div><label class="text-sm text-gray-600 mb-1 block">字段类型</label><select id="field_type" class="w-full px-3 py-2 border rounded-lg">
<option value="text">文本</option>
<option value="number">数字</option>
<option value="date">日期</option>
<option value="boolean">布尔值</option>
<option value="json">JSON对象</option>
<option value="url">URL链接</option>
</select></div>
<div><label class="text-sm text-gray-600 mb-1 block">说明</label><textarea id="field_desc" rows="2" class="w-full px-3 py-2 border rounded-lg" placeholder="字段用途说明"></textarea></div>
<div class="flex items-center gap-2">
<input type="checkbox" id="field_required" class="rounded">
<label class="text-sm text-gray-600">必填字段</label>
</div>
</div>
</div>
<div class="p-6 border-t flex justify-end gap-4">
<button onclick="closeFieldModal()" class="px-4 py-2 bg-gray-200 text-gray-600 rounded-lg hover:bg-gray-300">取消</button>
<button onclick="saveField()" class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700"><i class="ri-save-line mr-1"></i>保存</button>
</div>
</div>
</div>
<!-- 智能补充弹窗 -->
<div id="smartUpdateModal" class="fixed inset-0 bg-black/50 z-50 hidden flex items-center justify-center">
<div class="bg-white rounded-xl max-w-4xl w-full mx-4 max-h-[90vh] overflow-auto">
@@ -859,6 +893,7 @@
document.getElementById('admin-categories-table').innerHTML = categories.map(c => {
const isBuiltin = builtinCategories.includes(c.id);
const subcatCount = (c.subcategories || []).length;
const fieldsCount = (c.fields || []).length;
return `
<tr class="border-b hover:bg-gray-50 ${c.visible === false ? 'bg-gray-100 opacity-60' : ''} ${isBuiltin ? 'bg-indigo-50' : ''}">
<td class="px-4 py-3"><div class="w-10 h-10 rounded-lg ${colorMap[c.color] || 'bg-gray-100 text-gray-600'} flex items-center justify-center"><i class="${c.icon} text-xl"></i></div></td>
@@ -867,11 +902,14 @@
<td class="px-4 py-3 text-sm">
${isBuiltin ? '<span class="px-2 py-1 bg-indigo-100 text-indigo-600 rounded text-xs">内置</span>' : '<span class="text-gray-500">自定义</span>'}
</td>
<td class="px-4 py-3 text-sm">
${fieldsCount > 0 ? `<span class="px-2 py-1 bg-blue-100 text-blue-600 rounded text-xs">${fieldsCount} 个</span>` : '<span class="text-gray-400">无</span>'}
</td>
<td class="px-4 py-3 text-sm">
${subcatCount > 0 ? `<span class="px-2 py-1 bg-green-100 text-green-600 rounded text-xs">${subcatCount} 个</span>` : '<span class="text-gray-400">无</span>'}
</td>
<td class="px-4 py-3 text-center">
<button onclick="editItem('category', '${c.id}')" class="text-blue-600 hover:text-blue-800 mr-2" title="编辑子类别"><i class="ri-edit-line"></i></button>
<button onclick="editItem('category', '${c.id}')" class="text-blue-600 hover:text-blue-800 mr-2" title="编辑"><i class="ri-edit-line"></i></button>
${!isBuiltin ? `<button onclick="deleteItem('category', '${c.id}')" class="text-red-600 hover:text-red-800" title="删除"><i class="ri-delete-bin-line"></i></button>` : '<span class="text-gray-300 cursor-not-allowed"><i class="ri-delete-bin-line"></i></span>'}
</td>
</tr>
@@ -1136,6 +1174,12 @@
return;
}
}
else if (key === 'fields') {
// 解析参数字段JSON
try { data[key] = JSON.parse(value); } catch {
data[key] = [];
}
}
else data[key] = value;
}
});
@@ -1317,9 +1361,12 @@
// 内置类别只显示子类别管理
if (isBuiltin) {
const fields = data.fields || [];
window.currentEditingFields = JSON.parse(JSON.stringify(fields));
return `<form id="itemForm" class="space-y-4">
<div class="bg-indigo-50 rounded-lg p-4 mb-4">
<p class="text-sm text-indigo-700"><i class="ri-information-line mr-1"></i>内置分类的基础信息不可修改,只可编辑子类别配置。</p>
<p class="text-sm text-indigo-700"><i class="ri-information-line mr-1"></i>内置分类的基础信息不可修改,只可编辑参数字段和子类别配置。</p>
</div>
<div class="grid grid-cols-2 gap-4 bg-gray-50 p-4 rounded-lg">
<div><label class="text-sm text-gray-500 mb-1 block">ID</label><div class="text-gray-700 font-mono">${data.id}</div></div>
@@ -1335,6 +1382,20 @@
<input type="hidden" name="visible" value="${data.visible !== false ? 'true' : 'false'}">
<input type="hidden" name="description" value="${data.description || ''}">
<!-- 参数字段管理 -->
<div class="border-t pt-4">
<div class="flex justify-between items-center mb-3">
<label class="text-sm font-medium text-gray-700"><i class="ri-list-settings-line mr-1"></i>参数字段管理</label>
<button onclick="openFieldAddModal('category')" class="px-3 py-1.5 bg-indigo-600 text-white rounded-lg text-sm hover:bg-indigo-700">
<i class="ri-add-line mr-1"></i>添加字段
</button>
</div>
<div id="fieldsList" class="space-y-2">
${renderFieldsList(fields)}
</div>
<input type="hidden" name="fields" id="fieldsHidden" value='${JSON.stringify(fields)}'>
</div>
<!-- 子类别管理 -->
<div class="border-t pt-4">
<div class="flex justify-between items-center mb-3">
@@ -1353,6 +1414,9 @@
// 自定义类别完整编辑表单
const autoId = data.id || generateId();
const fields = data.fields || [];
window.currentEditingFields = JSON.parse(JSON.stringify(fields));
return `<form id="itemForm" class="space-y-4">
<div class="grid grid-cols-2 gap-4">
<div><label class="text-sm text-gray-600 mb-1 block">ID</label><input type="text" name="id" value="${autoId}" readonly class="w-full px-3 py-2 border rounded-lg bg-gray-100 text-gray-500 font-mono text-xs"><p class="text-xs text-gray-400 mt-1">自动生成,无需填写</p></div>
@@ -1374,6 +1438,20 @@
</div>
<div><label class="text-sm text-gray-600 mb-1 block">描述</label><textarea name="description" rows="2" class="w-full px-3 py-2 border rounded-lg" placeholder="分类描述">${data.description || ''}</textarea></div>
<!-- 参数字段管理 -->
<div class="border-t pt-4">
<div class="flex justify-between items-center mb-3">
<label class="text-sm font-medium text-gray-700"><i class="ri-list-settings-line mr-1"></i>参数字段管理(基础字段,所有子类别共享)</label>
<button onclick="openFieldAddModal('category')" class="px-3 py-1.5 bg-indigo-600 text-white rounded-lg text-sm hover:bg-indigo-700">
<i class="ri-add-line mr-1"></i>添加字段
</button>
</div>
<div id="fieldsList" class="space-y-2">
${renderFieldsList(fields)}
</div>
<input type="hidden" name="fields" id="fieldsHidden" value='${JSON.stringify(fields)}'>
</div>
<!-- 子类别管理 -->
<div class="border-t pt-4">
<div class="flex justify-between items-center mb-3">
@@ -1404,7 +1482,7 @@
</div>
<div>
<div class="font-medium text-gray-800">${sub.name}</div>
<div class="text-xs text-gray-500">ID: ${sub.id} | 特性: ${(sub.key_features || []).join(', ')}</div>
<div class="text-xs text-gray-500">ID: ${sub.id} | 特性: ${(sub.key_features || []).join(', ')} | 额外字段: ${(sub.extra_fields || []).length}</div>
</div>
</div>
<div class="flex gap-2 opacity-0 group-hover:opacity-100 transition">
@@ -1419,6 +1497,124 @@
`).join('');
}
// ============ 参数字段管理 ============
let currentFieldTarget = ''; // 'category' 或 'subcategory'
let currentFieldIndex = -1;
// 渲染参数字段列表
function renderFieldsList(fields) {
if (!fields || fields.length === 0) {
return '<div class="text-gray-400 text-sm py-4 text-center bg-gray-50 rounded-lg">暂无参数字段,点击上方按钮添加</div>';
}
const typeLabels = {text: '文本', number: '数字', date: '日期', boolean: '布尔', json: 'JSON', url: 'URL'};
return fields.map((field, index) => `
<div class="bg-gray-50 rounded-lg p-3 flex justify-between items-center group hover:bg-gray-100">
<div class="flex items-center gap-3">
<div class="w-8 h-8 rounded-lg bg-indigo-100 flex items-center justify-center font-mono text-xs text-indigo-600">
${field.key ? field.key.substring(0, 4) : '-'}
</div>
<div>
<div class="font-medium text-gray-800">${field.label || field.key}</div>
<div class="text-xs text-gray-500">
字段: ${field.key} | 类型: ${typeLabels[field.type] || '文本'} |
${field.required ? '<span class="text-red-500">必填</span>' : '可选'}
${field.description ? ` | ${field.description}` : ''}
</div>
</div>
</div>
<div class="flex gap-2 opacity-0 group-hover:opacity-100 transition">
<button onclick="editField(${index})" class="px-2 py-1 text-blue-600 hover:bg-blue-50 rounded text-sm">
<i class="ri-edit-line"></i>
</button>
<button onclick="deleteField(${index})" class="px-2 py-1 text-red-600 hover:bg-red-50 rounded text-sm">
<i class="ri-delete-bin-line"></i>
</button>
</div>
</div>
`).join('');
}
// 打开添加字段弹框
function openFieldAddModal(target) {
currentFieldTarget = target;
currentFieldIndex = -1;
document.getElementById('fieldModalTitle').textContent = '添加参数字段';
document.getElementById('field_key').value = '';
document.getElementById('field_label').value = '';
document.getElementById('field_type').value = 'text';
document.getElementById('field_desc').value = '';
document.getElementById('field_required').checked = false;
document.getElementById('fieldModal').classList.remove('hidden');
}
// 编辑字段
function editField(index) {
currentFieldTarget = 'category';
currentFieldIndex = index;
const field = window.currentEditingFields[index];
document.getElementById('fieldModalTitle').textContent = '编辑参数字段';
document.getElementById('field_key').value = field.key || '';
document.getElementById('field_label').value = field.label || '';
document.getElementById('field_type').value = field.type || 'text';
document.getElementById('field_desc').value = field.description || '';
document.getElementById('field_required').checked = field.required || false;
document.getElementById('fieldModal').classList.remove('hidden');
}
// 删除字段
function deleteField(index) {
if (!confirm('确定删除此字段?')) return;
window.currentEditingFields.splice(index, 1);
document.getElementById('fieldsList').innerHTML = renderFieldsList(window.currentEditingFields);
document.getElementById('fieldsHidden').value = JSON.stringify(window.currentEditingFields);
}
// 关闭字段弹框
function closeFieldModal() {
document.getElementById('fieldModal').classList.add('hidden');
}
// 保存字段
function saveField() {
const key = document.getElementById('field_key').value.trim();
const label = document.getElementById('field_label').value.trim();
const type = document.getElementById('field_type').value;
const description = document.getElementById('field_desc').value.trim();
const required = document.getElementById('field_required').checked;
if (!key || !label) {
alert('字段名和显示名不能为空');
return;
}
const field = { key, label, type, description, required };
if (currentFieldTarget === 'subcategory') {
// 子类别额外字段
if (currentFieldIndex === -1) {
window.currentEditingSubcategoryFields.push(field);
} else {
window.currentEditingSubcategoryFields[currentFieldIndex] = field;
}
document.getElementById('subcategoryFieldsList').innerHTML = renderFieldsList(window.currentEditingSubcategoryFields);
} else {
// 类别基础字段
if (currentFieldIndex === -1) {
window.currentEditingFields.push(field);
} else {
window.currentEditingFields[currentFieldIndex] = field;
}
document.getElementById('fieldsList').innerHTML = renderFieldsList(window.currentEditingFields);
document.getElementById('fieldsHidden').value = JSON.stringify(window.currentEditingFields);
}
closeFieldModal();
}
// 打开子类别添加弹框
function openSubcategoryAddModal() {
document.getElementById('subcategoryModalTitle').textContent = '添加子类别';
@@ -1450,6 +1646,8 @@
const featureLabels = data.feature_labels || {};
const featureLabelsStr = Object.entries(featureLabels).map(([k, v]) => `${k}:${v}`).join(', ');
const autoSubId = data.id || generateId();
const extraFields = data.extra_fields || [];
window.currentEditingSubcategoryFields = JSON.parse(JSON.stringify(extraFields));
return `<div class="space-y-4">
<div class="grid grid-cols-2 gap-4">
@@ -1458,18 +1656,44 @@
<div><label class="text-sm text-gray-600 mb-1 block">图标</label><input type="text" id="sub_icon" value="${data.icon || 'ri-folder-line'}" class="w-full px-3 py-2 border rounded-lg" placeholder="ri-folder-line"></div>
</div>
<div>
<label class="text-sm text-gray-600 mb-1 block">关键特性字段</label>
<label class="text-sm text-gray-600 mb-1 block">关键特性字段(表格显示的关键列)</label>
<input type="text" id="sub_key_features" value="${keyFeatures}" class="w-full px-3 py-2 border rounded-lg" placeholder="context_length, mmlu, input_price">
<p class="text-xs text-gray-500 mt-1">逗号分隔,context_length, mmlu, input_price</p>
<p class="text-xs text-gray-500 mt-1">逗号分隔,选择要重点显示的字段</p>
</div>
<div>
<label class="text-sm text-gray-600 mb-1 block">特性标签(显示名)</label>
<label class="text-sm text-gray-600 mb-1 block">特性标签(显示名映射</label>
<input type="text" id="sub_feature_labels" value="${featureLabelsStr}" class="w-full px-3 py-2 border rounded-lg" placeholder="context_length:上下文, mmlu:MMLU">
<p class="text-xs text-gray-500 mt-1">格式:字段名:显示名,逗号分隔</p>
</div>
<!-- 额外参数字段(子类别特有) -->
<div class="border-t pt-4">
<div class="flex justify-between items-center mb-3">
<label class="text-sm font-medium text-gray-700"><i class="ri-list-settings-line mr-1"></i>额外参数字段(子类别特有,继承父类别字段)</label>
<button onclick="openSubcategoryFieldAddModal()" class="px-3 py-1.5 bg-indigo-600 text-white rounded-lg text-sm hover:bg-indigo-700">
<i class="ri-add-line mr-1"></i>添加字段
</button>
</div>
<div id="subcategoryFieldsList" class="space-y-2">
${renderFieldsList(extraFields)}
</div>
</div>
</div>`;
}
// 子类别字段添加弹框
function openSubcategoryFieldAddModal() {
currentFieldTarget = 'subcategory';
currentFieldIndex = -1;
document.getElementById('fieldModalTitle').textContent = '添加额外参数字段';
document.getElementById('field_key').value = '';
document.getElementById('field_label').value = '';
document.getElementById('field_type').value = 'text';
document.getElementById('field_desc').value = '';
document.getElementById('field_required').checked = false;
document.getElementById('fieldModal').classList.remove('hidden');
}
// 保存子类别
function saveSubcategory() {
const id = document.getElementById('sub_id').value.trim();
@@ -1483,8 +1707,6 @@
return;
}
// ID自动生成无需校验
// 解析 key_features
const key_features = keyFeaturesStr ? keyFeaturesStr.split(',').map(s => s.trim()).filter(s => s) : [];
@@ -1504,22 +1726,19 @@
name,
icon,
key_features,
feature_labels
feature_labels,
extra_fields: window.currentEditingSubcategoryFields || []
};
if (window.editingSubcategoryIndex === -1) {
// 添加新子类别
window.currentEditingSubcategories.push(subcategory);
} else {
// 编辑现有子类别
window.currentEditingSubcategories[window.editingSubcategoryIndex] = subcategory;
}
// 更新显示
document.getElementById('subcategoriesList').innerHTML = renderSubcategoriesList(window.currentEditingSubcategories);
document.getElementById('subcategoriesHidden').value = JSON.stringify(window.currentEditingSubcategories);
// 关闭弹框
closeSubcategoryModal();
}
@@ -1672,6 +1891,7 @@
document.getElementById('rawDataModal').addEventListener('click', function(e) { if (e.target === this) closeRawDataModal(); });
document.getElementById('subcategoryModal').addEventListener('click', function(e) { if (e.target === this) closeSubcategoryModal(); });
document.getElementById('smartUpdateModal').addEventListener('click', function(e) { if (e.target === this) closeSmartUpdateModal(); });
document.getElementById('fieldModal').addEventListener('click', function(e) { if (e.target === this) closeFieldModal(); });
// ============ 智能添加功能 ============