Files
param-hub-python/templates/admin.html

2054 lines
129 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.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ParamHub - 参数百科</title>
<link rel="icon" type="image/svg+xml" href="/static/favicon.svg">
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://cdn.jsdelivr.net/npm/remixicon@3.5.0/fonts/remixicon.css" rel="stylesheet">
<style>
.sidebar-link { transition: all 0.2s; }
.sidebar-link:hover { background: #374151; }
.sidebar-link.active { background: #4f46e5; color: white; }
</style>
</head>
<body class="bg-gray-100 min-h-screen">
<!-- 侧边栏 -->
<aside class="fixed left-0 top-0 w-64 h-full bg-gray-800 text-white overflow-y-auto">
<div class="p-4 border-b border-gray-700">
<div class="flex items-center gap-2">
<i class="ri-dashboard-3-line text-2xl"></i>
<span class="text-xl font-bold">ParamHub</span>
</div>
<div class="text-sm text-gray-400 mt-1">后台管理</div>
</div>
<nav class="p-4">
<div class="space-y-1" id="sidebarNav">
<!-- 动态生成 -->
</div>
<div class="mt-8 pt-4 border-t border-gray-700">
<a href="/" target="_blank" class="flex items-center gap-2 px-3 py-2 rounded-lg hover:bg-gray-700 text-gray-400">
<i class="ri-external-link-line"></i>
<span>访问前台</span>
</a>
</div>
</nav>
</aside>
<!-- 主内容 -->
<main class="ml-64 p-8">
<!-- 概览 -->
<section id="section-overview">
<h1 class="text-2xl font-bold text-gray-800 mb-6">管理概览</h1>
<div class="grid grid-cols-5 gap-4 mb-8" id="statsCards"><div class="text-center text-gray-400 py-4">加载中...</div></div>
<div class="bg-white rounded-xl p-6 shadow-sm mb-8">
<h2 class="text-lg font-semibold text-gray-800 mb-4">快捷操作</h2>
<div class="grid grid-cols-5 gap-3" id="quickActions"><div class="text-gray-400">加载中...</div></div>
</div>
<div class="bg-white rounded-xl p-6 shadow-sm">
<h2 class="text-lg font-semibold text-gray-800 mb-4">最近添加的模型</h2>
<div id="recent-models" class="space-y-2">加载中...</div>
</div>
</section>
<!-- 网站配置 -->
<section id="section-config" class="hidden">
<h1 class="text-2xl font-bold text-gray-800 mb-6">网站配置</h1>
<div class="bg-white rounded-xl shadow-sm p-6">
<form id="configForm" class="space-y-6">
<!-- 网站基础配置 -->
<h3 class="text-lg font-semibold text-gray-800 border-b pb-2 mb-4"><i class="ri-global-line mr-2"></i>网站基础配置</h3>
<div class="grid grid-cols-2 gap-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">网站名称</label>
<input type="text" name="site_name" id="config_site_name" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="ParamHub">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">网站副标题</label>
<input type="text" name="site_subtitle" id="config_site_subtitle" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="参数百科">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">备案号</label>
<input type="text" name="icp_number" id="config_icp_number" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="如京ICP备12345678号">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">联系邮箱</label>
<input type="email" name="contact_email" id="config_contact_email" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="admin@example.com">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">GitHub地址</label>
<input type="url" name="github_url" id="config_github_url" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="https://github.com/...">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">版权年份</label>
<input type="text" name="copyright_year" id="config_copyright_year" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="2024">
</div>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">页脚文字</label>
<textarea name="footer_text" id="config_footer_text" rows="3" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="网站底部的版权信息等"></textarea>
</div>
<!-- 大模型接口配置 -->
<h3 class="text-lg font-semibold text-gray-800 border-b pb-2 mb-4 mt-8"><i class="ri-robot-line mr-2"></i>大模型接口配置(用于智能解析)</h3>
<div class="bg-blue-50 rounded-lg p-4 mb-4">
<p class="text-sm text-blue-700"><i class="ri-information-line mr-1"></i>配置用于智能解析产品参数的大模型API接口。文本解析使用普通模型图片解析使用视觉模型。</p>
</div>
<div class="grid grid-cols-2 gap-6">
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">API地址</label>
<input type="url" name="llm_base_url" id="config_llm_base_url" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="http://192.168.2.17:19007/v1">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">API Key</label>
<input type="text" name="llm_api_key" id="config_llm_api_key" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="留空则不验证">
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">文本解析模型</label>
<input type="text" name="llm_model" id="config_llm_model" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="auto">
<p class="text-xs text-gray-500 mt-1">用于解析文本数据,如 deepseek-v3、qwen3.5 等</p>
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">图片解析模型(视觉模型)</label>
<input type="text" name="llm_vision_model" id="config_llm_vision_model" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" placeholder="gpt-4-vision-preview">
<p class="text-xs text-gray-500 mt-1">用于解析图片,如 Qwen/Qwen2-VL-72B-Instruct、gpt-4-vision-preview 等</p>
</div>
</div>
<div class="flex justify-end gap-4">
<button type="button" onclick="loadSiteConfig()" class="px-4 py-2 bg-gray-200 text-gray-700 rounded-lg hover:bg-gray-300"><i class="ri-refresh-line mr-1"></i>重置</button>
<button type="button" onclick="saveSiteConfig()" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"><i class="ri-save-line mr-1"></i>保存配置</button>
</div>
</form>
</div>
</section>
<!-- 分类管理 -->
<section id="section-categories" class="hidden">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800">分类管理</h1>
<button onclick="openAddModal('category')" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"><i class="ri-add-line mr-2"></i>添加分类</button>
</div>
<div class="bg-blue-50 rounded-lg p-4 mb-4">
<p class="text-sm text-blue-700"><i class="ri-information-line mr-1"></i>内置分类AI模型、GPU、CPU的子类别配置可在此编辑其数据管理入口在左侧导航栏的独立页面。</p>
</div>
<div class="bg-white rounded-xl shadow-sm overflow-hidden">
<table class="w-full">
<thead class="bg-gray-50 border-b">
<tr>
<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">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-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>
</table>
</div>
</section>
<!-- 动态分类数据管理容器 -->
<section id="section-dynamic" class="hidden">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800" id="dynamic-title">数据管理</h1>
<div class="flex gap-2">
<button onclick="openSmartAddModal('dynamic')" class="px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700"><i class="ri-magic-line mr-2"></i>智能添加</button>
<button onclick="openAddModal('dynamic')" class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"><i class="ri-add-line mr-2"></i>手动添加</button>
</div>
</div>
<div class="bg-white rounded-xl shadow-sm overflow-hidden">
<table class="w-full"><tbody id="admin-dynamic-table"><tr><td class="text-center text-gray-400 py-8">加载中...</td></tr></tbody></table>
</div>
</section>
<!-- 大模型管理 -->
<section id="section-models" class="hidden">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800">大模型管理</h1>
<div class="flex gap-2">
<button onclick="openSmartAddModal('model')" class="px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700"><i class="ri-magic-line mr-2"></i>智能添加</button>
<button onclick="openAddModal('model')" class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700"><i class="ri-add-line mr-2"></i>手动添加</button>
</div>
</div>
<div class="bg-white rounded-xl shadow-sm overflow-x-auto">
<table class="w-full min-w-[1200px]">
<thead class="bg-gray-50 border-b">
<tr>
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">置顶</th>
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">名称</th>
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">厂商</th>
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">参数量</th>
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">上下文</th>
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">类型</th>
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">发布日期</th>
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">热度</th>
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">创建时间</th>
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">更新时间</th>
<th class="px-3 py-3 text-center text-sm font-medium text-gray-600">显示</th>
<th class="px-3 py-3 text-center text-sm font-medium text-gray-600">操作</th>
</tr>
</thead>
<tbody id="admin-models-table"><tr><td colspan="12" class="text-center text-gray-400 py-8">加载中...</td></tr></tbody>
</table>
</div>
</section>
<!-- GPU管理 -->
<section id="section-gpus" class="hidden">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800">GPU管理</h1>
<div class="flex gap-2">
<button onclick="openSmartAddModal('gpu')" class="px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700"><i class="ri-magic-line mr-2"></i>智能添加</button>
<button onclick="openAddModal('gpu')" class="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700"><i class="ri-add-line mr-2"></i>手动添加</button>
</div>
</div>
<div class="bg-white rounded-xl shadow-sm overflow-x-auto">
<table class="w-full min-w-[1200px]">
<thead class="bg-gray-50 border-b">
<tr>
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">置顶</th>
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">名称</th>
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">厂商</th>
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">显存</th>
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">架构</th>
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">价格</th>
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">发布日期</th>
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">热度</th>
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">创建时间</th>
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">更新时间</th>
<th class="px-3 py-3 text-center text-sm font-medium text-gray-600">显示</th>
<th class="px-3 py-3 text-center text-sm font-medium text-gray-600">操作</th>
</tr>
</thead>
<tbody id="admin-gpus-table"><tr><td colspan="12" class="text-center text-gray-400 py-8">加载中...</td></tr></tbody>
</table>
</div>
</section>
<!-- CPU管理 -->
<section id="section-cpus" class="hidden">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800">CPU管理</h1>
<div class="flex gap-2">
<button onclick="openSmartAddModal('cpu')" class="px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700"><i class="ri-magic-line mr-2"></i>智能添加</button>
<button onclick="openAddModal('cpu')" class="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700"><i class="ri-add-line mr-2"></i>手动添加</button>
</div>
</div>
<div class="bg-white rounded-xl shadow-sm overflow-x-auto">
<table class="w-full min-w-[1200px]">
<thead class="bg-gray-50 border-b">
<tr>
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">置顶</th>
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">名称</th>
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">厂商</th>
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">核心/线程</th>
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">主频</th>
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">价格</th>
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">发布日期</th>
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">热度</th>
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">创建时间</th>
<th class="px-3 py-3 text-left text-sm font-medium text-gray-600">更新时间</th>
<th class="px-3 py-3 text-center text-sm font-medium text-gray-600">显示</th>
<th class="px-3 py-3 text-center text-sm font-medium text-gray-600">操作</th>
</tr>
</thead>
<tbody id="admin-cpus-table"><tr><td colspan="12" class="text-center text-gray-400 py-8">加载中...</td></tr></tbody>
</table>
</div>
</section>
<!-- 知识库管理 -->
<section id="section-knowledge" class="hidden">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-bold text-gray-800">知识库管理</h1>
<button onclick="openAddModal('knowledge')" class="px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700"><i class="ri-add-line mr-2"></i>添加知识</button>
</div>
<div class="bg-white rounded-xl shadow-sm p-4 mb-4">
<div class="flex items-center gap-2">
<span class="text-sm text-gray-600">筛选分类:</span>
<button onclick="filterKnowledge('')" class="px-3 py-1 rounded text-sm bg-gray-100 text-gray-600 hover:bg-gray-200">全部</button>
<div id="category-filters" class="flex gap-2"></div>
</div>
</div>
<div class="bg-white rounded-xl shadow-sm overflow-hidden">
<table class="w-full">
<thead class="bg-gray-50 border-b">
<tr>
<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-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>
<th class="px-4 py-3 text-center text-sm font-medium text-gray-600">操作</th>
</tr>
</thead>
<tbody id="admin-knowledge-table"><tr><td colspan="7" class="text-center text-gray-400 py-8">加载中...</td></tr></tbody>
</table>
</div>
</section>
</main>
<!-- 编辑弹窗 -->
<div id="editModal" class="fixed inset-0 bg-black/50 z-50 hidden flex items-center justify-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="modalTitle">添加/编辑</h2>
<button onclick="closeModal()" class="text-gray-400 hover:text-gray-600"><i class="ri-close-line text-2xl"></i></button>
</div>
<div id="modalContent" class="p-6"></div>
<div class="p-6 border-t flex justify-end gap-4 sticky bottom-0 bg-white">
<button onclick="closeModal()" class="px-4 py-2 bg-gray-200 text-gray-600 rounded-lg hover:bg-gray-300">取消</button>
<button onclick="openSmartUpdateModal()" id="smartUpdateBtn" class="px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700 hidden"><i class="ri-magic-line mr-1"></i>智能补充</button>
<button onclick="saveItem()" class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700">保存</button>
</div>
</div>
</div>
<!-- 智能添加弹窗 -->
<div id="smartAddModal" 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">
<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"><i class="ri-magic-line mr-2 text-orange-600"></i>智能添加(支持多图解析)</h2>
<button onclick="closeSmartAddModal()" class="text-gray-400 hover:text-gray-600"><i class="ri-close-line text-2xl"></i></button>
</div>
<div class="p-6">
<div class="mb-6">
<p class="text-sm text-gray-500 mb-3">上传产品图片AI将自动识别并解析参数。支持一次上传多张图片可识别多个产品。</p>
<div class="flex flex-wrap gap-3 mb-3" id="smartImagePreviewArea">
<!-- 图片预览区 -->
</div>
<div class="flex gap-3">
<input type="file" id="smartImageInput" accept="image/*" multiple class="hidden" onchange="handleSmartImageUpload(event)">
<button onclick="document.getElementById('smartImageInput').click()" class="px-4 py-2 bg-orange-100 text-orange-600 rounded-lg hover:bg-orange-200 text-sm">
<i class="ri-image-add-line mr-1"></i>选择图片(支持多选)
</button>
<button onclick="pasteSmartImageFromClipboard()" class="px-4 py-2 bg-gray-100 text-gray-600 rounded-lg hover:bg-gray-200 text-sm">
<i class="ri-clipboard-line mr-1"></i>粘贴图片
</button>
<button onclick="clearSmartImages()" class="px-4 py-2 bg-gray-100 text-gray-600 rounded-lg hover:bg-gray-200 text-sm">
<i class="ri-delete-bin-line mr-1"></i>清空图片
</button>
</div>
<div class="mt-3 text-xs text-gray-400">
<i class="ri-information-line mr-1"></i>
已选择 <span id="smartImageCount">0</span> 张图片
</div>
</div>
<div class="border-t pt-4">
<label class="text-sm text-gray-600 mb-2 block">补充文本(可选)</label>
<textarea id="smartAddText" rows="4" class="w-full p-4 border border-gray-200 rounded-lg focus:outline-none focus:border-orange-400 text-gray-700" placeholder="可粘贴补充信息文本,与图片一起解析..."></textarea>
</div>
<div id="smartAddPreview" class="mt-4 hidden">
<h3 class="text-sm font-semibold text-gray-700 mb-2"><i class="ri-checkbox-circle-line text-green-600 mr-1"></i>解析结果预览:</h3>
<div class="bg-gray-50 rounded-lg p-4 text-sm text-gray-600" id="smartAddResult">
<!-- 解析结果显示 -->
</div>
<div class="mt-3 flex gap-2" id="smartAddActions">
<!-- 操作按钮 -->
</div>
</div>
</div>
<div class="p-6 border-t flex justify-end gap-4 sticky bottom-0 bg-white">
<button onclick="closeSmartAddModal()" class="px-4 py-2 bg-gray-200 text-gray-600 rounded-lg hover:bg-gray-300">取消</button>
<button onclick="previewSmartParse()" id="previewBtn" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"><i class="ri-eye-line mr-1"></i>预览解析结果</button>
<button onclick="smartAddSubmit()" id="smartAddBtn" class="px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700"><i class="ri-magic-line mr-1"></i>解析并添加</button>
</div>
</div>
</div>
<!-- 原始数据查看弹窗 -->
<div id="rawDataModal" class="fixed inset-0 bg-black/50 z-50 hidden flex items-center justify-center">
<div class="bg-white rounded-xl max-w-3xl 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"><i class="ri-file-text-line mr-2"></i>原始数据</h2>
<button onclick="closeRawDataModal()" class="text-gray-400 hover:text-gray-600"><i class="ri-close-line text-2xl"></i></button>
</div>
<div id="rawDataContent" class="p-6">
<div class="bg-gray-50 rounded-lg p-4 text-sm text-gray-700 whitespace-pre-wrap" id="rawDataText"></div>
</div>
</div>
</div>
<!-- 子类别编辑弹框 -->
<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">
<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">
<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="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">
<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"><i class="ri-magic-line mr-2 text-orange-600"></i>智能补充参数</h2>
<button onclick="closeSmartUpdateModal()" class="text-gray-400 hover:text-gray-600"><i class="ri-close-line text-2xl"></i></button>
</div>
<div class="p-6">
<div class="bg-blue-50 rounded-lg p-4 mb-4">
<p class="text-sm text-blue-700"><i class="ri-information-line mr-1"></i>上传图片或输入文本AI将识别参数并补充到现有数据中。只会填充缺失的字段不会覆盖已有值。</p>
</div>
<div class="mb-6">
<p class="text-sm text-gray-500 mb-3">上传产品图片AI将自动识别并解析参数</p>
<div class="flex flex-wrap gap-3 mb-3" id="smartUpdateImagePreviewArea">
<!-- 图片预览区 -->
</div>
<div class="flex gap-3">
<input type="file" id="smartUpdateImageInput" accept="image/*" multiple class="hidden" onchange="handleSmartUpdateImageUpload(event)">
<button onclick="document.getElementById('smartUpdateImageInput').click()" class="px-4 py-2 bg-orange-100 text-orange-600 rounded-lg hover:bg-orange-200 text-sm">
<i class="ri-image-add-line mr-1"></i>选择图片(支持多选)
</button>
<button onclick="pasteSmartUpdateImageFromClipboard()" class="px-4 py-2 bg-gray-100 text-gray-600 rounded-lg hover:bg-gray-200 text-sm">
<i class="ri-clipboard-line mr-1"></i>粘贴图片
</button>
<button onclick="clearSmartUpdateImages()" class="px-4 py-2 bg-gray-100 text-gray-600 rounded-lg hover:bg-gray-200 text-sm">
<i class="ri-delete-bin-line mr-1"></i>清空图片
</button>
</div>
<div class="mt-3 text-xs text-gray-400">
<i class="ri-information-line mr-1"></i>
已选择 <span id="smartUpdateImageCount">0</span> 张图片
</div>
</div>
<div class="border-t pt-4">
<label class="text-sm text-gray-600 mb-2 block">补充文本(可选)</label>
<textarea id="smartUpdateText" rows="4" class="w-full p-4 border border-gray-200 rounded-lg focus:outline-none focus:border-orange-400 text-gray-700" placeholder="可粘贴补充信息文本,如产品规格表、参数说明等..."></textarea>
</div>
<div id="smartUpdatePreview" class="mt-4 hidden">
<h3 class="text-sm font-semibold text-gray-700 mb-2"><i class="ri-checkbox-circle-line text-green-600 mr-1"></i>解析结果:</h3>
<div class="bg-gray-50 rounded-lg p-4 text-sm text-gray-600" id="smartUpdateResult">
<!-- 解析结果显示 -->
</div>
</div>
</div>
<div class="p-6 border-t flex justify-end gap-4 sticky bottom-0 bg-white">
<button onclick="closeSmartUpdateModal()" class="px-4 py-2 bg-gray-200 text-gray-600 rounded-lg hover:bg-gray-300">取消</button>
<button onclick="smartUpdateSubmit()" id="smartUpdateSubmitBtn" class="px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700"><i class="ri-magic-line mr-1"></i>解析并补充</button>
</div>
</div>
</div>
<script>
let currentType = '';
let currentId = '';
let currentData = {};
let categories = [];
let currentFilter = '';
let dynamicCategoryId = '';
const colorMap = {
blue: 'bg-blue-100 text-blue-600',
green: 'bg-green-100 text-green-600',
purple: 'bg-purple-100 text-purple-600',
orange: 'bg-orange-100 text-orange-600',
teal: 'bg-teal-100 text-teal-600',
red: 'bg-red-100 text-red-600'
};
// 价格格式化函数
function formatPrice(item) {
// 支持多种价格格式
const currency = item.currency || 'USD';
const symbols = { USD: '$', CNY: '¥', EUR: '€', JPY: '¥', GBP: '£' };
const symbol = symbols[currency] || currency;
// 价格区间支持
const minPrice = item.min_price || item.price_usd_min || item.price_min;
const maxPrice = item.max_price || item.price_usd_max || item.price_max;
const singlePrice = item.price_usd || item.price_cny || item.price;
// 单位处理
const unit = item.price_unit || '';
if (minPrice && maxPrice) {
// 价格区间
return `${symbol}${formatNumber(minPrice)}-${formatNumber(maxPrice)}${unit ? ' ' + unit : ''}`;
} else if (singlePrice) {
return `${symbol}${formatNumber(singlePrice)}${unit ? ' ' + unit : ''}`;
} else {
return '-';
}
}
// 数字格式化
function formatNumber(num) {
if (!num) return '0';
if (num >= 10000) {
return (num / 10000).toFixed(1) + '万';
}
if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return num.toLocaleString();
}
// 日期格式化(短格式)
function formatDateShort(dateStr) {
if (!dateStr) return '-';
// 2026-04-20 18:30:00 -> 04-20 18:30
const match = dateStr.match(/\d{4}-(\d{2}-\d{2})\s*(\d{2}:\d{2})?/);
if (match) {
return match[1] + (match[2] ? ' ' + match[2] : '');
}
return dateStr;
}
// 初始化
async function init() {
await loadCategories();
renderSidebar();
loadOverview();
}
// 加载分类
async function loadCategories() {
const res = await fetch('/api/categories');
categories = await res.json();
categories.sort((a, b) => (a.order || 0) - (b.order || 0));
}
// 渲染侧边栏
function renderSidebar() {
const fixedItems = [
{id: 'overview', name: '概览', icon: 'ri-home-4-line'},
{id: 'config', name: '网站配置', icon: 'ri-settings-3-line'},
{id: 'categories', name: '分类管理', icon: 'ri-folder-line'},
];
// 内置分类映射
const builtinMap = {
'ai-models': {id: 'models', name: '大模型管理', icon: 'ri-robot-line'},
'gpus': {id: 'gpus', name: 'GPU管理', icon: 'ri-cpu-line'},
'cpus': {id: 'cpus', name: 'CPU管理', icon: 'ri-cpu-line'}
};
let html = fixedItems.map(item => `
<a href="#${item.id}" onclick="showSection('${item.id}')" class="sidebar-link flex items-center gap-2 px-3 py-2 rounded-lg ${item.id === 'overview' ? 'active text-white' : 'text-gray-300'}" data-section="${item.id}">
<i class="${item.icon}"></i>
<span>${item.name}</span>
</a>
`).join('');
// 添加每个分类的管理入口
categories.forEach(cat => {
const builtin = builtinMap[cat.id];
if (builtin) {
html += `
<a href="#${builtin.id}" onclick="showSection('${builtin.id}')" class="sidebar-link flex items-center gap-2 px-3 py-2 rounded-lg text-gray-300" data-section="${builtin.id}">
<i class="${builtin.icon}"></i>
<span>${builtin.name}</span>
</a>
`;
} else {
// 动态分类
html += `
<a href="#cat-${cat.id}" onclick="showDynamicCategory('${cat.id}')" class="sidebar-link flex items-center gap-2 px-3 py-2 rounded-lg text-gray-300" data-section="cat-${cat.id}">
<i class="${cat.icon}"></i>
<span>${cat.name}管理</span>
</a>
`;
}
});
// 知识库管理
html += `
<a href="#knowledge" onclick="showSection('knowledge')" class="sidebar-link flex items-center gap-2 px-3 py-2 rounded-lg text-gray-300" data-section="knowledge">
<i class="ri-book-open-line"></i>
<span>知识库管理</span>
</a>
`;
document.getElementById('sidebarNav').innerHTML = html;
}
// 显示动态分类数据
async function showDynamicCategory(categoryId) {
dynamicCategoryId = categoryId;
const cat = categories.find(c => c.id === categoryId);
document.querySelectorAll('section').forEach(s => s.classList.add('hidden'));
document.getElementById('section-dynamic').classList.remove('hidden');
document.querySelectorAll('.sidebar-link').forEach(a => {
a.classList.remove('active', 'text-white');
a.classList.add('text-gray-300');
});
const activeLink = document.querySelector(`.sidebar-link[data-section="cat-${categoryId}"]`);
if (activeLink) {
activeLink.classList.add('active', 'text-white');
activeLink.classList.remove('text-gray-300');
}
document.getElementById('dynamic-title').textContent = cat.name + '管理';
// 加载该分类的数据(后台显示全部,包括隐藏的)
const res = await fetch(`/api/items/${categoryId}?all=1`);
const items = await res.json();
if (items.length === 0) {
document.getElementById('admin-dynamic-table').innerHTML = '<tr><td class="text-center text-gray-400 py-8">暂无数据,点击上方"添加数据"按钮添加</td></tr>';
} else {
const keys = Object.keys(items[0]).filter(k => !['id', 'created_at', 'updated_at', 'visible', 'raw_text'].includes(k));
let html = `<thead class="bg-gray-50 border-b"><tr>`;
keys.forEach(k => { html += `<th class="px-4 py-3 text-left text-sm font-medium text-gray-600">${k}</th>`; });
html += `<th class="px-4 py-3 text-center text-sm font-medium text-gray-600">显示</th>`;
html += `<th class="px-4 py-3 text-center text-sm font-medium text-gray-600">操作</th></tr></thead><tbody>`;
items.forEach(item => {
html += `<tr class="border-b hover:bg-gray-50 ${item.visible === false ? 'bg-gray-100 opacity-60' : ''}">`;
keys.forEach(k => { html += `<td class="px-4 py-3 text-gray-600">${item[k] || '-'}</td>`; });
html += `<td class="px-4 py-3 text-center">
<button onclick="toggleVisible('dynamic', '${item.id}')" class="${item.visible === false ? 'text-gray-400' : 'text-green-600'} hover:opacity-80" title="${item.visible === false ? '点击显示' : '点击隐藏'}">
<i class="${item.visible === false ? 'ri-eye-off-line' : 'ri-eye-line'}"></i>
</button>
${item.raw_text ? `<button onclick="showRawData('${item.id}', 'dynamic')" class="text-gray-400 hover:text-gray-600 ml-1" title="查看原始数据"><i class="ri-file-text-line"></i></button>` : ''}
</td>`;
html += `<td class="px-4 py-3 text-center">
<button onclick="editDynamicItem('${item.id}')" class="text-blue-600 hover:text-blue-800 mr-2"><i class="ri-edit-line"></i></button>
<button onclick="deleteDynamicItem('${item.id}')" class="text-red-600 hover:text-red-800"><i class="ri-delete-bin-line"></i></button>
</td></tr>`;
});
html += '</tbody>';
document.getElementById('admin-dynamic-table').innerHTML = html;
}
}
// 切换显示区域
function showSection(section) {
document.querySelectorAll('section').forEach(s => s.classList.add('hidden'));
document.getElementById('section-' + section).classList.remove('hidden');
document.querySelectorAll('.sidebar-link').forEach(a => {
a.classList.remove('active', 'text-white');
a.classList.add('text-gray-300');
});
const activeLink = document.querySelector(`.sidebar-link[data-section="${section}"]`);
if (activeLink) {
activeLink.classList.add('active', 'text-white');
activeLink.classList.remove('text-gray-300');
}
if (section === 'categories') loadAdminCategories();
if (section === 'config') loadSiteConfig();
if (section === 'models') loadAdminModels();
if (section === 'gpus') loadAdminGpus();
if (section === 'cpus') loadAdminCpus();
if (section === 'knowledge') loadAdminKnowledge();
}
// 加载网站配置
async function loadSiteConfig() {
const res = await fetch('/api/config');
const config = await res.json();
// 网站基础配置
document.getElementById('config_site_name').value = config.site_name || '';
document.getElementById('config_site_subtitle').value = config.site_subtitle || '';
document.getElementById('config_icp_number').value = config.icp_number || '';
document.getElementById('config_contact_email').value = config.contact_email || '';
document.getElementById('config_github_url').value = config.github_url || '';
document.getElementById('config_copyright_year').value = config.copyright_year || '';
document.getElementById('config_footer_text').value = config.footer_text || '';
// LLM配置
document.getElementById('config_llm_base_url').value = config.llm_base_url || 'http://192.168.2.17:19007/v1';
document.getElementById('config_llm_api_key').value = config.llm_api_key || '';
document.getElementById('config_llm_model').value = config.llm_model || 'auto';
document.getElementById('config_llm_vision_model').value = config.llm_vision_model || 'gpt-4-vision-preview';
}
// 保存网站配置
async function saveSiteConfig() {
const config = {
// 网站基础配置
site_name: document.getElementById('config_site_name').value,
site_subtitle: document.getElementById('config_site_subtitle').value,
icp_number: document.getElementById('config_icp_number').value,
contact_email: document.getElementById('config_contact_email').value,
github_url: document.getElementById('config_github_url').value,
copyright_year: document.getElementById('config_copyright_year').value,
footer_text: document.getElementById('config_footer_text').value,
// LLM配置
llm_base_url: document.getElementById('config_llm_base_url').value,
llm_api_key: document.getElementById('config_llm_api_key').value,
llm_model: document.getElementById('config_llm_model').value,
llm_vision_model: document.getElementById('config_llm_vision_model').value,
};
try {
const res = await fetch('/api/config', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
});
const data = await res.json();
alert('配置已保存!');
} catch (e) {
alert('保存失败: ' + e.message);
}
}
// 加载概览统计
async function loadOverview() {
const res = await fetch('/api/stats');
const data = await res.json();
const statsHtml = `
<div class="bg-white rounded-xl p-5 shadow-sm">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-lg bg-blue-100 flex items-center justify-center"><i class="ri-folder-line text-xl text-blue-600"></i></div>
<div><div class="text-2xl font-bold text-gray-800">${data.categories_count}</div><div class="text-xs text-gray-500">分类数量</div></div>
</div>
</div>
<div class="bg-white rounded-xl p-5 shadow-sm">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-lg bg-indigo-100 flex items-center justify-center"><i class="ri-robot-line text-xl text-indigo-600"></i></div>
<div><div class="text-2xl font-bold text-gray-800">${data.models_count}</div><div class="text-xs text-gray-500">模型数量</div></div>
</div>
</div>
<div class="bg-white rounded-xl p-5 shadow-sm">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-lg bg-green-100 flex items-center justify-center"><i class="ri-cpu-line text-xl text-green-600"></i></div>
<div><div class="text-2xl font-bold text-gray-800">${data.gpus_count}</div><div class="text-xs text-gray-500">GPU数量</div></div>
</div>
</div>
<div class="bg-white rounded-xl p-5 shadow-sm">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-lg bg-purple-100 flex items-center justify-center"><i class="ri-cpu-line text-xl text-purple-600"></i></div>
<div><div class="text-2xl font-bold text-gray-800">${data.cpus_count}</div><div class="text-xs text-gray-500">CPU数量</div></div>
</div>
</div>
<div class="bg-white rounded-xl p-5 shadow-sm">
<div class="flex items-center gap-3">
<div class="w-10 h-10 rounded-lg bg-teal-100 flex items-center justify-center"><i class="ri-book-open-line text-xl text-teal-600"></i></div>
<div><div class="text-2xl font-bold text-gray-800">${data.knowledge_count}</div><div class="text-xs text-gray-500">知识条目</div></div>
</div>
</div>
`;
document.getElementById('statsCards').innerHTML = statsHtml;
// 快捷操作
const actionsHtml = `
<button onclick="showSection('categories'); openAddModal('category')" class="px-4 py-3 bg-blue-50 text-blue-600 rounded-lg hover:bg-blue-100 transition text-sm"><i class="ri-add-line mr-1"></i>添加分类</button>
<button onclick="showSection('models'); openAddModal('model')" class="px-4 py-3 bg-indigo-50 text-indigo-600 rounded-lg hover:bg-indigo-100 transition text-sm"><i class="ri-add-line mr-1"></i>添加模型</button>
<button onclick="showSection('gpus'); openAddModal('gpu')" class="px-4 py-3 bg-green-50 text-green-600 rounded-lg hover:bg-green-100 transition text-sm"><i class="ri-add-line mr-1"></i>添加GPU</button>
<button onclick="showSection('cpus'); openAddModal('cpu')" class="px-4 py-3 bg-purple-50 text-purple-600 rounded-lg hover:bg-purple-100 transition text-sm"><i class="ri-add-line mr-1"></i>添加CPU</button>
<button onclick="showSection('knowledge'); openAddModal('knowledge')" class="px-4 py-3 bg-teal-50 text-teal-600 rounded-lg hover:bg-teal-100 transition text-sm"><i class="ri-add-line mr-1"></i>添加知识</button>
`;
document.getElementById('quickActions').innerHTML = actionsHtml;
const models = data.latest_models || [];
document.getElementById('recent-models').innerHTML = models.length > 0
? models.map(m => `<div class="flex items-center justify-between p-3 bg-gray-50 rounded-lg"><div><span class="font-medium text-gray-800">${m.name}</span><span class="text-sm text-gray-500 ml-2">${m.organization}</span></div><div class="text-sm text-gray-400">${m.is_open_source ? '开源' : '商业'}</div></div>`).join('')
: '<div class="text-gray-400">暂无数据</div>';
}
// 内置分类列表
const builtinCategories = ['ai-models', 'gpus', 'cpus'];
// 加载分类列表
async function loadAdminCategories() {
const res = await fetch('/api/categories?all=1');
categories = await res.json();
if (categories.length === 0) {
document.getElementById('admin-categories-table').innerHTML = '<tr><td colspan="6" class="text-center text-gray-400 py-8">暂无数据</td></tr>';
return;
}
document.getElementById('admin-categories-table').innerHTML = categories.map(c => {
const isBuiltin = builtinCategories.includes(c.id);
const subcatCount = (c.subcategories || []).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>
<td class="px-4 py-3 text-gray-500 text-sm font-mono">${c.id}</td>
<td class="px-4 py-3 font-medium text-gray-800">${c.name}</td>
<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">
${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>
${!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>
`;
}).join('');
}
// 加载模型列表
async function loadAdminModels() {
const res = await fetch('/api/models?all=1');
const models = await res.json();
if (models.length === 0) { document.getElementById('admin-models-table').innerHTML = '<tr><td colspan="12" class="text-center text-gray-400 py-8">暂无数据</td></tr>'; return; }
document.getElementById('admin-models-table').innerHTML = models.map(m => `
<tr class="border-b hover:bg-gray-50 ${m.visible === false ? 'bg-gray-100 opacity-60' : ''} ${m.is_pinned ? 'bg-yellow-50' : ''}">
<td class="px-3 py-3 text-center">
<button onclick="togglePin('model', '${m.id}')" class="${m.is_pinned ? 'text-yellow-500' : 'text-gray-400'} hover:opacity-80" title="${m.is_pinned ? '取消置顶' : '置顶'}">
<i class="${m.is_pinned ? 'ri-pushpin-fill' : 'ri-pushpin-line'}"></i>
</button>
</td>
<td class="px-3 py-3 font-medium text-gray-800">${m.name}</td>
<td class="px-3 py-3 text-gray-600">${m.organization}</td>
<td class="px-3 py-3">${m.parameters}B</td>
<td class="px-3 py-3 text-gray-600">${m.context_length || '-'}</td>
<td class="px-3 py-3">${m.is_open_source ? '<span class="text-green-600">开源</span>' : '<span class="text-gray-600">商业</span>'}</td>
<td class="px-3 py-3 text-gray-500 text-sm">${m.publish_date || '-'}</td>
<td class="px-3 py-3 text-gray-500 text-sm">${m.views || 0}</td>
<td class="px-3 py-3 text-gray-500 text-sm">${formatDateShort(m.created_at)}</td>
<td class="px-3 py-3 text-gray-500 text-sm">${formatDateShort(m.updated_at)}</td>
<td class="px-3 py-3 text-center">
<button onclick="toggleVisible('model', '${m.id}')" class="${m.visible === false ? 'text-gray-400' : 'text-green-600'} hover:opacity-80" title="${m.visible === false ? '点击显示' : '点击隐藏'}">
<i class="${m.visible === false ? 'ri-eye-off-line' : 'ri-eye-line'}"></i>
</button>
${m.raw_text ? `<button onclick="showRawData('${m.id}', 'model')" class="text-gray-400 hover:text-gray-600 ml-1" title="查看原始数据"><i class="ri-file-text-line"></i></button>` : ''}
</td>
<td class="px-3 py-3 text-center">
<button onclick="editItem('model', '${m.id}')" class="text-blue-600 hover:text-blue-800 mr-2"><i class="ri-edit-line"></i></button>
<button onclick="deleteItem('model', '${m.id}')" class="text-red-600 hover:text-red-800"><i class="ri-delete-bin-line"></i></button>
</td>
</tr>
`).join('');
}
// 加载GPU列表
async function loadAdminGpus() {
const res = await fetch('/api/gpus?all=1');
const gpus = await res.json();
if (gpus.length === 0) { document.getElementById('admin-gpus-table').innerHTML = '<tr><td colspan="12" class="text-center text-gray-400 py-8">暂无数据</td></tr>'; return; }
document.getElementById('admin-gpus-table').innerHTML = gpus.map(g => `
<tr class="border-b hover:bg-gray-50 ${g.visible === false ? 'bg-gray-100 opacity-60' : ''} ${g.is_pinned ? 'bg-yellow-50' : ''}">
<td class="px-3 py-3 text-center">
<button onclick="togglePin('gpu', '${g.id}')" class="${g.is_pinned ? 'text-yellow-500' : 'text-gray-400'} hover:opacity-80" title="${g.is_pinned ? '取消置顶' : '置顶'}">
<i class="${g.is_pinned ? 'ri-pushpin-fill' : 'ri-pushpin-line'}"></i>
</button>
</td>
<td class="px-3 py-3 font-medium text-gray-800">${g.name}</td>
<td class="px-3 py-3 text-gray-600">${g.manufacturer}</td>
<td class="px-3 py-3">${g.memory_gb}GB</td>
<td class="px-3 py-3 text-gray-600">${g.architecture || '-'}</td>
<td class="px-3 py-3 text-gray-600">${formatPrice(g)}</td>
<td class="px-3 py-3 text-gray-500 text-sm">${g.publish_date || '-'}</td>
<td class="px-3 py-3 text-gray-500 text-sm">${g.views || 0}</td>
<td class="px-3 py-3 text-gray-500 text-sm">${formatDateShort(g.created_at)}</td>
<td class="px-3 py-3 text-gray-500 text-sm">${formatDateShort(g.updated_at)}</td>
<td class="px-3 py-3 text-center">
<button onclick="toggleVisible('gpu', '${g.id}')" class="${g.visible === false ? 'text-gray-400' : 'text-green-600'} hover:opacity-80" title="${g.visible === false ? '点击显示' : '点击隐藏'}">
<i class="${g.visible === false ? 'ri-eye-off-line' : 'ri-eye-line'}"></i>
</button>
${g.raw_text ? `<button onclick="showRawData('${g.id}', 'gpu')" class="text-gray-400 hover:text-gray-600 ml-1" title="查看原始数据"><i class="ri-file-text-line"></i></button>` : ''}
</td>
<td class="px-3 py-3 text-center">
<button onclick="editItem('gpu', '${g.id}')" class="text-blue-600 hover:text-blue-800 mr-2"><i class="ri-edit-line"></i></button>
<button onclick="deleteItem('gpu', '${g.id}')" class="text-red-600 hover:text-red-800"><i class="ri-delete-bin-line"></i></button>
</td>
</tr>
`).join('');
}
// 加载CPU列表
async function loadAdminCpus() {
const res = await fetch('/api/cpus?all=1');
const cpus = await res.json();
if (cpus.length === 0) { document.getElementById('admin-cpus-table').innerHTML = '<tr><td colspan="12" class="text-center text-gray-400 py-8">暂无数据</td></tr>'; return; }
document.getElementById('admin-cpus-table').innerHTML = cpus.map(c => `
<tr class="border-b hover:bg-gray-50 ${c.visible === false ? 'bg-gray-100 opacity-60' : ''} ${c.is_pinned ? 'bg-yellow-50' : ''}">
<td class="px-3 py-3 text-center">
<button onclick="togglePin('cpu', '${c.id}')" class="${c.is_pinned ? 'text-yellow-500' : 'text-gray-400'} hover:opacity-80" title="${c.is_pinned ? '取消置顶' : '置顶'}">
<i class="${c.is_pinned ? 'ri-pushpin-fill' : 'ri-pushpin-line'}"></i>
</button>
</td>
<td class="px-3 py-3 font-medium text-gray-800">${c.name}</td>
<td class="px-3 py-3 text-gray-600">${c.manufacturer}</td>
<td class="px-3 py-3">${c.cores}/${c.threads}</td>
<td class="px-3 py-3 text-gray-600">${c.base_clock_ghz || '-'}-${c.boost_clock_ghz || '-'}GHz</td>
<td class="px-3 py-3 text-gray-600">${formatPrice(c)}</td>
<td class="px-3 py-3 text-gray-500 text-sm">${c.publish_date || '-'}</td>
<td class="px-3 py-3 text-gray-500 text-sm">${c.views || 0}</td>
<td class="px-3 py-3 text-gray-500 text-sm">${formatDateShort(c.created_at)}</td>
<td class="px-3 py-3 text-gray-500 text-sm">${formatDateShort(c.updated_at)}</td>
<td class="px-3 py-3 text-center">
<button onclick="toggleVisible('cpu', '${c.id}')" class="${c.visible === false ? 'text-gray-400' : 'text-green-600'} hover:opacity-80" title="${c.visible === false ? '点击显示' : '点击隐藏'}">
<i class="${c.visible === false ? 'ri-eye-off-line' : 'ri-eye-line'}"></i>
</button>
${c.raw_text ? `<button onclick="showRawData('${c.id}', 'cpu')" class="text-gray-400 hover:text-gray-600 ml-1" title="查看原始数据"><i class="ri-file-text-line"></i></button>` : ''}
</td>
<td class="px-3 py-3 text-center">
<button onclick="editItem('cpu', '${c.id}')" class="text-blue-600 hover:text-blue-800 mr-2"><i class="ri-edit-line"></i></button>
<button onclick="deleteItem('cpu', '${c.id}')" class="text-red-600 hover:text-red-800"><i class="ri-delete-bin-line"></i></button>
</td>
</tr>
`).join('');
}
// 加载知识列表
async function loadAdminKnowledge() {
const catRes = await fetch('/api/categories');
categories = await catRes.json();
document.getElementById('category-filters').innerHTML = categories.map(c => `<button onclick="filterKnowledge('${c.id}')" class="px-3 py-1 rounded text-sm bg-gray-100 text-gray-600 hover:bg-gray-200">${c.name}</button>`).join('');
let url = '/api/knowledge?all=1';
if (currentFilter) url += `&category=${currentFilter}`;
const res = await fetch(url);
const knowledge = await res.json();
if (knowledge.length === 0) { document.getElementById('admin-knowledge-table').innerHTML = '<tr><td colspan="7" class="text-center text-gray-400 py-8">暂无数据</td></tr>'; return; }
document.getElementById('admin-knowledge-table').innerHTML = knowledge.map(k => {
const cat = categories.find(c => c.id === k.category);
return `<tr class="border-b hover:bg-gray-50 ${k.visible === false ? 'bg-gray-100 opacity-60' : ''}">
<td class="px-4 py-3"><i class="${k.icon || 'ri-file-list-line'} text-xl text-gray-400"></i></td>
<td class="px-4 py-3 font-medium text-gray-800">${k.title}</td>
<td class="px-4 py-3 text-gray-600">${cat ? cat.name : '-'}</td>
<td class="px-4 py-3 text-gray-500 text-sm max-w-xs truncate">${k.content || '-'}</td>
<td class="px-4 py-3">${k.order || 0}</td>
<td class="px-4 py-3 text-center">
<button onclick="toggleVisible('knowledge', '${k.id}')" class="${k.visible === false ? 'text-gray-400' : 'text-green-600'} hover:opacity-80" title="${k.visible === false ? '点击显示' : '点击隐藏'}">
<i class="${k.visible === false ? 'ri-eye-off-line' : 'ri-eye-line'}"></i>
</button>
</td>
<td class="px-4 py-3 text-center">
<button onclick="editItem('knowledge', '${k.id}')" class="text-blue-600 hover:text-blue-800 mr-2"><i class="ri-edit-line"></i></button>
<button onclick="deleteItem('knowledge', '${k.id}')" class="text-red-600 hover:text-red-800"><i class="ri-delete-bin-line"></i></button>
</td>
</tr>`;
}).join('');
}
function filterKnowledge(category) { currentFilter = category; loadAdminKnowledge(); }
// 打开添加弹窗
function openAddModal(type) {
currentType = type;
currentId = '';
currentData = {};
const titles = {category: '分类', model: '模型', gpu: 'GPU', cpu: 'CPU', knowledge: '知识', dynamic: '数据'};
document.getElementById('modalTitle').textContent = '添加' + titles[type];
const forms = {category: getCategoryForm, model: getModelForm, gpu: getGpuForm, cpu: getCpuForm, knowledge: getKnowledgeForm, dynamic: getDynamicForm};
document.getElementById('modalContent').innerHTML = forms[type]();
showSmartUpdateButton(); // 添加时隐藏智能补充按钮
document.getElementById('editModal').classList.remove('hidden');
}
// 编辑项
async function editItem(type, id) {
currentType = type;
currentId = id;
const endpoint = type === 'category' ? 'categories' : type === 'knowledge' ? 'knowledge' : type + 's';
const res = await fetch(`/api/${endpoint}/${id}`);
currentData = await res.json();
const titles = {category: '分类', model: '模型', gpu: 'GPU', cpu: 'CPU', knowledge: '知识'};
document.getElementById('modalTitle').textContent = '编辑' + titles[type];
const forms = {category: getCategoryForm, model: getModelForm, gpu: getGpuForm, cpu: getCpuForm, knowledge: getKnowledgeForm};
document.getElementById('modalContent').innerHTML = forms[type](currentData);
showSmartUpdateButton(); // 显示智能补充按钮
document.getElementById('editModal').classList.remove('hidden');
}
// 编辑动态分类数据
async function editDynamicItem(id) {
currentType = 'dynamic';
currentId = id;
const res = await fetch(`/api/items/${dynamicCategoryId}/${id}`);
currentData = await res.json();
document.getElementById('modalTitle').textContent = '编辑数据';
document.getElementById('modalContent').innerHTML = getDynamicForm(currentData);
showSmartUpdateButton(); // 显示智能补充按钮
document.getElementById('editModal').classList.remove('hidden');
}
// 删除项
async function deleteItem(type, id) {
if (!confirm('确定删除?')) return;
const endpoint = type === 'category' ? 'categories' : type === 'knowledge' ? 'knowledge' : type + 's';
await fetch(`/api/${endpoint}/${id}`, { method: 'DELETE' });
const loaders = {category: loadAdminCategories, model: loadAdminModels, gpu: loadAdminGpus, cpu: loadAdminCpus, knowledge: loadAdminKnowledge};
loaders[type]();
loadOverview();
if (type === 'category') { await loadCategories(); renderSidebar(); }
}
// 删除动态分类数据
async function deleteDynamicItem(id) {
if (!confirm('确定删除?')) return;
await fetch(`/api/items/${dynamicCategoryId}/${id}`, { method: 'DELETE' });
showDynamicCategory(dynamicCategoryId);
}
// 保存项
async function saveItem() {
const form = document.getElementById('itemForm');
const formData = new FormData(form);
const data = {};
formData.forEach((value, key) => {
if (value) {
const numFields = ['parameters', 'context_length', 'mmlu', 'humaneval', 'input_price', 'output_price', 'memory_gb', 'cuda_cores', 'tensor_cores', 'memory_bandwidth_gbs', 'fp32_tflops', 'fp16_tflops', 'int8_perf_tops', 'price_usd', 'min_price', 'max_price', 'release_year', 'cores', 'threads', 'base_clock_ghz', 'boost_clock_ghz', 'l3_cache_mb', 'tdp_watts', 'order', 'price', 'views'];
if (numFields.includes(key)) data[key] = parseFloat(value);
else if (key === 'is_open_source' || key === 'visible') data[key] = value === 'true';
else if (key === 'images') {
try { data[key] = JSON.parse(value); } catch { data[key] = []; }
}
else if (key === 'subcategories') {
// 解析子类别JSON
try { data[key] = JSON.parse(value); } catch {
alert('子类别JSON格式错误请检查格式');
return;
}
}
else data[key] = value;
}
});
// 处理图片数据
const imagesInput = document.getElementById('imagesInput');
if (imagesInput) {
try { data['images'] = JSON.parse(imagesInput.value); } catch { data['images'] = []; }
}
if (currentType === 'dynamic') {
data.category_id = dynamicCategoryId;
}
let endpoint;
if (currentType === 'category') endpoint = 'categories';
else if (currentType === 'knowledge') endpoint = 'knowledge';
else if (currentType === 'dynamic') endpoint = `items/${dynamicCategoryId}`;
else endpoint = currentType + 's';
if (currentId) {
await fetch(`/api/${endpoint}/${currentId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) });
} else {
await fetch(`/api/${endpoint}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) });
}
closeModal();
if (currentType === 'dynamic') showDynamicCategory(dynamicCategoryId);
else {
const loaders = {category: loadAdminCategories, model: loadAdminModels, gpu: loadAdminGpus, cpu: loadAdminCpus, knowledge: loadAdminKnowledge};
loaders[currentType]();
}
loadOverview();
if (currentType === 'category') { await loadCategories(); renderSidebar(); }
}
function closeModal() { document.getElementById('editModal').classList.add('hidden'); }
// 图片上传组件
function getImageUploadComponent(images = [], type) {
const imageUrls = images || [];
return `
<div class="border-t pt-4 mt-4">
<label class="text-sm text-gray-600 mb-2 block">图片上传(支持多张)</label>
<div class="flex flex-wrap gap-2 mb-2" id="imagePreviewArea">
${imageUrls.map((url, idx) => `
<div class="relative w-24 h-24 border rounded-lg overflow-hidden group">
<img src="${url}" class="w-full h-full object-cover">
<button onclick="removeImage(${idx})" class="absolute top-1 right-1 w-6 h-6 bg-red-500 text-white rounded-full opacity-0 group-hover:opacity-100 transition flex items-center justify-center">
<i class="ri-close-line"></i>
</button>
</div>
`).join('')}
</div>
<div class="flex gap-2">
<input type="file" id="imageInput" accept="image/*" multiple class="hidden" onchange="handleImageUpload(event, '${type}')">
<button onclick="document.getElementById('imageInput').click()" class="px-4 py-2 bg-gray-100 text-gray-600 rounded-lg hover:bg-gray-200 text-sm">
<i class="ri-image-add-line mr-1"></i>选择图片
</button>
<button onclick="pasteImageFromClipboard('${type}')'" class="px-4 py-2 bg-gray-100 text-gray-600 rounded-lg hover:bg-gray-200 text-sm">
<i class="ri-clipboard-line mr-1"></i>粘贴图片
</button>
</div>
<input type="hidden" name="images" value="${JSON.stringify(imageUrls)}" id="imagesInput">
</div>
`;
}
// 当前图片列表
let currentImages = [];
// 处理图片上传
async function handleImageUpload(event, type) {
const files = event.target.files;
for (let file of files) {
const formData = new FormData();
formData.append('file', file);
try {
const res = await fetch('/api/upload/image', {
method: 'POST',
body: formData
});
const data = await res.json();
if (data.success) {
currentImages.push(data.url);
updateImagePreview();
}
} catch (e) {
alert('上传失败: ' + e.message);
}
}
event.target.value = '';
}
// 从剪贴板粘贴图片
async function pasteImageFromClipboard(type) {
try {
// 检查剪贴板API是否可用需要HTTPS或localhost
if (!navigator.clipboard || !navigator.clipboard.read) {
alert('剪贴板API需要HTTPS或localhost环境。\n当前访问地址不支持请使用文件选择上传。\n\n可改用 localhost:19010 访问来支持粘贴功能。');
return;
}
const clipboardItems = await navigator.clipboard.read();
let found = false;
for (const item of clipboardItems) {
for (const itemType of item.types) {
if (itemType.startsWith('image/')) {
found = true;
const blob = await item.getType(itemType);
const reader = new FileReader();
reader.onload = async (e) => {
const base64 = e.target.result;
try {
const res = await fetch('/api/upload/image/base64', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image: base64 })
});
const data = await res.json();
if (data.success) {
currentImages.push(data.url);
updateImagePreview();
}
} catch (err) {
alert('上传失败: ' + err.message);
}
};
reader.readAsDataURL(blob);
}
}
}
if (!found) {
alert('剪贴板中没有图片,请先复制一张图片');
}
} catch (e) {
if (e.name === 'NotAllowedError') {
alert('浏览器拒绝访问剪贴板。\n请使用文件选择上传或改用 localhost:19010 访问。');
} else {
alert('无法从剪贴板获取图片: ' + e.message + '\n请使用文件选择上传');
}
}
}
// 移除图片
function removeImage(index) {
currentImages.splice(index, 1);
updateImagePreview();
}
// 更新图片预览
function updateImagePreview() {
const area = document.getElementById('imagePreviewArea');
if (!area) return;
area.innerHTML = currentImages.map((url, idx) => `
<div class="relative w-24 h-24 border rounded-lg overflow-hidden group">
<img src="${url}" class="w-full h-full object-cover">
<button onclick="removeImage(${idx})" class="absolute top-1 right-1 w-6 h-6 bg-red-500 text-white rounded-full opacity-0 group-hover:opacity-100 transition flex items-center justify-center">
<i class="ri-close-line"></i>
</button>
</div>
`).join('');
const input = document.getElementById('imagesInput');
if (input) {
input.value = JSON.stringify(currentImages);
}
}
// 表单模板
function getCategoryForm(data = {}) {
const subcategories = data.subcategories || [];
const isBuiltin = builtinCategories.includes(data.id);
// 存储到全局变量,便于管理
window.currentEditingSubcategories = JSON.parse(JSON.stringify(subcategories));
// 内置类别只显示子类别管理
if (isBuiltin) {
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>
</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>
<div><label class="text-sm text-gray-500 mb-1 block">名称</label><div class="text-gray-700">${data.name}</div></div>
<div><label class="text-sm text-gray-500 mb-1 block">图标</label><div class="text-gray-700"><i class="${data.icon} mr-1"></i>${data.icon}</div></div>
<div><label class="text-sm text-gray-500 mb-1 block">颜色</label><div class="text-gray-700">${data.color}</div></div>
</div>
<input type="hidden" name="id" value="${data.id}">
<input type="hidden" name="name" value="${data.name}">
<input type="hidden" name="icon" value="${data.icon}">
<input type="hidden" name="color" value="${data.color}">
<input type="hidden" name="order" value="${data.order || 0}">
<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-folder-line mr-1"></i>子类别管理</label>
<button onclick="openSubcategoryAddModal()" class="px-3 py-1.5 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700">
<i class="ri-add-line mr-1"></i>添加子类别
</button>
</div>
<div id="subcategoriesList" class="space-y-2">
${renderSubcategoriesList(subcategories)}
</div>
<input type="hidden" name="subcategories" id="subcategoriesHidden" value='${JSON.stringify(subcategories)}'>
</div>
</form>`;
}
// 自定义类别完整编辑表单
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="${data.id || ''}" ${data.id ? 'readonly' : ''} required class="w-full px-3 py-2 border rounded-lg ${data.id ? 'bg-gray-100' : ''}"></div>
<div><label class="text-sm text-gray-600 mb-1 block">名称 *</label><input type="text" name="name" value="${data.name || ''}" required class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">图标</label><input type="text" name="icon" value="${data.icon || 'ri-folder-line'}" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">颜色</label><select name="color" class="w-full px-3 py-2 border rounded-lg">
<option value="blue" ${data.color === 'blue' ? 'selected' : ''}>蓝色</option>
<option value="green" ${data.color === 'green' ? 'selected' : ''}>绿色</option>
<option value="purple" ${data.color === 'purple' ? 'selected' : ''}>紫色</option>
<option value="orange" ${data.color === 'orange' ? 'selected' : ''}>橙色</option>
<option value="teal" ${data.color === 'teal' ? 'selected' : ''}>青色</option>
<option value="red" ${data.color === 'red' ? 'selected' : ''}>红色</option>
</select></div>
<div><label class="text-sm text-gray-600 mb-1 block">排序</label><input type="number" name="order" value="${data.order || 0}" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">是否显示</label><select name="visible" class="w-full px-3 py-2 border rounded-lg">
<option value="true" ${data.visible !== false ? 'selected' : ''}>显示</option>
<option value="false" ${data.visible === false ? 'selected' : ''}>隐藏</option>
</select></div>
</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">${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-folder-line mr-1"></i>子类别管理</label>
<button onclick="openSubcategoryAddModal()" class="px-3 py-1.5 bg-green-600 text-white rounded-lg text-sm hover:bg-green-700">
<i class="ri-add-line mr-1"></i>添加子类别
</button>
</div>
<div id="subcategoriesList" class="space-y-2">
${renderSubcategoriesList(subcategories)}
</div>
<input type="hidden" name="subcategories" id="subcategoriesHidden" value='${JSON.stringify(subcategories)}'>
</div>
</form>`;
}
// 渲染子类别列表
function renderSubcategoriesList(subcategories) {
if (!subcategories || subcategories.length === 0) {
return '<div class="text-gray-400 text-sm py-4 text-center bg-gray-50 rounded-lg">暂无子类别,点击上方按钮添加</div>';
}
return subcategories.map((sub, 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">
<i class="${sub.icon || 'ri-folder-line'} text-indigo-600"></i>
</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>
</div>
<div class="flex gap-2 opacity-0 group-hover:opacity-100 transition">
<button onclick="editSubcategory(${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="deleteSubcategory(${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 openSubcategoryAddModal() {
document.getElementById('subcategoryModalTitle').textContent = '添加子类别';
document.getElementById('subcategoryModalContent').innerHTML = getSubcategoryForm();
document.getElementById('subcategoryModal').classList.remove('hidden');
window.editingSubcategoryIndex = -1;
}
// 编辑子类别
function editSubcategory(index) {
const sub = window.currentEditingSubcategories[index];
document.getElementById('subcategoryModalTitle').textContent = '编辑子类别';
document.getElementById('subcategoryModalContent').innerHTML = getSubcategoryForm(sub);
document.getElementById('subcategoryModal').classList.remove('hidden');
window.editingSubcategoryIndex = index;
}
// 删除子类别
function deleteSubcategory(index) {
if (!confirm('确定删除此子类别?')) return;
window.currentEditingSubcategories.splice(index, 1);
document.getElementById('subcategoriesList').innerHTML = renderSubcategoriesList(window.currentEditingSubcategories);
document.getElementById('subcategoriesHidden').value = JSON.stringify(window.currentEditingSubcategories);
}
// 子类别表单
function getSubcategoryForm(data = {}) {
const keyFeatures = (data.key_features || []).join(', ');
const featureLabels = data.feature_labels || {};
const featureLabelsStr = Object.entries(featureLabels).map(([k, v]) => `${k}:${v}`).join(', ');
return `<div 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" id="sub_id" value="${data.id || ''}" required class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">名称 *</label><input type="text" id="sub_name" value="${data.name || ''}" required class="w-full px-3 py-2 border rounded-lg"></div>
<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>
<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>
</div>
<div>
<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>`;
}
// 保存子类别
function saveSubcategory() {
const id = document.getElementById('sub_id').value.trim();
const name = document.getElementById('sub_name').value.trim();
const icon = document.getElementById('sub_icon').value.trim() || 'ri-folder-line';
const keyFeaturesStr = document.getElementById('sub_key_features').value.trim();
const featureLabelsStr = document.getElementById('sub_feature_labels').value.trim();
if (!id || !name) {
alert('ID和名称不能为空');
return;
}
// 解析 key_features
const key_features = keyFeaturesStr ? keyFeaturesStr.split(',').map(s => s.trim()).filter(s => s) : [];
// 解析 feature_labels
const feature_labels = {};
if (featureLabelsStr) {
featureLabelsStr.split(',').forEach(pair => {
const [key, value] = pair.split(':').map(s => s.trim());
if (key && value) {
feature_labels[key] = value;
}
});
}
const subcategory = {
id,
name,
icon,
key_features,
feature_labels
};
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();
}
// 关闭子类别弹框
function closeSubcategoryModal() {
document.getElementById('subcategoryModal').classList.add('hidden');
}
function getKnowledgeForm(data = {}) {
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">标题 *</label><input type="text" name="title" value="${data.title || ''}" required class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">分类</label><select name="category" class="w-full px-3 py-2 border rounded-lg"><option value="">选择分类</option>${categories.map(c => `<option value="${c.id}" ${data.category === c.id ? 'selected' : ''}>${c.name}</option>`).join('')}</select></div>
<div><label class="text-sm text-gray-600 mb-1 block">图标</label><input type="text" name="icon" value="${data.icon || 'ri-file-list-line'}" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">排序</label><input type="number" name="order" value="${data.order || 0}" class="w-full px-3 py-2 border rounded-lg"></div>
</div>
<div><label class="text-sm text-gray-600 mb-1 block">简介 *</label><textarea name="content" rows="2" required class="w-full px-3 py-2 border rounded-lg">${data.content || ''}</textarea></div>
<div><label class="text-sm text-gray-600 mb-1 block">详细内容</label><textarea name="detail" rows="6" class="w-full px-3 py-2 border rounded-lg">${data.detail || ''}</textarea></div>
</form>`;
}
function getDynamicForm(data = {}) {
const cat = categories.find(c => c.id === dynamicCategoryId);
currentImages = data.images || [];
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">名称 *</label><input type="text" name="name" value="${data.name || ''}" required class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">品牌</label><input type="text" name="brand" value="${data.brand || ''}" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">价格</label><input type="number" name="price" value="${data.price || ''}" step="0.01" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">年份</label><input type="number" name="year" value="${data.year || ''}" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">发布日期</label><input type="date" name="publish_date" value="${data.publish_date || ''}" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">热度</label><input type="number" name="views" value="${data.views || 0}" class="w-full px-3 py-2 border rounded-lg"></div>
</div>
<div><label class="text-sm text-gray-600 mb-1 block">参数JSON格式</label><textarea name="specs" rows="4" class="w-full px-3 py-2 border rounded-lg font-mono text-sm" placeholder='{"key": "value"}'>${data.specs || ''}</textarea></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">${data.description || ''}</textarea></div>
${getImageUploadComponent(currentImages, 'dynamic')}
</form>`;
}
function getModelForm(data = {}) {
currentImages = data.images || [];
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">名称 *</label><input type="text" name="name" value="${data.name || ''}" required class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">厂商 *</label><input type="text" name="organization" value="${data.organization || ''}" required class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">参数量(B) *</label><input type="number" name="parameters" value="${data.parameters || ''}" required class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">架构</label><input type="text" name="architecture" value="${data.architecture || ''}" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">上下文长度</label><input type="number" name="context_length" value="${data.context_length || ''}" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">类型</label><select name="is_open_source" class="w-full px-3 py-2 border rounded-lg"><option value="false" ${!data.is_open_source ? 'selected' : ''}>商业</option><option value="true" ${data.is_open_source ? 'selected' : ''}>开源</option></select></div>
<div><label class="text-sm text-gray-600 mb-1 block">MMLU分数</label><input type="number" name="mmlu" value="${data.mmlu || ''}" step="0.1" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">HumanEval分数</label><input type="number" name="humaneval" value="${data.humaneval || ''}" step="0.1" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">输入价格($/1K)</label><input type="number" name="input_price" value="${data.input_price || ''}" step="0.001" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">输出价格($/1K)</label><input type="number" name="output_price" value="${data.output_price || ''}" step="0.001" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">许可证</label><input type="text" name="license" value="${data.license || ''}" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">发布日期</label><input type="date" name="publish_date" value="${data.publish_date || ''}" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">热度</label><input type="number" name="views" value="${data.views || 0}" class="w-full px-3 py-2 border rounded-lg"></div>
</div>
<div><label class="text-sm text-gray-600 mb-1 block">描述</label><textarea name="description" rows="3" class="w-full px-3 py-2 border rounded-lg">${data.description || ''}</textarea></div>
${getImageUploadComponent(currentImages, 'model')}
</form>`;
}
function getGpuForm(data = {}) {
currentImages = data.images || [];
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">名称 *</label><input type="text" name="name" value="${data.name || ''}" required class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">厂商 *</label><input type="text" name="manufacturer" value="${data.manufacturer || ''}" required class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">架构</label><input type="text" name="architecture" value="${data.architecture || ''}" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">显存(GB) *</label><input type="number" name="memory_gb" value="${data.memory_gb || ''}" required class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">CUDA核心</label><input type="number" name="cuda_cores" value="${data.cuda_cores || ''}" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">Tensor核心</label><input type="number" name="tensor_cores" value="${data.tensor_cores || ''}" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">显存带宽(GB/s)</label><input type="number" name="memory_bandwidth_gbs" value="${data.memory_bandwidth_gbs || ''}" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">FP16性能(TF)</label><input type="number" name="fp16_tflops" value="${data.fp16_tflops || ''}" step="0.1" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">币种</label><select name="currency" class="w-full px-3 py-2 border rounded-lg">
<option value="USD" ${data.currency === 'USD' || !data.currency ? 'selected' : ''}>美元 (USD)</option>
<option value="CNY" ${data.currency === 'CNY' ? 'selected' : ''}>人民币 (CNY)</option>
<option value="EUR" ${data.currency === 'EUR' ? 'selected' : ''}>欧元 (EUR)</option>
</select></div>
<div><label class="text-sm text-gray-600 mb-1 block">价格</label><input type="number" name="price_usd" value="${data.price_usd || ''}" step="0.01" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">最低价</label><input type="number" name="min_price" value="${data.min_price || ''}" step="0.01" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">最高价</label><input type="number" name="max_price" value="${data.max_price || ''}" step="0.01" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">价格单位</label><input type="text" name="price_unit" value="${data.price_unit || ''}" placeholder="如: 万" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">发布日期</label><input type="date" name="publish_date" value="${data.publish_date || ''}" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">热度</label><input type="number" name="views" value="${data.views || 0}" class="w-full px-3 py-2 border rounded-lg"></div>
</div>
<div><label class="text-sm text-gray-600 mb-1 block">描述</label><textarea name="description" rows="3" class="w-full px-3 py-2 border rounded-lg">${data.description || ''}</textarea></div>
${getImageUploadComponent(currentImages, 'gpu')}
</form>`;
}
function getCpuForm(data = {}) {
currentImages = data.images || [];
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">名称 *</label><input type="text" name="name" value="${data.name || ''}" required class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">厂商 *</label><input type="text" name="manufacturer" value="${data.manufacturer || ''}" required class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">架构</label><input type="text" name="architecture" value="${data.architecture || ''}" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">核心数 *</label><input type="number" name="cores" value="${data.cores || ''}" required class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">线程数 *</label><input type="number" name="threads" value="${data.threads || ''}" required class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">基础频率(GHz)</label><input type="number" name="base_clock_ghz" value="${data.base_clock_ghz || ''}" step="0.1" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">加速频率(GHz)</label><input type="number" name="boost_clock_ghz" value="${data.boost_clock_ghz || ''}" step="0.1" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">L3缓存(MB)</label><input type="number" name="l3_cache_mb" value="${data.l3_cache_mb || ''}" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">TDP功耗(W)</label><input type="number" name="tdp_watts" value="${data.tdp_watts || ''}" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">币种</label><select name="currency" class="w-full px-3 py-2 border rounded-lg">
<option value="USD" ${data.currency === 'USD' || !data.currency ? 'selected' : ''}>美元 (USD)</option>
<option value="CNY" ${data.currency === 'CNY' ? 'selected' : ''}>人民币 (CNY)</option>
<option value="EUR" ${data.currency === 'EUR' ? 'selected' : ''}>欧元 (EUR)</option>
</select></div>
<div><label class="text-sm text-gray-600 mb-1 block">价格</label><input type="number" name="price_usd" value="${data.price_usd || ''}" step="0.01" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">最低价</label><input type="number" name="min_price" value="${data.min_price || ''}" step="0.01" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">最高价</label><input type="number" name="max_price" value="${data.max_price || ''}" step="0.01" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">价格单位</label><input type="text" name="price_unit" value="${data.price_unit || ''}" placeholder="如: 万" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">发布日期</label><input type="date" name="publish_date" value="${data.publish_date || ''}" class="w-full px-3 py-2 border rounded-lg"></div>
<div><label class="text-sm text-gray-600 mb-1 block">热度</label><input type="number" name="views" value="${data.views || 0}" class="w-full px-3 py-2 border rounded-lg"></div>
</div>
<div><label class="text-sm text-gray-600 mb-1 block">描述</label><textarea name="description" rows="3" class="w-full px-3 py-2 border rounded-lg">${data.description || ''}</textarea></div>
${getImageUploadComponent(currentImages, 'cpu')}
</form>`;
}
document.getElementById('editModal').addEventListener('click', function(e) { if (e.target === this) closeModal(); });
document.getElementById('smartAddModal').addEventListener('click', function(e) { if (e.target === this) closeSmartAddModal(); });
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(); });
// ============ 智能添加功能 ============
let smartAddType = '';
let smartAddImages = []; // 智能添加的图片列表
function openSmartAddModal(type) {
smartAddType = type;
smartAddImages = [];
document.getElementById('smartAddText').value = '';
document.getElementById('smartAddPreview').classList.add('hidden');
document.getElementById('smartImagePreviewArea').innerHTML = '';
document.getElementById('smartImageCount').textContent = '0';
document.getElementById('smartAddModal').classList.remove('hidden');
}
function closeSmartAddModal() {
document.getElementById('smartAddModal').classList.add('hidden');
}
// 处理智能添加图片上传
async function handleSmartImageUpload(event) {
const files = event.target.files;
for (let file of files) {
const formData = new FormData();
formData.append('file', file);
try {
const res = await fetch('/api/upload/image', {
method: 'POST',
body: formData
});
const data = await res.json();
if (data.success) {
smartAddImages.push(data.url);
updateSmartImagePreview();
}
} catch (e) {
alert('上传失败: ' + e.message);
}
}
event.target.value = '';
}
// 从剪贴板粘贴图片
async function pasteSmartImageFromClipboard() {
try {
// 检查剪贴板API是否可用需要HTTPS或localhost
if (!navigator.clipboard || !navigator.clipboard.read) {
alert('剪贴板API需要HTTPS或localhost环境。\n当前访问地址不支持请使用文件选择上传。\n\n可改用 localhost:19010 访问来支持粘贴功能。');
return;
}
const clipboardItems = await navigator.clipboard.read();
let found = false;
for (const item of clipboardItems) {
for (const type of item.types) {
if (type.startsWith('image/')) {
found = true;
const blob = await item.getType(type);
const reader = new FileReader();
reader.onload = async (e) => {
const base64 = e.target.result;
try {
const res = await fetch('/api/upload/image/base64', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image: base64 })
});
const data = await res.json();
if (data.success) {
smartAddImages.push(data.url);
updateSmartImagePreview();
}
} catch (err) {
alert('上传失败: ' + err.message);
}
};
reader.readAsDataURL(blob);
}
}
}
if (!found) {
alert('剪贴板中没有图片,请先复制一张图片');
}
} catch (e) {
if (e.name === 'NotAllowedError') {
alert('浏览器拒绝访问剪贴板。\n请使用文件选择上传或改用 localhost:19010 访问。');
} else {
alert('无法从剪贴板获取图片: ' + e.message + '\n请使用文件选择上传');
}
}
}
// 清空智能添加图片
function clearSmartImages() {
smartAddImages = [];
updateSmartImagePreview();
}
// 移除单张图片
function removeSmartImage(index) {
smartAddImages.splice(index, 1);
updateSmartImagePreview();
}
// 更新智能添加图片预览
function updateSmartImagePreview() {
const area = document.getElementById('smartImagePreviewArea');
const count = document.getElementById('smartImageCount');
area.innerHTML = smartAddImages.map((url, idx) => `
<div class="relative w-24 h-24 border rounded-lg overflow-hidden group">
<img src="${url}" class="w-full h-full object-cover">
<button onclick="removeSmartImage(${idx})" class="absolute top-1 right-1 w-6 h-6 bg-red-500 text-white rounded-full opacity-0 group-hover:opacity-100 transition flex items-center justify-center">
<i class="ri-close-line"></i>
</button>
</div>
`).join('');
count.textContent = smartAddImages.length;
}
// 预览解析结果
async function previewSmartParse() {
const text = document.getElementById('smartAddText').value.trim();
if (!text && smartAddImages.length === 0) {
alert('请上传图片或输入文本');
return;
}
const btn = document.getElementById('previewBtn');
btn.disabled = true;
btn.innerHTML = '<i class="ri-loader-4-line animate-spin mr-1"></i>解析中...';
try {
const res = await fetch('/api/parse-images', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: text,
images: smartAddImages,
category_type: smartAddType
})
});
const data = await res.json();
if (data.error) {
alert('解析失败: ' + data.error);
} else {
// 显示解析结果
document.getElementById('smartAddPreview').classList.remove('hidden');
let html = `<div class="mb-2 text-green-600"><i class="ri-checkbox-circle-line mr-1"></i>识别到 ${data.count} 个产品</div>`;
data.products.forEach((product, idx) => {
html += `
<div class="bg-white rounded-lg p-3 mb-2 border">
<div class="font-medium text-gray-800 mb-2">产品 ${idx + 1}: ${product.name || '未命名'}</div>
<div class="text-xs text-gray-600 grid grid-cols-2 gap-2">
${Object.entries(product).map(([k, v]) => `
<div><span class="text-gray-400">${k}:</span> ${v || '-'}</div>
`).join('')}
</div>
</div>
`;
});
document.getElementById('smartAddResult').innerHTML = html;
}
} catch (e) {
alert('请求失败: ' + e.message);
}
btn.disabled = false;
btn.innerHTML = '<i class="ri-eye-line mr-1"></i>预览解析结果';
}
async function smartAddSubmit() {
const text = document.getElementById('smartAddText').value.trim();
if (!text && smartAddImages.length === 0) {
alert('请上传图片或输入文本');
return;
}
const btn = document.getElementById('smartAddBtn');
btn.disabled = true;
btn.innerHTML = '<i class="ri-loader-4-line animate-spin mr-1"></i>解析并保存中...';
try {
let endpoint;
if (smartAddType === 'model') endpoint = '/api/models/smart-add';
else if (smartAddType === 'gpu') endpoint = '/api/gpus/smart-add';
else if (smartAddType === 'cpu') endpoint = '/api/cpus/smart-add';
else if (smartAddType === 'dynamic') endpoint = `/api/items/${dynamicCategoryId}/smart-add`;
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: text,
images: smartAddImages
})
});
const data = await res.json();
if (data.error) {
alert('解析失败: ' + data.error);
} else {
// 显示解析结果
document.getElementById('smartAddPreview').classList.remove('hidden');
let html = `<div class="mb-2 text-green-600"><i class="ri-checkbox-circle-fill mr-1"></i>成功添加 ${data.count} 个产品</div>`;
data.products.forEach((product, idx) => {
html += `
<div class="bg-white rounded-lg p-3 mb-2 border border-green-200">
<div class="font-medium text-gray-800 mb-2">产品 ${idx + 1}: ${product.name || '未命名'}</div>
<div class="text-xs text-gray-500">ID: ${product.id}</div>
</div>
`;
});
document.getElementById('smartAddResult').innerHTML = html;
// 关闭弹窗并刷新列表
setTimeout(() => {
closeSmartAddModal();
if (smartAddType === 'dynamic') showDynamicCategory(dynamicCategoryId);
else {
const loaders = {model: loadAdminModels, gpu: loadAdminGpus, cpu: loadAdminCpus};
loaders[smartAddType]();
}
loadOverview();
}, 1500);
}
} catch (e) {
alert('请求失败: ' + e.message);
}
btn.disabled = false;
btn.innerHTML = '<i class="ri-magic-line mr-1"></i>解析并添加';
}
// ============ 显示切换功能 ============
async function toggleVisible(type, id) {
let endpoint;
if (type === 'category') endpoint = `/api/categories/${id}/visible`;
else if (type === 'model') endpoint = `/api/models/${id}/visible`;
else if (type === 'gpu') endpoint = `/api/gpus/${id}/visible`;
else if (type === 'cpu') endpoint = `/api/cpus/${id}/visible`;
else if (type === 'knowledge') endpoint = `/api/knowledge/${id}/visible`;
else if (type === 'dynamic') endpoint = `/api/items/${dynamicCategoryId}/${id}/visible`;
try {
await fetch(endpoint, { method: 'POST' });
// 刷新列表
if (type === 'dynamic') showDynamicCategory(dynamicCategoryId);
else {
const loaders = {category: loadAdminCategories, model: loadAdminModels, gpu: loadAdminGpus, cpu: loadAdminCpus, knowledge: loadAdminKnowledge};
loaders[type]();
}
} catch (e) {
alert('切换失败: ' + e.message);
}
}
// ============ 置顶切换功能 ============
async function togglePin(type, id) {
let endpoint;
if (type === 'model') endpoint = `/api/models/${id}/pin`;
else if (type === 'gpu') endpoint = `/api/gpus/${id}/pin`;
else if (type === 'cpu') endpoint = `/api/cpus/${id}/pin`;
else if (type === 'dynamic') endpoint = `/api/items/${dynamicCategoryId}/${id}/pin`;
try {
await fetch(endpoint, { method: 'POST' });
// 刷新列表
if (type === 'dynamic') showDynamicCategory(dynamicCategoryId);
else {
const loaders = {model: loadAdminModels, gpu: loadAdminGpus, cpu: loadAdminCpus};
loaders[type]();
}
} catch (e) {
alert('置顶切换失败: ' + e.message);
}
}
// ============ 原始数据查看 ============
async function showRawData(id, type) {
let endpoint;
if (type === 'model') endpoint = `/api/models/${id}`;
else if (type === 'gpu') endpoint = `/api/gpus/${id}`;
else if (type === 'cpu') endpoint = `/api/cpus/${id}`;
else if (type === 'dynamic') endpoint = `/api/items/${dynamicCategoryId}/${id}`;
try {
const res = await fetch(endpoint);
const data = await res.json();
let content = '';
if (data.raw_text) {
content = `【原始文本】\n${data.raw_text}\n\n【解析数据】\n${JSON.stringify(data, null, 2)}`;
} else {
content = JSON.stringify(data, null, 2);
}
document.getElementById('rawDataText').textContent = content;
document.getElementById('rawDataModal').classList.remove('hidden');
} catch (e) {
alert('获取数据失败');
}
}
function closeRawDataModal() {
document.getElementById('rawDataModal').classList.add('hidden');
}
// ============ 智能补充功能 ============
let smartUpdateImages = [];
// 打开智能补充弹框(编辑时显示按钮)
function showSmartUpdateButton() {
const btn = document.getElementById('smartUpdateBtn');
// 只有编辑已有数据时才显示智能补充按钮排除category和knowledge
if (currentId && currentType && currentType !== 'category' && currentType !== 'knowledge') {
btn.classList.remove('hidden');
} else {
btn.classList.add('hidden');
}
}
// 打开智能补充弹框
function openSmartUpdateModal() {
smartUpdateImages = [];
document.getElementById('smartUpdateText').value = '';
document.getElementById('smartUpdatePreview').classList.add('hidden');
document.getElementById('smartUpdateImagePreviewArea').innerHTML = '';
document.getElementById('smartUpdateImageCount').textContent = '0';
document.getElementById('smartUpdateModal').classList.remove('hidden');
}
function closeSmartUpdateModal() {
document.getElementById('smartUpdateModal').classList.add('hidden');
}
// 处理图片上传
async function handleSmartUpdateImageUpload(event) {
const files = event.target.files;
for (let file of files) {
const formData = new FormData();
formData.append('file', file);
try {
const res = await fetch('/api/upload/image', {
method: 'POST',
body: formData
});
const data = await res.json();
if (data.success) {
smartUpdateImages.push(data.url);
updateSmartUpdateImagePreview();
}
} catch (e) {
alert('上传失败: ' + e.message);
}
}
event.target.value = '';
}
// 粘贴图片
async function pasteSmartUpdateImageFromClipboard() {
try {
if (!navigator.clipboard || !navigator.clipboard.read) {
alert('剪贴板API需要HTTPS或localhost环境。\n当前访问地址不支持请使用文件选择上传。\n\n可改用 localhost:19010 访问来支持粘贴功能。');
return;
}
const clipboardItems = await navigator.clipboard.read();
let found = false;
for (const item of clipboardItems) {
for (const type of item.types) {
if (type.startsWith('image/')) {
found = true;
const blob = await item.getType(type);
const reader = new FileReader();
reader.onload = async (e) => {
const base64 = e.target.result;
try {
const res = await fetch('/api/upload/image/base64', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ image: base64 })
});
const data = await res.json();
if (data.success) {
smartUpdateImages.push(data.url);
updateSmartUpdateImagePreview();
}
} catch (err) {
alert('上传失败: ' + err.message);
}
};
reader.readAsDataURL(blob);
}
}
}
if (!found) {
alert('剪贴板中没有图片,请先复制一张图片');
}
} catch (e) {
if (e.name === 'NotAllowedError') {
alert('浏览器拒绝访问剪贴板。\n请使用文件选择上传或改用 localhost:19010 访问。');
} else {
alert('无法从剪贴板获取图片: ' + e.message + '\n请使用文件选择上传');
}
}
}
// 清空图片
function clearSmartUpdateImages() {
smartUpdateImages = [];
updateSmartUpdateImagePreview();
}
// 移除单张图片
function removeSmartUpdateImage(index) {
smartUpdateImages.splice(index, 1);
updateSmartUpdateImagePreview();
}
// 更新图片预览
function updateSmartUpdateImagePreview() {
const area = document.getElementById('smartUpdateImagePreviewArea');
const count = document.getElementById('smartUpdateImageCount');
area.innerHTML = smartUpdateImages.map((url, idx) => `
<div class="relative w-24 h-24 border rounded-lg overflow-hidden group">
<img src="${url}" class="w-full h-full object-cover">
<button onclick="removeSmartUpdateImage(${idx})" class="absolute top-1 right-1 w-6 h-6 bg-red-500 text-white rounded-full opacity-0 group-hover:opacity-100 transition flex items-center justify-center">
<i class="ri-close-line"></i>
</button>
</div>
`).join('');
count.textContent = smartUpdateImages.length;
}
// 执行智能补充
async function smartUpdateSubmit() {
const text = document.getElementById('smartUpdateText').value.trim();
if (!text && smartUpdateImages.length === 0) {
alert('请上传图片或输入文本');
return;
}
const btn = document.getElementById('smartUpdateSubmitBtn');
btn.disabled = true;
btn.innerHTML = '<i class="ri-loader-4-line animate-spin mr-1"></i>解析中...';
try {
let endpoint;
if (currentType === 'model') endpoint = `/api/models/${currentId}/smart-update`;
else if (currentType === 'gpu') endpoint = `/api/gpus/${currentId}/smart-update`;
else if (currentType === 'cpu') endpoint = `/api/cpus/${currentId}/smart-update`;
else if (currentType === 'dynamic') endpoint = `/api/items/${dynamicCategoryId}/${currentId}/smart-update`;
const res = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: text,
images: smartUpdateImages
})
});
const data = await res.json();
if (data.error) {
alert('解析失败: ' + data.error);
} else {
// 显示更新结果
document.getElementById('smartUpdatePreview').classList.remove('hidden');
let html = `<div class="mb-2 text-green-600"><i class="ri-checkbox-circle-fill mr-1"></i>成功补充 ${data.updated_fields.length} 个字段</div>`;
if (data.updated_fields.length > 0) {
html += '<div class="mt-2">';
data.updated_fields.forEach(field => {
html += `<span class="inline-block px-2 py-1 bg-green-100 text-green-700 rounded text-xs mr-1 mb-1">${field}</span>`;
});
html += '</div>';
} else {
html += '<div class="text-gray-500 mt-2">所有字段都已存在,无需补充</div>';
}
document.getElementById('smartUpdateResult').innerHTML = html;
// 更新编辑表单数据
currentData = data[currentType] || data.item || data.model || data.gpu || data.cpu;
const forms = {model: getModelForm, gpu: getGpuForm, cpu: getCpuForm, dynamic: getDynamicForm};
document.getElementById('modalContent').innerHTML = forms[currentType](currentData);
// 关闭智能补充弹框
setTimeout(() => {
closeSmartUpdateModal();
}, 1500);
}
} catch (e) {
alert('请求失败: ' + e.message);
}
btn.disabled = false;
btn.innerHTML = '<i class="ri-magic-line mr-1"></i>解析并补充';
}
// 监听编辑弹框打开,显示智能补充按钮
document.getElementById('editModal').addEventListener('showSmartUpdate', showSmartUpdateButton);
init();
</script>
</body>
</html>