2054 lines
129 KiB
HTML
2054 lines
129 KiB
HTML
<!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> |