|
| 1 | +import { GetServerSideProps, NextPage } from 'next'; |
| 2 | +import { useRouter } from 'next/router'; |
| 3 | +import { Trans, useTranslation } from 'next-i18next'; |
| 4 | +import { serverSideTranslations } from 'next-i18next/serverSideTranslations'; |
| 5 | +import { useMemo } from 'react'; |
| 6 | + |
| 7 | +import Loading from '@/components/loading/Loading'; |
| 8 | +import Navbar from '@/components/navbar/Navbar'; |
| 9 | +import { |
| 10 | + getMonthName, |
| 11 | + RankSearchBar, |
| 12 | + RankTable, |
| 13 | +} from '@/components/rankTable/RankTable'; |
| 14 | +import { RatingWithTrendRender } from '@/components/report/Report'; |
| 15 | +import Seo from '@/components/Seo'; |
| 16 | + |
| 17 | +import { getLMArenaRank } from '@/services/rank'; |
| 18 | +import { getClientIP } from '@/utils/util'; |
| 19 | + |
| 20 | +import { LMArenaRankPageProps } from '@/types/rank'; |
| 21 | + |
| 22 | +// 类别名称映射 |
| 23 | +const getCategoryName = (category: string, lang: string) => { |
| 24 | + const categoryMap: Record<string, { en: string; zh: string }> = { |
| 25 | + text: { en: 'Text', zh: '文本' }, |
| 26 | + webdev: { en: 'WebDev', zh: '网页开发' }, |
| 27 | + vision: { en: 'Vision', zh: '视觉' }, |
| 28 | + 'text-to-image': { en: 'Text to Image', zh: '文生图' }, |
| 29 | + 'text-to-video': { en: 'Text to Video', zh: '文生视频' }, |
| 30 | + }; |
| 31 | + return categoryMap[category]?.[lang === 'en' ? 'en' : 'zh'] || category; |
| 32 | +}; |
| 33 | + |
| 34 | +const LMRankPage: NextPage<LMArenaRankPageProps> = ({ |
| 35 | + year, |
| 36 | + month, |
| 37 | + monthList, |
| 38 | + category, |
| 39 | + categoryList, |
| 40 | + list, |
| 41 | +}) => { |
| 42 | + const { t, i18n } = useTranslation('rank'); |
| 43 | + const router = useRouter(); |
| 44 | + |
| 45 | + const onSearch = (key: string, value: string) => { |
| 46 | + const currentQuery = { ...router.query }; |
| 47 | + if (key === 'month') { |
| 48 | + currentQuery.month = value; |
| 49 | + } |
| 50 | + if (key === 'target') { |
| 51 | + router.push(`${value}`); |
| 52 | + return; |
| 53 | + } |
| 54 | + router.push({ |
| 55 | + pathname: '/report/lm-rank', |
| 56 | + query: currentQuery, |
| 57 | + }); |
| 58 | + }; |
| 59 | + |
| 60 | + const onCategoryChange = (opt: { key: string; value: string }) => { |
| 61 | + const currentQuery = { ...router.query }; |
| 62 | + currentQuery.category = opt.key; |
| 63 | + router.push({ |
| 64 | + pathname: '/report/lm-rank', |
| 65 | + query: currentQuery, |
| 66 | + }); |
| 67 | + }; |
| 68 | + |
| 69 | + // 类别选项 |
| 70 | + const categoryOptions = useMemo(() => { |
| 71 | + return categoryList?.map((cat) => ({ |
| 72 | + key: cat, |
| 73 | + value: getCategoryName(cat, i18n.language), |
| 74 | + })); |
| 75 | + }, [categoryList, i18n.language]); |
| 76 | + |
| 77 | + // 根据最长模型名称计算宽度 |
| 78 | + const maxNameWidth = useMemo(() => { |
| 79 | + if (!list?.length) return 200; |
| 80 | + const maxLen = Math.max(...list.map((item) => item.name?.length || 0)); |
| 81 | + // 每个字符约 8px,最小 120px,最大 360px |
| 82 | + return Math.min(Math.max(maxLen * 8, 120), 380); |
| 83 | + }, [list, category]); |
| 84 | + |
| 85 | + // 表格列配置 |
| 86 | + const columns: any[] = useMemo( |
| 87 | + () => [ |
| 88 | + { key: 'position', title: t('lmrank.thead.position'), width: 80 }, |
| 89 | + { key: 'name', title: t('lmrank.thead.name'), width: maxNameWidth }, |
| 90 | + { |
| 91 | + key: 'rating', |
| 92 | + title: t('lmrank.thead.rating'), |
| 93 | + render: RatingWithTrendRender, |
| 94 | + width: 120, |
| 95 | + }, |
| 96 | + { |
| 97 | + key: 'organization', |
| 98 | + title: t('lmrank.thead.organization'), |
| 99 | + width: 140, |
| 100 | + }, |
| 101 | + ], |
| 102 | + [i18n.language, maxNameWidth] |
| 103 | + ); |
| 104 | + |
| 105 | + // 移动端列配置(分数和趋势合并为一列) |
| 106 | + const md_columns: any[] = useMemo( |
| 107 | + () => [ |
| 108 | + { key: 'position', title: t('lmrank.thead.position'), width: 50 }, |
| 109 | + { key: 'name', title: t('lmrank.thead.name') }, |
| 110 | + { |
| 111 | + key: 'rating', |
| 112 | + title: t('lmrank.thead.rating'), |
| 113 | + render: RatingWithTrendRender, |
| 114 | + width: 80, |
| 115 | + }, |
| 116 | + ], |
| 117 | + [i18n.language] |
| 118 | + ); |
| 119 | + |
| 120 | + return ( |
| 121 | + <> |
| 122 | + <Seo title={t('lmrank.title')} /> |
| 123 | + {list ? ( |
| 124 | + <div> |
| 125 | + <Navbar |
| 126 | + middleText={t('lmrank.nav', { |
| 127 | + year: year, |
| 128 | + month: getMonthName(month, i18n.language, { forceEnglish: true }), |
| 129 | + })} |
| 130 | + /> |
| 131 | + <div className='my-2 bg-white px-2 pt-2 dark:bg-gray-800 md:rounded-lg'> |
| 132 | + <RankSearchBar |
| 133 | + title='LMArena' |
| 134 | + logo='https://img.hellogithub.com/logo/lmarena.png!small' |
| 135 | + i18n_lang={i18n.language} |
| 136 | + monthList={monthList} |
| 137 | + onChange={onSearch} |
| 138 | + /> |
| 139 | + <div className='mb-2 flex items-center justify-center'> |
| 140 | + <div className='flex flex-wrap items-center gap-1 md:gap-4'> |
| 141 | + {categoryOptions?.map((opt) => ( |
| 142 | + <button |
| 143 | + key={opt.key} |
| 144 | + onClick={() => onCategoryChange(opt)} |
| 145 | + className={`rounded-md px-2 py-1 text-xs font-medium transition-colors md:px-3 md:py-1.5 md:text-sm ${ |
| 146 | + category === opt.key |
| 147 | + ? 'bg-blue-500 text-white' |
| 148 | + : 'bg-gray-100 text-gray-600 hover:bg-gray-200 dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600' |
| 149 | + }`} |
| 150 | + > |
| 151 | + {opt.value} |
| 152 | + </button> |
| 153 | + ))} |
| 154 | + </div> |
| 155 | + </div> |
| 156 | + <div className='md:hidden'> |
| 157 | + <RankTable |
| 158 | + key={`mobile-${category}`} |
| 159 | + columns={md_columns} |
| 160 | + list={list} |
| 161 | + i18n_lang={i18n.language} |
| 162 | + /> |
| 163 | + </div> |
| 164 | + <div className='hidden md:block'> |
| 165 | + <RankTable |
| 166 | + key={`desktop-${category}`} |
| 167 | + columns={columns} |
| 168 | + list={list} |
| 169 | + i18n_lang={i18n.language} |
| 170 | + /> |
| 171 | + </div> |
| 172 | + <div className='mt-2 rounded-lg border bg-white p-2 text-sm dark:border-gray-700 dark:bg-gray-800 dark:text-gray-300'> |
| 173 | + <div className='whitespace-pre-wrap leading-8'> |
| 174 | + <p> |
| 175 | + <Trans ns='rank' i18nKey='lmrank.p_text' /> |
| 176 | + </p> |
| 177 | + </div> |
| 178 | + </div> |
| 179 | + <div className='h-2' /> |
| 180 | + </div> |
| 181 | + </div> |
| 182 | + ) : ( |
| 183 | + <Loading /> |
| 184 | + )} |
| 185 | + </> |
| 186 | + ); |
| 187 | +}; |
| 188 | + |
| 189 | +export const getServerSideProps: GetServerSideProps = async ({ |
| 190 | + query, |
| 191 | + req, |
| 192 | + locale, |
| 193 | +}) => { |
| 194 | + const ip = getClientIP(req); |
| 195 | + const data = await getLMArenaRank( |
| 196 | + ip, |
| 197 | + query['month'] as unknown as number, |
| 198 | + query['category'] as string |
| 199 | + ); |
| 200 | + if (!data.success) { |
| 201 | + return { |
| 202 | + notFound: true, |
| 203 | + }; |
| 204 | + } else { |
| 205 | + return { |
| 206 | + props: { |
| 207 | + year: data.year, |
| 208 | + month: data.month, |
| 209 | + list: data.data, |
| 210 | + monthList: data.month_list, |
| 211 | + category: data.category, |
| 212 | + categoryList: data.category_list, |
| 213 | + ...(await serverSideTranslations(locale as string, ['common', 'rank'])), |
| 214 | + }, |
| 215 | + }; |
| 216 | + } |
| 217 | +}; |
| 218 | + |
| 219 | +export default LMRankPage; |
0 commit comments