| 1 | import styles from './aiRanking.module.scss'; |
| 2 | import RankingTags from '../../components/RankingTags'; |
| 3 | import Cycleselects, { CycleTypeEnum } from '../../components/CycleSelects'; |
| 4 | import { useEffect, useMemo, useRef, useState } from 'react'; |
| 5 | import { platformApi } from '../../../../api/platform'; |
| 6 | import { |
| 7 | AiToolsRankingItemType, |
| 8 | GetAiToolsRankingApiParams, |
| 9 | } from '../../../../api/types/platform.type'; |
| 10 | import { Avatar, Popover, Table, TableProps, Tag, Tooltip } from 'antd'; |
| 11 | import { |
| 12 | CaretDownOutlined, |
| 13 | CaretUpOutlined, |
| 14 | QuestionCircleOutlined, |
| 15 | SearchOutlined, |
| 16 | } from '@ant-design/icons'; |
| 17 | import initWeepPie from './echarts-weekPie'; |
| 18 | |
| 19 | const optionsTags = [ |
| 20 | { |
| 21 | label: '总榜', |
| 22 | value: '1', |
| 23 | }, |
| 24 | { |
| 25 | label: '国内榜', |
| 26 | value: '2', |
| 27 | }, |
| 28 | { |
| 29 | label: '国外榜', |
| 30 | value: '3', |
| 31 | }, |
| 32 | ]; |
| 33 | |
| 34 | function parseChineseNumber(input: string): number { |
| 35 | if (!input) return 0; |
| 36 | const units: { [key: string]: number } = { |
| 37 | 千: 1000, |
| 38 | 万: 10000, |
| 39 | 亿: 100000000, |
| 40 | }; |
| 41 | |
| 42 | let result = 0; |
| 43 | let currentNumber = 0; |
| 44 | let decimalPart = 0; |
| 45 | let isDecimal = false; |
| 46 | let decimalScale = 0.1; |
| 47 | |
| 48 | for (const char of input) { |
| 49 | if (char === '.') { |
| 50 | isDecimal = true; // 标记进入小数部分 |
| 51 | continue; |
| 52 | } |
| 53 | |
| 54 | if (char in units) { |
| 55 | if (isDecimal) { |
| 56 | result += (currentNumber + decimalPart) * units[char]; |
| 57 | } else { |
| 58 | result += currentNumber * units[char]; |
| 59 | } |
| 60 | currentNumber = 0; |
| 61 | decimalPart = 0; |
| 62 | isDecimal = false; |
| 63 | decimalScale = 0.1; |
| 64 | } else { |
| 65 | const num = parseInt(char, 10); |
| 66 | if (!isNaN(num)) { |
| 67 | if (isDecimal) { |
| 68 | decimalPart += num * decimalScale; |
| 69 | decimalScale *= 0.1; |
| 70 | } else { |
| 71 | currentNumber = currentNumber * 10 + num; |
| 72 | } |
| 73 | } |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | // 如果最后还有剩余的数字或小数部分,直接加到结果里 |
| 78 | result += currentNumber + decimalPart; |
| 79 | |
| 80 | return result; |
| 81 | } |
| 82 | |
| 83 | export default function Page() { |
| 84 | const [params, setParams] = useState<GetAiToolsRankingApiParams>({ |
| 85 | dateType: '', |
| 86 | startDate: '', |
| 87 | area: '1', |
| 88 | }); |
| 89 | const [loading, setLoading] = useState(false); |
| 90 | const [data, setData] = useState<AiToolsRankingItemType[]>([]); |
| 91 | const aiRankingContentRef = useRef<HTMLDivElement>(null); |
| 92 | |
| 93 | useEffect(() => { |
| 94 | if (params.dateType === '' || params.startDate === '') return; |
| 95 | if (loading) return; |
| 96 | setLoading(true); |
| 97 | platformApi |
| 98 | .getAiToolsRankingApi(params) |
| 99 | .then((res) => { |
| 100 | setLoading(false); |
| 101 | setData(res.items); |
| 102 | document.querySelector('.ant-table-body')!.scrollTo(0, 0); |
| 103 | }) |
| 104 | .catch(() => setLoading(false)); |
| 105 | }, [params]); |
| 106 | |
| 107 | const columns = useMemo(() => { |
| 108 | const columns: TableProps<AiToolsRankingItemType>['columns'] = [ |
| 109 | { |
| 110 | title: () => <p style={{ textAlign: 'center' }}>#</p>, |
| 111 | render: (text, prm, ind) => { |
| 112 | return ( |
| 113 | <div className="aiRanking-rank"> |
| 114 | {ind <= 2 ? ( |
| 115 | <div className="aiRanking-topthree">{ind + 1}</div> |
| 116 | ) : ( |
| 117 | <span>{ind + 1}</span> |
| 118 | )} |
| 119 | {prm.rankChange > 0 && ( |
| 120 | <Tag className="aiRanking-rank-up" color="error"> |
| 121 | <CaretUpOutlined /> |
| 122 | {prm.rankChange} |
| 123 | </Tag> |
| 124 | )} |
| 125 | {prm.rankChange < 0 && ( |
| 126 | <Tag className="aiRanking-rank-down" color="success"> |
| 127 | <CaretDownOutlined /> |
| 128 | {Math.abs(prm.rankChange)} |
| 129 | </Tag> |
| 130 | )} |
| 131 | </div> |
| 132 | ); |
| 133 | }, |
| 134 | width: '40px', |
| 135 | key: '序号', |
| 136 | }, |
| 137 | { |
| 138 | title: '产品名', |
| 139 | render: (text, prm) => { |
| 140 | return ( |
| 141 | <div className="aiRanking-productName"> |
| 142 | <Avatar src={prm.cover} size="large" /> |
| 143 | <div className="aiRanking-productName-con"> |
| 144 | <label style={{ marginBottom: '4px', display: 'inline-block' }}> |
| 145 | {prm.title} |
| 146 | </label> |
| 147 | <div |
| 148 | className="aiRanking-productName-con-des" |
| 149 | title={prm.description} |
| 150 | > |
| 151 | {prm.description} |
| 152 | </div> |
| 153 | </div> |
| 154 | </div> |
| 155 | ); |
| 156 | }, |
| 157 | width: 250, |
| 158 | key: 'title', |
| 159 | }, |
| 160 | { |
| 161 | title: '标签', |
| 162 | render: (text, prm) => { |
| 163 | return ( |
| 164 | <div className="aiRanking-productName"> |
| 165 | <div className="aiRanking-productName-con" title={prm.intro}> |
| 166 | {prm.intro} |
| 167 | </div> |
| 168 | </div> |
| 169 | ); |
| 170 | }, |
| 171 | width: 200, |
| 172 | key: 'title', |
| 173 | }, |
| 174 | { |
| 175 | title: '月访问量', |
| 176 | render: (text, prm) => { |
| 177 | return prm.stats.viewCount; |
| 178 | }, |
| 179 | sorter: (a, b) => |
| 180 | parseChineseNumber(b.stats.viewCount) - |
| 181 | parseChineseNumber(a.stats.viewCount), |
| 182 | width: 80, |
| 183 | key: 'title', |
| 184 | }, |
| 185 | { |
| 186 | title: '月下载量', |
| 187 | render: (text, prm) => { |
| 188 | return prm.downCount; |
| 189 | }, |
| 190 | sorter: (a, b) => |
| 191 | parseChineseNumber(b.downCount) - parseChineseNumber(a.downCount), |
| 192 | width: 80, |
| 193 | key: '月下载量', |
| 194 | }, |
| 195 | { |
| 196 | title: () => { |
| 197 | return ( |
| 198 | <> |
| 199 | <Tooltip title="统计AI工具排行榜的周提及作品数"> |
| 200 | <QuestionCircleOutlined /> |
| 201 | </Tooltip> |
| 202 | 周提及量 |
| 203 | </> |
| 204 | ); |
| 205 | }, |
| 206 | sorter: (a, b) => b.referCount - a.referCount, |
| 207 | render: (text, prm, ind) => { |
| 208 | return ( |
| 209 | <> |
| 210 | {prm.referCount} |
| 211 | <Popover |
| 212 | content={() => { |
| 213 | return ( |
| 214 | <> |
| 215 | <div |
| 216 | className={`weekChat${ind}`} |
| 217 | style={{ width: '350px', height: '200px' }} |
| 218 | /> |
| 219 | </> |
| 220 | ); |
| 221 | }} |
| 222 | onOpenChange={() => { |
| 223 | setTimeout(() => { |
| 224 | initWeepPie( |
| 225 | document.querySelector(`.weekChat${ind}`)!, |
| 226 | Object.keys(prm.detail).map((key) => { |
| 227 | return { |
| 228 | name: key, |
| 229 | value: prm.detail[key as '抖音'], |
| 230 | }; |
| 231 | }), |
| 232 | ); |
| 233 | }, 10); |
| 234 | }} |
| 235 | > |
| 236 | <SearchOutlined /> |
| 237 | </Popover> |
| 238 | </> |
| 239 | ); |
| 240 | }, |
| 241 | width: 90, |
| 242 | key: 'referCount', |
| 243 | }, |
| 244 | { |
| 245 | title: () => { |
| 246 | return ( |
| 247 | <> |
| 248 | <Tooltip title="统计AI工具排行榜的提及作品、传播范围等多维度数据分析做出的综合评分"> |
| 249 | <QuestionCircleOutlined /> |
| 250 | </Tooltip> |
| 251 | 声望值 |
| 252 | </> |
| 253 | ); |
| 254 | }, |
| 255 | sorter: (a, b) => b.volumeCount - a.volumeCount, |
| 256 | render: (text, prm) => { |
| 257 | return prm.volumeCount; |
| 258 | }, |
| 259 | width: 80, |
| 260 | key: 'volumeCount', |
| 261 | }, |
| 262 | { |
| 263 | title: () => { |
| 264 | return ( |
| 265 | <> |
| 266 | <Tooltip title="统计AI工具排行榜的用户使用情况等多维度数据分析做出的综合评分"> |
| 267 | <QuestionCircleOutlined /> |
| 268 | </Tooltip> |
| 269 | 综合评分 |
| 270 | </> |
| 271 | ); |
| 272 | }, |
| 273 | sorter: (a, b) => b.exponentCount - a.exponentCount, |
| 274 | render: (text, prm) => { |
| 275 | return ( |
| 276 | <span style={{ color: 'var(--colorPrimary6)' }}> |
| 277 | {prm.exponentCount} |
| 278 | </span> |
| 279 | ); |
| 280 | }, |
| 281 | width: 90, |
| 282 | key: 'exponentCount', |
| 283 | }, |
| 284 | ]; |
| 285 | return columns; |
| 286 | }, []); |
| 287 | |
| 288 | return ( |
| 289 | <div className={styles.aiRanking}> |
| 290 | <div className="aiRanking-title"> |
| 291 | AI工具排行榜 · {optionsTags.find((v) => v.value === params.area)?.label}{' '} |
| 292 | ·{params.dateType === CycleTypeEnum.Week ? '周榜' : '月榜'} |
| 293 | </div> |
| 294 | <div className="aiRanking-head"> |
| 295 | <div className="aiRanking-head-item"> |
| 296 | <div className="aiRanking-head-title">类型</div> |
| 297 | <RankingTags |
| 298 | options={optionsTags} |
| 299 | defaultValue={optionsTags[0].value} |
| 300 | onChange={(val) => { |
| 301 | setParams((prevState) => { |
| 302 | const newState = { ...prevState }; |
| 303 | newState.area = val; |
| 304 | return newState; |
| 305 | }); |
| 306 | }} |
| 307 | /> |
| 308 | </div> |
| 309 | <div className="aiRanking-head-item"> |
| 310 | <div className="aiRanking-head-title">周期</div> |
| 311 | <div className="aiRanking-head-cycleselects"> |
| 312 | <Cycleselects |
| 313 | defaultType={CycleTypeEnum.Week} |
| 314 | onChange={(v) => { |
| 315 | setParams((prevState) => { |
| 316 | const newState = { ...prevState }; |
| 317 | newState.dateType = v.type; |
| 318 | newState.startDate = v.value; |
| 319 | return newState; |
| 320 | }); |
| 321 | }} |
| 322 | /> |
| 323 | </div> |
| 324 | </div> |
| 325 | </div> |
| 326 | |
| 327 | <div className="aiRanking-content" ref={aiRankingContentRef}> |
| 328 | <Table<AiToolsRankingItemType> |
| 329 | columns={columns} |
| 330 | dataSource={data} |
| 331 | rowKey="id" |
| 332 | loading={loading} |
| 333 | pagination={false} |
| 334 | scroll={{ |
| 335 | // 高度通过css flex设置为自适应 |
| 336 | y: 0, |
| 337 | }} |
| 338 | /> |
| 339 | </div> |
| 340 | </div> |
| 341 | ); |
| 342 | } |
| 343 |