| | import React, { useEffect, useState } from 'react'; |
| | import { Button, Form, Header, Label, Pagination, Segment, Select, Table } from 'semantic-ui-react'; |
| | import { API, isAdmin, showError, timestamp2string } from '../helpers'; |
| |
|
| | import { ITEMS_PER_PAGE } from '../constants'; |
| | import { renderQuota } from '../helpers/render'; |
| |
|
| | function renderTimestamp(timestamp) { |
| | return ( |
| | <> |
| | {timestamp2string(timestamp)} |
| | </> |
| | ); |
| | } |
| |
|
| | const MODE_OPTIONS = [ |
| | { key: 'all', text: '全部用户', value: 'all' }, |
| | { key: 'self', text: '当前用户', value: 'self' } |
| | ]; |
| |
|
| | const LOG_OPTIONS = [ |
| | { key: '0', text: '全部', value: 0 }, |
| | { key: '1', text: '充值', value: 1 }, |
| | { key: '2', text: '消费', value: 2 }, |
| | { key: '3', text: '管理', value: 3 }, |
| | { key: '4', text: '系统', value: 4 } |
| | ]; |
| |
|
| | function renderType(type) { |
| | switch (type) { |
| | case 1: |
| | return <Label basic color='green'> 充值 </Label>; |
| | case 2: |
| | return <Label basic color='olive'> 消费 </Label>; |
| | case 3: |
| | return <Label basic color='orange'> 管理 </Label>; |
| | case 4: |
| | return <Label basic color='purple'> 系统 </Label>; |
| | default: |
| | return <Label basic color='black'> 未知 </Label>; |
| | } |
| | } |
| |
|
| | const LogsTable = () => { |
| | const [logs, setLogs] = useState([]); |
| | const [showStat, setShowStat] = useState(false); |
| | const [loading, setLoading] = useState(true); |
| | const [activePage, setActivePage] = useState(1); |
| | const [searchKeyword, setSearchKeyword] = useState(''); |
| | const [searching, setSearching] = useState(false); |
| | const [logType, setLogType] = useState(0); |
| | const isAdminUser = isAdmin(); |
| | let now = new Date(); |
| | const [inputs, setInputs] = useState({ |
| | username: '', |
| | token_name: '', |
| | model_name: '', |
| | start_timestamp: timestamp2string(0), |
| | end_timestamp: timestamp2string(now.getTime() / 1000 + 3600) |
| | }); |
| | const { username, token_name, model_name, start_timestamp, end_timestamp } = inputs; |
| |
|
| | const [stat, setStat] = useState({ |
| | quota: 0, |
| | token: 0 |
| | }); |
| |
|
| | const handleInputChange = (e, { name, value }) => { |
| | setInputs((inputs) => ({ ...inputs, [name]: value })); |
| | }; |
| |
|
| | const getLogSelfStat = async () => { |
| | let localStartTimestamp = Date.parse(start_timestamp) / 1000; |
| | let localEndTimestamp = Date.parse(end_timestamp) / 1000; |
| | let res = await API.get(`/api/log/self/stat?type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`); |
| | const { success, message, data } = res.data; |
| | if (success) { |
| | setStat(data); |
| | } else { |
| | showError(message); |
| | } |
| | }; |
| |
|
| | const getLogStat = async () => { |
| | let localStartTimestamp = Date.parse(start_timestamp) / 1000; |
| | let localEndTimestamp = Date.parse(end_timestamp) / 1000; |
| | let res = await API.get(`/api/log/stat?type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`); |
| | const { success, message, data } = res.data; |
| | if (success) { |
| | setStat(data); |
| | } else { |
| | showError(message); |
| | } |
| | }; |
| |
|
| | const handleEyeClick = async () => { |
| | if (!showStat) { |
| | if (isAdminUser) { |
| | await getLogStat(); |
| | } else { |
| | await getLogSelfStat(); |
| | } |
| | } |
| | setShowStat(!showStat); |
| | }; |
| |
|
| | const loadLogs = async (startIdx) => { |
| | let url = ''; |
| | let localStartTimestamp = Date.parse(start_timestamp) / 1000; |
| | let localEndTimestamp = Date.parse(end_timestamp) / 1000; |
| | if (isAdminUser) { |
| | url = `/api/log/?p=${startIdx}&type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`; |
| | } else { |
| | url = `/api/log/self/?p=${startIdx}&type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`; |
| | } |
| | const res = await API.get(url); |
| | const { success, message, data } = res.data; |
| | if (success) { |
| | if (startIdx === 0) { |
| | setLogs(data); |
| | } else { |
| | let newLogs = [...logs]; |
| | newLogs.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data); |
| | setLogs(newLogs); |
| | } |
| | } else { |
| | showError(message); |
| | } |
| | setLoading(false); |
| | }; |
| |
|
| | const onPaginationChange = (e, { activePage }) => { |
| | (async () => { |
| | if (activePage === Math.ceil(logs.length / ITEMS_PER_PAGE) + 1) { |
| | |
| | await loadLogs(activePage - 1); |
| | } |
| | setActivePage(activePage); |
| | })(); |
| | }; |
| |
|
| | const refresh = async () => { |
| | setLoading(true); |
| | setActivePage(1); |
| | await loadLogs(0); |
| | }; |
| |
|
| | useEffect(() => { |
| | refresh().then(); |
| | }, [logType]); |
| |
|
| | const searchLogs = async () => { |
| | if (searchKeyword === '') { |
| | |
| | await loadLogs(0); |
| | setActivePage(1); |
| | return; |
| | } |
| | setSearching(true); |
| | const res = await API.get(`/api/log/self/search?keyword=${searchKeyword}`); |
| | const { success, message, data } = res.data; |
| | if (success) { |
| | setLogs(data); |
| | setActivePage(1); |
| | } else { |
| | showError(message); |
| | } |
| | setSearching(false); |
| | }; |
| |
|
| | const handleKeywordChange = async (e, { value }) => { |
| | setSearchKeyword(value.trim()); |
| | }; |
| |
|
| | const sortLog = (key) => { |
| | if (logs.length === 0) return; |
| | setLoading(true); |
| | let sortedLogs = [...logs]; |
| | if (typeof sortedLogs[0][key] === 'string') { |
| | sortedLogs.sort((a, b) => { |
| | return ('' + a[key]).localeCompare(b[key]); |
| | }); |
| | } else { |
| | sortedLogs.sort((a, b) => { |
| | if (a[key] === b[key]) return 0; |
| | if (a[key] > b[key]) return -1; |
| | if (a[key] < b[key]) return 1; |
| | }); |
| | } |
| | if (sortedLogs[0].id === logs[0].id) { |
| | sortedLogs.reverse(); |
| | } |
| | setLogs(sortedLogs); |
| | setLoading(false); |
| | }; |
| |
|
| | return ( |
| | <> |
| | <Segment> |
| | <Header as='h3'> |
| | 使用明细(总消耗额度: |
| | {showStat && renderQuota(stat.quota)} |
| | {!showStat && <span onClick={handleEyeClick} style={{ cursor: 'pointer', color: 'gray' }}>点击查看</span>} |
| | ) |
| | </Header> |
| | <Form> |
| | <Form.Group> |
| | { |
| | isAdminUser && ( |
| | <Form.Input fluid label={'用户名称'} width={2} value={username} |
| | placeholder={'可选值'} name='username' |
| | onChange={handleInputChange} /> |
| | ) |
| | } |
| | <Form.Input fluid label={'令牌名称'} width={isAdminUser ? 2 : 3} value={token_name} |
| | placeholder={'可选值'} name='token_name' onChange={handleInputChange} /> |
| | <Form.Input fluid label='模型名称' width={isAdminUser ? 2 : 3} value={model_name} placeholder='可选值' |
| | name='model_name' |
| | onChange={handleInputChange} /> |
| | <Form.Input fluid label='起始时间' width={4} value={start_timestamp} type='datetime-local' |
| | name='start_timestamp' |
| | onChange={handleInputChange} /> |
| | <Form.Input fluid label='结束时间' width={4} value={end_timestamp} type='datetime-local' |
| | name='end_timestamp' |
| | onChange={handleInputChange} /> |
| | <Form.Button fluid label='操作' width={2} onClick={refresh}>查询</Form.Button> |
| | </Form.Group> |
| | </Form> |
| | <Table basic compact size='small'> |
| | <Table.Header> |
| | <Table.Row> |
| | <Table.HeaderCell |
| | style={{ cursor: 'pointer' }} |
| | onClick={() => { |
| | sortLog('created_time'); |
| | }} |
| | width={3} |
| | > |
| | 时间 |
| | </Table.HeaderCell> |
| | { |
| | isAdminUser && <Table.HeaderCell |
| | style={{ cursor: 'pointer' }} |
| | onClick={() => { |
| | sortLog('username'); |
| | }} |
| | width={1} |
| | > |
| | 用户 |
| | </Table.HeaderCell> |
| | } |
| | <Table.HeaderCell |
| | style={{ cursor: 'pointer' }} |
| | onClick={() => { |
| | sortLog('token_name'); |
| | }} |
| | width={1} |
| | > |
| | 令牌 |
| | </Table.HeaderCell> |
| | <Table.HeaderCell |
| | style={{ cursor: 'pointer' }} |
| | onClick={() => { |
| | sortLog('type'); |
| | }} |
| | width={1} |
| | > |
| | 类型 |
| | </Table.HeaderCell> |
| | <Table.HeaderCell |
| | style={{ cursor: 'pointer' }} |
| | onClick={() => { |
| | sortLog('model_name'); |
| | }} |
| | width={2} |
| | > |
| | 模型 |
| | </Table.HeaderCell> |
| | <Table.HeaderCell |
| | style={{ cursor: 'pointer' }} |
| | onClick={() => { |
| | sortLog('prompt_tokens'); |
| | }} |
| | width={1} |
| | > |
| | 提示 |
| | </Table.HeaderCell> |
| | <Table.HeaderCell |
| | style={{ cursor: 'pointer' }} |
| | onClick={() => { |
| | sortLog('completion_tokens'); |
| | }} |
| | width={1} |
| | > |
| | 补全 |
| | </Table.HeaderCell> |
| | <Table.HeaderCell |
| | style={{ cursor: 'pointer' }} |
| | onClick={() => { |
| | sortLog('quota'); |
| | }} |
| | width={2} |
| | > |
| | 消耗额度 |
| | </Table.HeaderCell> |
| | <Table.HeaderCell |
| | style={{ cursor: 'pointer' }} |
| | onClick={() => { |
| | sortLog('content'); |
| | }} |
| | width={isAdminUser ? 4 : 5} |
| | > |
| | 详情 |
| | </Table.HeaderCell> |
| | </Table.Row> |
| | </Table.Header> |
| | |
| | <Table.Body> |
| | {logs |
| | .slice( |
| | (activePage - 1) * ITEMS_PER_PAGE, |
| | activePage * ITEMS_PER_PAGE |
| | ) |
| | .map((log, idx) => { |
| | if (log.deleted) return <></>; |
| | return ( |
| | <Table.Row key={log.created_at}> |
| | <Table.Cell>{renderTimestamp(log.created_at)}</Table.Cell> |
| | { |
| | isAdminUser && ( |
| | <Table.Cell>{log.username ? <Label>{log.username}</Label> : ''}</Table.Cell> |
| | ) |
| | } |
| | <Table.Cell>{log.token_name ? <Label basic>{log.token_name}</Label> : ''}</Table.Cell> |
| | <Table.Cell>{renderType(log.type)}</Table.Cell> |
| | <Table.Cell>{log.model_name ? <Label basic>{log.model_name}</Label> : ''}</Table.Cell> |
| | <Table.Cell>{log.prompt_tokens ? log.prompt_tokens : ''}</Table.Cell> |
| | <Table.Cell>{log.completion_tokens ? log.completion_tokens : ''}</Table.Cell> |
| | <Table.Cell>{log.quota ? renderQuota(log.quota, 6) : ''}</Table.Cell> |
| | <Table.Cell>{log.content}</Table.Cell> |
| | </Table.Row> |
| | ); |
| | })} |
| | </Table.Body> |
| | |
| | <Table.Footer> |
| | <Table.Row> |
| | <Table.HeaderCell colSpan={'9'}> |
| | <Select |
| | placeholder='选择明细分类' |
| | options={LOG_OPTIONS} |
| | style={{ marginRight: '8px' }} |
| | name='logType' |
| | value={logType} |
| | onChange={(e, { name, value }) => { |
| | setLogType(value); |
| | }} |
| | /> |
| | <Button size='small' onClick={refresh} loading={loading}>刷新</Button> |
| | <Pagination |
| | floated='right' |
| | activePage={activePage} |
| | onPageChange={onPaginationChange} |
| | size='small' |
| | siblingRange={1} |
| | totalPages={ |
| | Math.ceil(logs.length / ITEMS_PER_PAGE) + |
| | (logs.length % ITEMS_PER_PAGE === 0 ? 1 : 0) |
| | } |
| | /> |
| | </Table.HeaderCell> |
| | </Table.Row> |
| | </Table.Footer> |
| | </Table> |
| | </Segment> |
| | </> |
| | ); |
| | }; |
| |
|
| | export default LogsTable; |
| |
|