index.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. import React, { useEffect, useRef, useState } from 'react';
  2. import { initVChartSemiTheme } from '@visactor/vchart-semi-theme';
  3. import { Button, Col, Form, Layout, Row, Spin } from '@douyinfe/semi-ui';
  4. import VChart from '@visactor/vchart';
  5. import {
  6. API,
  7. isAdmin,
  8. showError,
  9. timestamp2string,
  10. timestamp2string1,
  11. } from '../../helpers';
  12. import {
  13. getQuotaWithUnit,
  14. modelColorMap,
  15. renderNumber,
  16. renderQuota,
  17. renderQuotaNumberWithDigit,
  18. stringToColor,
  19. } from '../../helpers/render';
  20. const Detail = (props) => {
  21. const formRef = useRef();
  22. let now = new Date();
  23. const [inputs, setInputs] = useState({
  24. username: '',
  25. token_name: '',
  26. model_name: '',
  27. start_timestamp:
  28. localStorage.getItem('data_export_default_time') === 'hour'
  29. ? timestamp2string(now.getTime() / 1000 - 86400)
  30. : localStorage.getItem('data_export_default_time') === 'week'
  31. ? timestamp2string(now.getTime() / 1000 - 86400 * 30)
  32. : timestamp2string(now.getTime() / 1000 - 86400 * 7),
  33. end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
  34. channel: '',
  35. data_export_default_time: '',
  36. });
  37. const { username, model_name, start_timestamp, end_timestamp, channel } =
  38. inputs;
  39. const isAdminUser = isAdmin();
  40. const initialized = useRef(false);
  41. const [modelDataChart, setModelDataChart] = useState(null);
  42. const [modelDataPieChart, setModelDataPieChart] = useState(null);
  43. const [loading, setLoading] = useState(false);
  44. const [quotaData, setQuotaData] = useState([]);
  45. const [consumeQuota, setConsumeQuota] = useState(0);
  46. const [times, setTimes] = useState(0);
  47. const [dataExportDefaultTime, setDataExportDefaultTime] = useState(
  48. localStorage.getItem('data_export_default_time') || 'hour',
  49. );
  50. const handleInputChange = (value, name) => {
  51. if (name === 'data_export_default_time') {
  52. setDataExportDefaultTime(value);
  53. return;
  54. }
  55. setInputs((inputs) => ({ ...inputs, [name]: value }));
  56. };
  57. const spec_line = {
  58. type: 'bar',
  59. data: [
  60. {
  61. id: 'barData',
  62. values: [],
  63. },
  64. ],
  65. xField: 'Time',
  66. yField: 'Usage',
  67. seriesField: 'Model',
  68. stack: true,
  69. legends: {
  70. visible: true,
  71. selectMode: 'single',
  72. },
  73. title: {
  74. visible: true,
  75. text: '模型消耗分布',
  76. subtext: '0',
  77. },
  78. bar: {
  79. // The state style of bar
  80. state: {
  81. hover: {
  82. stroke: '#000',
  83. lineWidth: 1,
  84. },
  85. },
  86. },
  87. tooltip: {
  88. mark: {
  89. content: [
  90. {
  91. key: (datum) => datum['Model'],
  92. value: (datum) =>
  93. renderQuotaNumberWithDigit(parseFloat(datum['Usage']), 4),
  94. },
  95. ],
  96. },
  97. dimension: {
  98. content: [
  99. {
  100. key: (datum) => datum['Model'],
  101. value: (datum) => datum['Usage'],
  102. },
  103. ],
  104. updateContent: (array) => {
  105. // sort by value
  106. array.sort((a, b) => b.value - a.value);
  107. // add $
  108. let sum = 0;
  109. for (let i = 0; i < array.length; i++) {
  110. sum += parseFloat(array[i].value);
  111. array[i].value = renderQuotaNumberWithDigit(
  112. parseFloat(array[i].value),
  113. 4,
  114. );
  115. }
  116. // add to first
  117. array.unshift({
  118. key: '总计',
  119. value: renderQuotaNumberWithDigit(sum, 4),
  120. });
  121. return array;
  122. },
  123. },
  124. },
  125. color: {
  126. specified: modelColorMap,
  127. },
  128. };
  129. const spec_pie = {
  130. type: 'pie',
  131. data: [
  132. {
  133. id: 'id0',
  134. values: [{ type: 'null', value: '0' }],
  135. },
  136. ],
  137. outerRadius: 0.8,
  138. innerRadius: 0.5,
  139. padAngle: 0.6,
  140. valueField: 'value',
  141. categoryField: 'type',
  142. pie: {
  143. style: {
  144. cornerRadius: 10,
  145. },
  146. state: {
  147. hover: {
  148. outerRadius: 0.85,
  149. stroke: '#000',
  150. lineWidth: 1,
  151. },
  152. selected: {
  153. outerRadius: 0.85,
  154. stroke: '#000',
  155. lineWidth: 1,
  156. },
  157. },
  158. },
  159. title: {
  160. visible: true,
  161. text: '模型调用次数占比',
  162. },
  163. legends: {
  164. visible: true,
  165. orient: 'left',
  166. },
  167. label: {
  168. visible: true,
  169. },
  170. tooltip: {
  171. mark: {
  172. content: [
  173. {
  174. key: (datum) => datum['type'],
  175. value: (datum) => renderNumber(datum['value']),
  176. },
  177. ],
  178. },
  179. },
  180. color: {
  181. specified: modelColorMap,
  182. },
  183. };
  184. const loadQuotaData = async (lineChart, pieChart) => {
  185. setLoading(true);
  186. let url = '';
  187. let localStartTimestamp = Date.parse(start_timestamp) / 1000;
  188. let localEndTimestamp = Date.parse(end_timestamp) / 1000;
  189. if (isAdminUser) {
  190. url = `/api/data/?username=${username}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
  191. } else {
  192. url = `/api/data/self/?start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
  193. }
  194. const res = await API.get(url);
  195. const { success, message, data } = res.data;
  196. if (success) {
  197. setQuotaData(data);
  198. if (data.length === 0) {
  199. data.push({
  200. count: 0,
  201. model_name: '无数据',
  202. quota: 0,
  203. created_at: now.getTime() / 1000,
  204. });
  205. }
  206. // 根据dataExportDefaultTime重制时间粒度
  207. let timeGranularity = 3600;
  208. if (dataExportDefaultTime === 'day') {
  209. timeGranularity = 86400;
  210. } else if (dataExportDefaultTime === 'week') {
  211. timeGranularity = 604800;
  212. }
  213. // sort created_at
  214. data.sort((a, b) => a.created_at - b.created_at);
  215. data.forEach((item) => {
  216. item['created_at'] =
  217. Math.floor(item['created_at'] / timeGranularity) * timeGranularity;
  218. });
  219. updateChart(lineChart, pieChart, data);
  220. } else {
  221. showError(message);
  222. }
  223. setLoading(false);
  224. };
  225. const refresh = async () => {
  226. await loadQuotaData(modelDataChart, modelDataPieChart);
  227. };
  228. const initChart = async () => {
  229. let lineChart = modelDataChart;
  230. if (!modelDataChart) {
  231. lineChart = new VChart(spec_line, { dom: 'model_data' });
  232. setModelDataChart(lineChart);
  233. lineChart.renderAsync();
  234. }
  235. let pieChart = modelDataPieChart;
  236. if (!modelDataPieChart) {
  237. pieChart = new VChart(spec_pie, { dom: 'model_pie' });
  238. setModelDataPieChart(pieChart);
  239. pieChart.renderAsync();
  240. }
  241. console.log('init vchart');
  242. await loadQuotaData(lineChart, pieChart);
  243. };
  244. const updateChart = (lineChart, pieChart, data) => {
  245. if (isAdminUser) {
  246. // 将所有用户合并
  247. }
  248. let pieData = [];
  249. let lineData = [];
  250. let consumeQuota = 0;
  251. let times = 0;
  252. for (let i = 0; i < data.length; i++) {
  253. const item = data[i];
  254. consumeQuota += item.quota;
  255. times += item.count;
  256. // 合并model_name
  257. let pieItem = pieData.find((it) => it.type === item.model_name);
  258. if (pieItem) {
  259. pieItem.value += item.count;
  260. } else {
  261. pieData.push({
  262. type: item.model_name,
  263. value: item.count,
  264. });
  265. }
  266. // 合并created_at和model_name 为 lineData, created_at 数据类型是小时的时间戳
  267. // 转换日期格式
  268. let createTime = timestamp2string1(
  269. item.created_at,
  270. dataExportDefaultTime,
  271. );
  272. let lineItem = lineData.find(
  273. (it) => it.Time === createTime && it.Model === item.model_name,
  274. );
  275. if (lineItem) {
  276. lineItem.Usage += parseFloat(getQuotaWithUnit(item.quota));
  277. } else {
  278. lineData.push({
  279. Time: createTime,
  280. Model: item.model_name,
  281. Usage: parseFloat(getQuotaWithUnit(item.quota)),
  282. });
  283. }
  284. }
  285. setConsumeQuota(consumeQuota);
  286. setTimes(times);
  287. // sort by count
  288. pieData.sort((a, b) => b.value - a.value);
  289. spec_pie.title.subtext = `总计:${renderNumber(times)}`;
  290. spec_pie.data[0].values = pieData;
  291. spec_line.title.subtext = `总计:${renderQuota(consumeQuota, 2)}`;
  292. spec_line.data[0].values = lineData;
  293. pieChart.updateSpec(spec_pie);
  294. lineChart.updateSpec(spec_line);
  295. // pieChart.updateData('id0', pieData);
  296. // lineChart.updateData('barData', lineData);
  297. pieChart.reLayout();
  298. lineChart.reLayout();
  299. };
  300. useEffect(() => {
  301. // setDataExportDefaultTime(localStorage.getItem('data_export_default_time'));
  302. // if (dataExportDefaultTime === 'day') {
  303. // // 设置开始时间为7天前
  304. // let st = timestamp2string(now.getTime() / 1000 - 86400 * 7)
  305. // inputs.start_timestamp = st;
  306. // formRef.current.formApi.setValue('start_timestamp', st);
  307. // }
  308. if (!initialized.current) {
  309. initVChartSemiTheme({
  310. isWatchingThemeSwitch: true,
  311. });
  312. initialized.current = true;
  313. initChart();
  314. }
  315. }, []);
  316. return (
  317. <>
  318. <Layout>
  319. <Layout.Header>
  320. <h3>数据看板</h3>
  321. </Layout.Header>
  322. <Layout.Content>
  323. <Form ref={formRef} layout='horizontal' style={{ marginTop: 10 }}>
  324. <>
  325. <Form.DatePicker
  326. field='start_timestamp'
  327. label='起始时间'
  328. style={{ width: 272 }}
  329. initValue={start_timestamp}
  330. value={start_timestamp}
  331. type='dateTime'
  332. name='start_timestamp'
  333. onChange={(value) =>
  334. handleInputChange(value, 'start_timestamp')
  335. }
  336. />
  337. <Form.DatePicker
  338. field='end_timestamp'
  339. fluid
  340. label='结束时间'
  341. style={{ width: 272 }}
  342. initValue={end_timestamp}
  343. value={end_timestamp}
  344. type='dateTime'
  345. name='end_timestamp'
  346. onChange={(value) => handleInputChange(value, 'end_timestamp')}
  347. />
  348. <Form.Select
  349. field='data_export_default_time'
  350. label='时间粒度'
  351. style={{ width: 176 }}
  352. initValue={dataExportDefaultTime}
  353. placeholder={'时间粒度'}
  354. name='data_export_default_time'
  355. optionList={[
  356. { label: '小时', value: 'hour' },
  357. { label: '天', value: 'day' },
  358. { label: '周', value: 'week' },
  359. ]}
  360. onChange={(value) =>
  361. handleInputChange(value, 'data_export_default_time')
  362. }
  363. ></Form.Select>
  364. {isAdminUser && (
  365. <>
  366. <Form.Input
  367. field='username'
  368. label='用户名称'
  369. style={{ width: 176 }}
  370. value={username}
  371. placeholder={'可选值'}
  372. name='username'
  373. onChange={(value) => handleInputChange(value, 'username')}
  374. />
  375. </>
  376. )}
  377. <Form.Section>
  378. <Button
  379. label='查询'
  380. type='primary'
  381. htmlType='submit'
  382. className='btn-margin-right'
  383. onClick={refresh}
  384. loading={loading}
  385. >
  386. 查询
  387. </Button>
  388. </Form.Section>
  389. </>
  390. </Form>
  391. <Spin spinning={loading}>
  392. <div style={{ height: 500 }}>
  393. <div
  394. id='model_pie'
  395. style={{ width: '100%', minWidth: 100 }}
  396. ></div>
  397. </div>
  398. <div style={{ height: 500 }}>
  399. <div
  400. id='model_data'
  401. style={{ width: '100%', minWidth: 100 }}
  402. ></div>
  403. </div>
  404. </Spin>
  405. </Layout.Content>
  406. </Layout>
  407. </>
  408. );
  409. };
  410. export default Detail;