index.tsx 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534
  1. import { useAjax } from '@/Hook/useAjax';
  2. import React, { useEffect, useState } from 'react';
  3. import { getCorpExternalUserRepeatListApi, getExternalUserRepeatByCorpListApi, getExternalUserRepeatCorpApi, getExternalUserRepeatCorpUserApi } from '../../API/home';
  4. import { Avatar, Card, Col, Flex, Input, Row, Spin, Statistic, Table, Tabs, Typography } from 'antd';
  5. import { BarChartOutlined, GlobalOutlined, RetweetOutlined, UserOutlined } from '@ant-design/icons';
  6. import useEcharts from '@/Hook/useEcharts';
  7. const { Title } = Typography;
  8. import style from './index.less'
  9. const Home: React.FC = () => {
  10. /*******************************************/
  11. const { Bar, Pie } = useEcharts()
  12. const [queryParmas, setQueryParmas] = useState<{ pageNum: number, pageSize: number, corpName?: string }>({ pageNum: 1, pageSize: 20 })
  13. const [queryParmasZt, setQueryParmasZt] = useState<{ pageNum: number, pageSize: number, corpName?: string }>({ pageNum: 1, pageSize: 20 })
  14. const [corpRepeat, setCorpRepeat] = useState<{ [x: string]: any }>({})
  15. const [corpUserRepeat, setCorpUserRepeat] = useState<{ [x: string]: any }>({})
  16. const [barCorpData, setBarCorpData] = useState<{ name: string, value: number }[]>([])
  17. const [barCorpUserData, setBarCorpUserData] = useState<{ name: string, value: number }[]>([])
  18. const [activeKey, setActiveKey] = useState<string>('1')
  19. const [pieData, setPieData] = useState<{ name: string, value: number }[]>([])
  20. const [overflowData, setOverflowData] = useState<{ avgCorpRepeatUserRate: number, repeatUserRate: number, userCount: number }>({ avgCorpRepeatUserRate: 0, repeatUserRate: 0, userCount: 0 })
  21. const getExternalUserRepeatCorp = useAjax(() => getExternalUserRepeatCorpApi())
  22. const getExternalUserRepeatCorpUser = useAjax(() => getExternalUserRepeatCorpUserApi())
  23. const getExternalUserRepeatByCorpList = useAjax((params) => getExternalUserRepeatByCorpListApi(params))
  24. const getCorpExternalUserRepeatList = useAjax((params) => getCorpExternalUserRepeatListApi(params))
  25. /*******************************************/
  26. useEffect(() => {
  27. getExternalUserRepeatByCorpList.run(queryParmas).then(res => {
  28. if (res?.data?.records?.length) {
  29. setPieData(res?.data?.records?.map(item => {
  30. return { name: item.corpName, value: item.externalUserRepeatCount }
  31. }))
  32. } else {
  33. setPieData([])
  34. }
  35. })
  36. }, [queryParmas])
  37. useEffect(() => {
  38. getCorpExternalUserRepeatList.run(queryParmasZt)
  39. }, [queryParmasZt])
  40. useEffect(() => {
  41. getExternalUserRepeatCorp.run().then(res => {
  42. if (res?.data) {
  43. const cr = res.data
  44. setCorpRepeat(cr)
  45. setBarCorpData([
  46. { name: '非重复添加', value: cr?.oneRepeatCount },
  47. { name: '重复添加2名', value: cr?.twoRepeatCount },
  48. { name: '重复添加3名', value: cr?.threeRepeatCount },
  49. { name: '重复添加4名', value: cr?.fourRepeatCount },
  50. { name: '重复添加5名', value: cr?.fiveRepeatCount },
  51. { name: '重复添加5名以上', value: cr?.gtFiveRepeatCount }
  52. ])
  53. } else {
  54. setCorpRepeat({})
  55. setBarCorpData([])
  56. }
  57. })
  58. getExternalUserRepeatCorpUser.run().then(res => {
  59. if (res?.data) {
  60. const cur = res.data
  61. setCorpUserRepeat(cur)
  62. setOverflowData({ avgCorpRepeatUserRate: cur?.avgCorpRepeatUserRate || 0, repeatUserRate: cur?.repeatUserRate || 0, userCount: cur?.userCount || 0 })
  63. setBarCorpUserData([
  64. { name: '非重复添加', value: cur?.oneRepeatCount },
  65. { name: '重复添加2名', value: cur?.twoRepeatCount },
  66. { name: '重复添加3名', value: cur?.threeRepeatCount },
  67. { name: '重复添加4名', value: cur?.fourRepeatCount },
  68. { name: '重复添加5名', value: cur?.fiveRepeatCount },
  69. { name: '重复添加5名以上', value: cur?.gtFiveRepeatCount }
  70. ])
  71. } else {
  72. setCorpUserRepeat({})
  73. setOverflowData({ avgCorpRepeatUserRate: 0, repeatUserRate: 0, userCount: 0 })
  74. setBarCorpUserData([])
  75. }
  76. })
  77. }, [])
  78. return <div>
  79. <Spin spinning={getExternalUserRepeatCorpUser.loading}>
  80. <Row gutter={16}>
  81. <Col span={8}>
  82. <Card variant="borderless">
  83. <Flex justify='space-between'>
  84. <Statistic
  85. title={<strong style={{ fontSize: 14 }}>集团总粉丝数</strong>}
  86. value={overflowData.userCount}
  87. />
  88. <Avatar style={{ backgroundColor: '#DBEAFE', color: '#2563eb' }} size={40}><UserOutlined /></Avatar>
  89. </Flex>
  90. </Card>
  91. </Col>
  92. <Col span={8}>
  93. <Card variant="borderless">
  94. <Flex justify='space-between'>
  95. <Statistic
  96. title={<strong style={{ fontSize: 14 }}>平均主体重粉率</strong>}
  97. value={overflowData.avgCorpRepeatUserRate ? overflowData.avgCorpRepeatUserRate * 100 : 0}
  98. precision={4}
  99. suffix="%"
  100. />
  101. <Avatar style={{ backgroundColor: '#DCFCE7', color: '#16a34a' }} size={40}><RetweetOutlined /></Avatar>
  102. </Flex>
  103. </Card>
  104. </Col>
  105. <Col span={8}>
  106. <Card variant="borderless">
  107. <Flex justify='space-between'>
  108. <Statistic
  109. title={<strong style={{ fontSize: 14 }}>集团重粉率</strong>}
  110. value={overflowData.repeatUserRate ? overflowData.repeatUserRate * 100 : 0}
  111. precision={4}
  112. suffix="%"
  113. />
  114. <Avatar style={{ backgroundColor: '#F3E8FF', color: '#9333ea' }} size={40}><GlobalOutlined /></Avatar>
  115. </Flex>
  116. </Card>
  117. </Col>
  118. </Row>
  119. </Spin>
  120. <Spin spinning={getExternalUserRepeatByCorpList.loading}>
  121. <Flex justify='space-between' style={{ margin: '20px 0 10px' }}>
  122. <Title level={3} style={{ margin: 0 }}><RetweetOutlined style={{ color: '#1890ff' }} /> 主体重粉次数统计</Title>
  123. <Input.Search
  124. placeholder="请输入企业名称"
  125. onSearch={(e) => { setQueryParmas({ ...queryParmas, corpName: e, pageNum: 1 }); }}
  126. style={{ width: 200 }}
  127. allowClear
  128. />
  129. </Flex>
  130. <Row gutter={16}>
  131. <Col span={12}>
  132. <Card style={{ height: '100%' }}>
  133. <Pie data={pieData} title="主体重粉占比" />
  134. </Card>
  135. </Col>
  136. <Col span={12}>
  137. <Card style={{ height: '100%' }}>
  138. <Table
  139. columns={[
  140. {
  141. title: '企业名称',
  142. dataIndex: 'corpName',
  143. key: 'corpName',
  144. ellipsis: true,
  145. width: 150,
  146. },
  147. {
  148. title: '集团粉丝总数',
  149. dataIndex: 'totalExternalUserCount',
  150. key: 'totalExternalUserCount',
  151. align: 'center',
  152. render: (text: any) => <Statistic value={text || 0} valueStyle={{ fontSize: 12 }} />
  153. },
  154. {
  155. title: '集团内重粉数',
  156. dataIndex: 'externalUserRepeatCount',
  157. key: 'externalUserRepeatCount',
  158. align: 'center',
  159. render: (text: any) => <Statistic value={text || 0} valueStyle={{ fontSize: 12 }} />
  160. },
  161. {
  162. title: '集团内重粉率',
  163. dataIndex: 'externalUserRepeatRate',
  164. key: 'externalUserRepeatRate',
  165. align: 'center',
  166. render: (text: any) => <Statistic
  167. value={text ? text * 100 : 0}
  168. valueStyle={text > 0.2 ? { color: '#cf1322', fontSize: 12 } : { color: '#3f8600', fontSize: 12 }}
  169. suffix="%"
  170. precision={4}
  171. />
  172. },
  173. {
  174. title: '主体粉丝总数',
  175. dataIndex: 'corpExternalUserCount',
  176. key: 'corpExternalUserCount',
  177. align: 'center',
  178. render: (text: any) => <Statistic value={text || 0} valueStyle={{ fontSize: 12 }} />
  179. },
  180. {
  181. title: '主体粉丝在集团占比',
  182. dataIndex: 'corpExternalUserRate',
  183. key: 'corpExternalUserRate',
  184. align: 'center',
  185. render: (text: any) => <Statistic
  186. value={text ? text * 100 : 0}
  187. valueStyle={text > 0.2 ? { color: '#cf1322', fontSize: 12 } : { color: '#3f8600', fontSize: 12 }}
  188. suffix="%"
  189. precision={4}
  190. />
  191. },
  192. {
  193. title: '主体客服号数量',
  194. dataIndex: 'corpUserCount',
  195. key: 'corpUserCount',
  196. align: 'center',
  197. render: (text: any) => <Statistic value={text || 0} valueStyle={{ fontSize: 12 }} />
  198. },
  199. {
  200. title: '主体内重粉数',
  201. dataIndex: 'corpExternalUserRepeatCount',
  202. key: 'corpExternalUserRepeatCount',
  203. align: 'center',
  204. render: (text: any) => <Statistic value={text || 0} valueStyle={{ fontSize: 12 }} />
  205. },
  206. {
  207. title: '主体内重粉率',
  208. dataIndex: 'corpExternalUserRepeatRate',
  209. key: 'corpExternalUserRepeatRate',
  210. align: 'center',
  211. render: (text: any) => <Statistic
  212. value={text ? text * 100 : 0}
  213. valueStyle={text > 0.2 ? { color: '#cf1322', fontSize: 12 } : { color: '#3f8600', fontSize: 12 }}
  214. suffix="%"
  215. precision={4}
  216. />
  217. },
  218. ]}
  219. scroll={{ y: 300, x: 900 }}
  220. bordered
  221. dataSource={getExternalUserRepeatByCorpList.data?.data?.records}
  222. loading={getExternalUserRepeatByCorpList.loading}
  223. rowKey="corpId"
  224. pagination={{
  225. total: getExternalUserRepeatByCorpList.data?.data?.total,
  226. current: getExternalUserRepeatByCorpList?.data?.data?.current || 1,
  227. pageSize: getExternalUserRepeatByCorpList?.data?.data?.size || 20,
  228. onChange: (page: number, pageSize: number) => {
  229. setQueryParmas({ ...queryParmas, pageNum: page, pageSize })
  230. }
  231. }}
  232. />
  233. </Card>
  234. </Col>
  235. </Row>
  236. </Spin>
  237. <Spin spinning={getExternalUserRepeatCorp.loading || getExternalUserRepeatCorpUser.loading || getCorpExternalUserRepeatList.loading}>
  238. <Title level={3}><BarChartOutlined style={{ color: '#22c55e' }} /> 用户重粉次数统计</Title>
  239. <Tabs
  240. tabBarExtraContent={activeKey === '1' && <Input.Search
  241. placeholder="请输入企业名称"
  242. onSearch={(e) => { setQueryParmasZt({ ...queryParmasZt, corpName: e, pageNum: 1 }); }}
  243. style={{ width: 200 }}
  244. allowClear
  245. />}
  246. items={[
  247. {
  248. key: '1',
  249. label: '主体维度',
  250. children: <Card>
  251. <Table
  252. columns={[
  253. {
  254. title: '企业名称',
  255. dataIndex: 'corpName',
  256. key: 'corpName',
  257. ellipsis: true,
  258. width: 150,
  259. },
  260. {
  261. title: '粉丝总数',
  262. dataIndex: 'userCount',
  263. key: 'userCount',
  264. align: 'center',
  265. render: (text: any) => <Statistic value={text || 0} valueStyle={{ fontSize: 12 }} />
  266. },
  267. {
  268. title: '非重复添加人数',
  269. dataIndex: 'oneRepeatCount',
  270. key: 'oneRepeatCount',
  271. align: 'center',
  272. render: (text: any) => <Statistic value={text || 0} valueStyle={{ fontSize: 12 }} />
  273. },
  274. {
  275. title: '非重复添加人数比例',
  276. dataIndex: 'oneRepeatCountRate',
  277. key: 'oneRepeatCountRate',
  278. align: 'center',
  279. render: (text: any) => <Statistic
  280. value={text ? text * 100 : 0}
  281. valueStyle={text > 0.5 ? { color: '#cf1322', fontSize: 12 } : { color: '#3f8600', fontSize: 12 }}
  282. suffix="%"
  283. precision={4}
  284. />
  285. },
  286. {
  287. title: '重复添加2名人数',
  288. dataIndex: 'twoRepeatCount',
  289. key: 'twoRepeatCount',
  290. align: 'center',
  291. render: (text: any) => <Statistic value={text || 0} valueStyle={{ fontSize: 12 }} />
  292. },
  293. {
  294. title: '重复添加2名人数比例',
  295. dataIndex: 'twoRepeatCountRate',
  296. key: 'twoRepeatCountRate',
  297. align: 'center',
  298. render: (text: any) => <Statistic
  299. value={text ? text * 100 : 0}
  300. valueStyle={text > 0.1 ? { color: '#cf1322', fontSize: 12 } : { color: '#3f8600', fontSize: 12 }}
  301. suffix="%"
  302. precision={4}
  303. />
  304. },
  305. {
  306. title: '重复添加3名人数',
  307. dataIndex: 'threeRepeatCount',
  308. key: 'threeRepeatCount',
  309. align: 'center',
  310. render: (text: any) => <Statistic value={text || 0} valueStyle={{ fontSize: 12 }} />
  311. },
  312. {
  313. title: '重复添加3名人数比例',
  314. dataIndex: 'threeRepeatCountRate',
  315. key: 'threeRepeatCountRate',
  316. align: 'center',
  317. render: (text: any) => <Statistic
  318. value={text ? text * 100 : 0}
  319. valueStyle={text > 0.09 ? { color: '#cf1322', fontSize: 12 } : { color: '#3f8600', fontSize: 12 }}
  320. suffix="%"
  321. precision={4}
  322. />
  323. },
  324. {
  325. title: '重复添加4名人数',
  326. dataIndex: 'fourRepeatCount',
  327. key: 'fourRepeatCount',
  328. align: 'center',
  329. render: (text: any) => <Statistic value={text || 0} valueStyle={{ fontSize: 12 }} />
  330. },
  331. {
  332. title: '重复添加4名人数比例',
  333. dataIndex: 'fourRepeatCountRate',
  334. key: 'fourRepeatCountRate',
  335. align: 'center',
  336. render: (text: any) => <Statistic
  337. value={text ? text * 100 : 0}
  338. valueStyle={text > 0.08 ? { color: '#cf1322', fontSize: 12 } : { color: '#3f8600', fontSize: 12 }}
  339. suffix="%"
  340. precision={4}
  341. />
  342. },
  343. {
  344. title: '重复添加5名人数',
  345. dataIndex: 'fiveRepeatCount',
  346. key: 'fiveRepeatCount',
  347. align: 'center',
  348. render: (text: any) => <Statistic value={text || 0} valueStyle={{ fontSize: 12 }} />
  349. },
  350. {
  351. title: '重复添加5名人数比例',
  352. dataIndex: 'fiveRepeatCountRate',
  353. key: 'fiveRepeatCountRate',
  354. align: 'center',
  355. render: (text: any) => <Statistic
  356. value={text ? text * 100 : 0}
  357. valueStyle={text > 0.07 ? { color: '#cf1322', fontSize: 12 } : { color: '#3f8600', fontSize: 12 }}
  358. suffix="%"
  359. precision={4}
  360. />
  361. },
  362. {
  363. title: '重复添加5名以上人数',
  364. dataIndex: 'gtFiveRepeatCount',
  365. key: 'gtFiveRepeatCount',
  366. align: 'center',
  367. render: (text: any) => <Statistic value={text || 0} valueStyle={{ fontSize: 12 }} />
  368. },
  369. {
  370. title: '重复添加5名以上人数比例',
  371. dataIndex: 'gtFiveRepeatCountRate',
  372. key: 'gtFiveRepeatCountRate',
  373. align: 'center',
  374. render: (text: any) => <Statistic
  375. value={text ? text * 100 : 0}
  376. valueStyle={text > 0.06 ? { color: '#cf1322', fontSize: 12 } : { color: '#3f8600', fontSize: 12 }}
  377. suffix="%"
  378. precision={4}
  379. />
  380. },
  381. ]}
  382. scroll={{ y: 300, x: 900 }}
  383. bordered
  384. dataSource={getCorpExternalUserRepeatList.data?.data?.records}
  385. loading={getCorpExternalUserRepeatList.loading}
  386. rowKey="corpId"
  387. pagination={{
  388. total: getCorpExternalUserRepeatList.data?.data?.total,
  389. current: getCorpExternalUserRepeatList?.data?.data?.current || 1,
  390. pageSize: getCorpExternalUserRepeatList?.data?.data?.size || 20,
  391. onChange: (page: number, pageSize: number) => {
  392. setQueryParmasZt({ ...queryParmasZt, pageNum: page, pageSize })
  393. }
  394. }}
  395. />
  396. </Card>
  397. },
  398. {
  399. key: '2',
  400. label: '企业维度',
  401. children: <Row gutter={16}>
  402. <Col span={12}>
  403. <Card style={{ height: '100%' }}>
  404. <Bar data={barCorpData} title="企业维度重粉次数分布" horizontal />
  405. </Card>
  406. </Col>
  407. <Col span={12}>
  408. <DetailsTemplate data={corpRepeat} title='企业维度重粉详细数据' />
  409. </Col>
  410. </Row>
  411. },
  412. {
  413. key: '3',
  414. label: '客服号维度',
  415. children: <Row gutter={16}>
  416. <Col span={12}>
  417. <Card style={{ height: '100%' }}>
  418. <Bar data={barCorpUserData} title="客服号维度重粉次数分布" horizontal />
  419. </Card>
  420. </Col>
  421. <Col span={12}>
  422. <DetailsTemplate data={corpUserRepeat} title='客服号维度重粉详细数据' />
  423. </Col>
  424. </Row>
  425. },
  426. ]}
  427. onChange={(e) => { setActiveKey(e) }}
  428. activeKey={activeKey}
  429. />
  430. </Spin>
  431. </div>
  432. };
  433. const DetailsTemplate: React.FC<{ data: { [x: string]: any }, title: string }> = ({ data, title }) => {
  434. return <Card style={{ height: '100%' }}>
  435. <Title level={3} style={{ marginTop: 0, textAlign: 'center', fontSize: 18, color: '#313131' }}>{title || '详细数据'}</Title>
  436. <Flex vertical gap={7}>
  437. <div className={style.item}>
  438. <span>粉丝总数</span>
  439. <div className={style.num}>
  440. <Statistic value={data?.userCount || 0} valueStyle={{ fontSize: 16 }} />
  441. </div>
  442. </div>
  443. <div className={style.item}>
  444. <span>非重复添加人数</span>
  445. <div className={style.num}>
  446. <Statistic value={data?.oneRepeatCount || 0} valueStyle={{ fontSize: 16 }} />
  447. (<Statistic
  448. value={data?.oneRepeatCountRate ? data?.oneRepeatCountRate * 100 : 0}
  449. valueStyle={data?.oneRepeatCountRate > 0.5 ? { color: '#cf1322', fontSize: 16 } : { color: '#3f8600', fontSize: 16 }}
  450. suffix="%"
  451. precision={4}
  452. />)
  453. </div>
  454. </div>
  455. <div className={style.item}>
  456. <span>重复添加2名人数</span>
  457. <div className={style.num}>
  458. <Statistic value={data?.twoRepeatCount || 0} valueStyle={{ fontSize: 16 }} />
  459. (<Statistic
  460. value={data?.twoRepeatCountRate ? data?.twoRepeatCountRate * 100 : 0}
  461. valueStyle={data?.twoRepeatCountRate > 0.1 ? { color: '#cf1322', fontSize: 16 } : { color: '#3f8600', fontSize: 16 }}
  462. suffix="%"
  463. precision={4}
  464. />)
  465. </div>
  466. </div>
  467. <div className={style.item}>
  468. <span>重复添加3名人数</span>
  469. <div className={style.num}>
  470. <Statistic value={data?.threeRepeatCount || 0} valueStyle={{ fontSize: 16 }} />
  471. (<Statistic
  472. value={data?.threeRepeatCountRate ? data?.threeRepeatCountRate * 100 : 0}
  473. valueStyle={data?.threeRepeatCountRate > 0.09 ? { color: '#cf1322', fontSize: 16 } : { color: '#3f8600', fontSize: 16 }}
  474. suffix="%"
  475. precision={4}
  476. />)
  477. </div>
  478. </div>
  479. <div className={style.item}>
  480. <span>重复添加4名人数</span>
  481. <div className={style.num}>
  482. <Statistic value={data?.fourRepeatCount || 0} valueStyle={{ fontSize: 16 }} />
  483. (<Statistic
  484. value={data?.fourRepeatCountRate ? data?.fourRepeatCountRate * 100 : 0}
  485. valueStyle={data?.fourRepeatCountRate > 0.08 ? { color: '#cf1322', fontSize: 16 } : { color: '#3f8600', fontSize: 16 }}
  486. suffix="%"
  487. precision={4}
  488. />)
  489. </div>
  490. </div>
  491. <div className={style.item}>
  492. <span>重复添加5名人数</span>
  493. <div className={style.num}>
  494. <Statistic value={data?.fiveRepeatCount || 0} valueStyle={{ fontSize: 16 }} />
  495. (<Statistic
  496. value={data?.fiveRepeatCountRate ? data?.fiveRepeatCountRate * 100 : 0}
  497. valueStyle={data?.fiveRepeatCountRate > 0.07 ? { color: '#cf1322', fontSize: 16 } : { color: '#3f8600', fontSize: 16 }}
  498. suffix="%"
  499. precision={4}
  500. />)
  501. </div>
  502. </div>
  503. <div className={style.item}>
  504. <span>重复添加5名以上人数</span>
  505. <div className={style.num}>
  506. <Statistic value={data?.gtFiveRepeatCount || 0} valueStyle={{ fontSize: 16 }} />
  507. (<Statistic
  508. value={data?.gtFiveRepeatCountRate ? data?.gtFiveRepeatCountRate * 100 : 0}
  509. valueStyle={data?.gtFiveRepeatCountRate > 0.06 ? { color: '#cf1322', fontSize: 16 } : { color: '#3f8600', fontSize: 16 }}
  510. suffix="%"
  511. precision={4}
  512. />)
  513. </div>
  514. </div>
  515. </Flex>
  516. </Card>
  517. };
  518. export default Home;