| | <!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> |