返回 AiToEarn
index.tsx
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
343 lines Plain Text