| <!DOCTYPE html> |
| <html lang="zh-CN"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>全自动 LLM 情报中心</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet"> |
| <style> |
| body { font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; } |
| .spin-slow { animation: spin 2s linear infinite; } |
| @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } |
| |
| th.sortable { cursor: pointer; user-select: none; transition: background-color 0.2s; } |
| th.sortable:hover { background-color: #e2e8f0; } |
| th.active-sort { background-color: #dbeafe; color: #2563eb; } |
| </style> |
| </head> |
| <body class="bg-slate-50 min-h-screen text-slate-800 flex flex-col"> |
|
|
| <div id="app" class="container mx-auto px-4 py-8 max-w-7xl flex-grow"> |
| |
| |
| <div class="flex flex-col md:flex-row justify-between items-center mb-8 gap-4"> |
| <div> |
| <h1 class="text-3xl font-extrabold text-slate-800 bg-clip-text text-transparent bg-gradient-to-r from-blue-600 to-purple-600"> |
| <i class="fas fa-sort-amount-down-alt text-blue-600 mr-2"></i>LLM 情报中心 |
| </h1> |
| <p class="text-slate-500 mt-1 text-sm">支持按上下文、价格排序 | 数据源: OpenRouter API</p> |
| </div> |
| |
| <div class="flex gap-3"> |
| <div class="relative"> |
| <i class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-400"></i> |
| <input v-model="searchQuery" type="text" placeholder="搜索模型名称/厂商..." |
| class="pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:outline-none shadow-sm w-64 transition"> |
| </div> |
| |
| <button @click="fetchData" :disabled="loading" |
| class="flex items-center px-4 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 transition shadow-md disabled:opacity-70 disabled:cursor-not-allowed"> |
| <i class="fas fa-sync-alt mr-2" :class="{'spin-slow': loading}"></i> |
| {{ loading ? '正在获取...' : '获取最新数据' }} |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div class="bg-white rounded-xl shadow-lg border border-slate-200 overflow-hidden"> |
| <div class="overflow-x-auto"> |
| <table class="w-full text-sm text-left"> |
| <thead class="text-xs text-slate-500 uppercase bg-slate-100 border-b border-slate-200"> |
| <tr> |
| <th class="px-6 py-4 font-bold whitespace-nowrap">模型名称 (ID)</th> |
| <th class="px-6 py-4 font-bold whitespace-nowrap">厂商</th> |
| <th class="px-6 py-4 font-bold whitespace-nowrap">类型</th> |
| |
| <th class="px-6 py-4 font-bold whitespace-nowrap">参数量</th> |
|
|
| <th @click="sortBy('context_length')" class="sortable px-6 py-4 font-bold whitespace-nowrap" :class="{'active-sort': currentSort === 'context_length'}"> |
| 上下文 |
| <i v-if="currentSort === 'context_length'" class="fas ml-1" :class="currentSortDir === 'asc' ? 'fa-arrow-up' : 'fa-arrow-down'"></i> |
| <i v-else class="fas fa-sort text-slate-300 ml-1"></i> |
| </th> |
|
|
| <th @click="sortBy('pricing_prompt')" class="sortable px-6 py-4 font-bold whitespace-nowrap" :class="{'active-sort': currentSort === 'pricing_prompt'}"> |
| 输入价格 |
| <i v-if="currentSort === 'pricing_prompt'" class="fas ml-1" :class="currentSortDir === 'asc' ? 'fa-arrow-up' : 'fa-arrow-down'"></i> |
| <i v-else class="fas fa-sort text-slate-300 ml-1"></i> |
| </th> |
|
|
| <th @click="sortBy('pricing_completion')" class="sortable px-6 py-4 font-bold whitespace-nowrap" :class="{'active-sort': currentSort === 'pricing_completion'}"> |
| 输出价格 |
| <i v-if="currentSort === 'pricing_completion'" class="fas ml-1" :class="currentSortDir === 'asc' ? 'fa-arrow-up' : 'fa-arrow-down'"></i> |
| <i v-else class="fas fa-sort text-slate-300 ml-1"></i> |
| </th> |
| </tr> |
| </thead> |
| <tbody class="divide-y divide-slate-100"> |
| <tr v-if="loading"> |
| <td colspan="7" class="px-6 py-12 text-center text-slate-500"> |
| <i class="fas fa-spinner fa-spin text-3xl mb-3 text-blue-500"></i> |
| <p>正在获取全球模型数据...</p> |
| </td> |
| </tr> |
| <tr v-else-if="sortedModels.length === 0"> |
| <td colspan="7" class="px-6 py-12 text-center text-slate-500"> |
| <i class="fas fa-inbox text-3xl mb-3 text-gray-300"></i> |
| <p>暂无数据,请点击右上角按钮获取。</p> |
| </td> |
| </tr> |
| <tr v-for="model in sortedModels" :key="model.id" class="hover:bg-blue-50 transition duration-150"> |
| <td class="px-6 py-4 min-w-[200px]"> |
| <div class="font-bold text-slate-800">{{ model.name }}</div> |
| <div class="text-xs text-slate-400 font-mono mt-1">{{ model.id }}</div> |
| </td> |
| <td class="px-6 py-4 whitespace-nowrap"> |
| {{ model.company }} |
| </td> |
| <td class="px-6 py-4"> |
| <span :class="getTypeBadgeClass(model.type)" class="px-2 py-1 rounded text-xs font-medium border whitespace-nowrap"> |
| {{ model.type }} |
| </span> |
| </td> |
| <td class="px-6 py-4 font-mono text-slate-600 whitespace-nowrap"> |
| {{ model.parameters }} |
| </td> |
| <td class="px-6 py-4 whitespace-nowrap"> |
| <div class="flex items-center"> |
| <span class="font-bold text-slate-700 mr-2 w-16 text-right">{{ formatContext(model.context_length) }}</span> |
| <div class="w-16 bg-gray-200 rounded-full h-1.5 hidden md:block"> |
| <div class="bg-blue-500 h-1.5 rounded-full" :style="{ width: Math.min((model.context_length / 200000) * 100, 100) + '%' }"></div> |
| </div> |
| </div> |
| </td> |
| <td class="px-6 py-4 font-mono text-xs text-slate-600 whitespace-nowrap"> |
| {{ formatPriceDisplay(model.pricing_prompt) }} |
| </td> |
| <td class="px-6 py-4 font-mono text-xs text-slate-600 whitespace-nowrap"> |
| {{ formatPriceDisplay(model.pricing_completion) }} |
| </td> |
| </tr> |
| </tbody> |
| </table> |
| </div> |
| <div class="px-6 py-3 bg-slate-50 border-t border-slate-200 text-xs text-slate-400"> |
| 共 {{ sortedModels.length }} 个模型。点击表头可排序。 |
| </div> |
| </div> |
|
|
| </div> |
|
|
| |
| <footer class="mt-8 py-4 text-center text-slate-500 text-sm"> |
| Powered by 飙猪狂 |
| </footer> |
|
|
| <script> |
| const { createApp, ref, computed } = Vue; |
| |
| createApp({ |
| setup() { |
| const models = ref([]); |
| const loading = ref(false); |
| const searchQuery = ref(''); |
| |
| const currentSort = ref('context_length'); |
| const currentSortDir = ref('desc'); |
| |
| const formatContext = (num) => { |
| if (!num) return '0'; |
| if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M'; |
| if (num >= 1000) return (num / 1000).toFixed(0) + 'k'; |
| return num; |
| }; |
| |
| const formatPriceDisplay = (val) => { |
| if (val === 0) return 'Free'; |
| return '$' + val.toFixed(2); |
| }; |
| |
| const sortBy = (key) => { |
| if (currentSort.value === key) { |
| currentSortDir.value = currentSortDir.value === 'asc' ? 'desc' : 'asc'; |
| } else { |
| currentSort.value = key; |
| currentSortDir.value = 'desc'; |
| } |
| }; |
| |
| const processModelData = (apiData) => { |
| return apiData.map(item => { |
| const idParts = item.id.split('/'); |
| const companyRaw = idParts[0]; |
| const nameRaw = item.name || idParts[1]; |
| |
| const companyMap = { |
| 'openai': 'OpenAI', 'anthropic': 'Anthropic', 'google': 'Google', |
| 'meta-llama': 'Meta', 'mistralai': 'Mistral AI', 'qwen': 'Alibaba Cloud', |
| 'deepseek': 'DeepSeek', 'microsoft': 'Microsoft' |
| }; |
| const company = companyMap[companyRaw] || companyRaw.charAt(0).toUpperCase() + companyRaw.slice(1); |
| |
| const paramMatch = nameRaw.match(/(\d+)[bB]/); |
| let parameters = paramMatch ? paramMatch[0].toUpperCase() : '-'; |
| if (nameRaw.includes('GPT-4')) parameters = 'Unknown'; |
| |
| let type = '文生文'; |
| const lowerName = nameRaw.toLowerCase(); |
| if (lowerName.includes('vision') || lowerName.includes('vl') || lowerName.includes('4o')) type = '多模态'; |
| else if (lowerName.includes('code')) type = '代码'; |
| |
| const p_prompt = parseFloat(item.pricing?.prompt || 0) * 1000000; |
| const p_compl = parseFloat(item.pricing?.completion || 0) * 1000000; |
| |
| return { |
| id: item.id, |
| name: item.name, |
| company: company, |
| context_length: Number(item.context_length || 0), |
| pricing_prompt: p_prompt, |
| pricing_completion: p_compl, |
| parameters: parameters, |
| type: type |
| }; |
| }); |
| }; |
| |
| const fetchData = async () => { |
| loading.value = true; |
| models.value = []; |
| try { |
| const response = await fetch('https://openrouter.ai/api/v1/models'); |
| const result = await response.json(); |
| if (result && result.data) { |
| models.value = processModelData(result.data); |
| } |
| } catch (error) { |
| console.error(error); |
| alert("获取失败,请重试"); |
| } finally { |
| loading.value = false; |
| } |
| }; |
| |
| const sortedModels = computed(() => { |
| let filtered = models.value; |
| if (searchQuery.value) { |
| const query = searchQuery.value.toLowerCase(); |
| filtered = filtered.filter(m => |
| m.name.toLowerCase().includes(query) || |
| m.company.toLowerCase().includes(query) |
| ); |
| } |
| |
| return filtered.sort((a, b) => { |
| let modifier = currentSortDir.value === 'desc' ? -1 : 1; |
| let valA = a[currentSort.value]; |
| let valB = b[currentSort.value]; |
| if (valA < valB) return -1 * modifier; |
| if (valA > valB) return 1 * modifier; |
| return 0; |
| }); |
| }); |
| |
| const getTypeBadgeClass = (type) => { |
| if (type.includes('多模态')) return 'bg-purple-100 text-purple-700 border-purple-200'; |
| if (type.includes('代码')) return 'bg-yellow-100 text-yellow-700 border-yellow-200'; |
| return 'bg-blue-50 text-blue-600 border-blue-200'; |
| }; |
| |
| return { |
| loading, |
| searchQuery, |
| fetchData, |
| sortedModels, |
| formatContext, |
| formatPriceDisplay, |
| getTypeBadgeClass, |
| sortBy, |
| currentSort, |
| currentSortDir |
| }; |
| } |
| }).mount('#app'); |
| </script> |
|
|
| </body> |
| </html> |