mirror of
https://github.com/songquanpeng/one-api.git
synced 2025-11-14 04:13:41 +08:00
feat: add new theme berry (#860)
* feat: add theme berry * docs: add development notes * fix: fix blank page * chore: update implementation * fix: fix package.json * chore: update ui copy --------- Co-authored-by: JustSong <songquanpeng@foxmail.com>
This commit is contained in:
27
web/berry/src/views/Log/component/TableHead.js
Normal file
27
web/berry/src/views/Log/component/TableHead.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { TableCell, TableHead, TableRow } from '@mui/material';
|
||||
|
||||
const LogTableHead = ({ userIsAdmin }) => {
|
||||
return (
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>时间</TableCell>
|
||||
{userIsAdmin && <TableCell>渠道</TableCell>}
|
||||
{userIsAdmin && <TableCell>用户</TableCell>}
|
||||
<TableCell>令牌</TableCell>
|
||||
<TableCell>类型</TableCell>
|
||||
<TableCell>模型</TableCell>
|
||||
<TableCell>提示</TableCell>
|
||||
<TableCell>补全</TableCell>
|
||||
<TableCell>额度</TableCell>
|
||||
<TableCell>详情</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogTableHead;
|
||||
|
||||
LogTableHead.propTypes = {
|
||||
userIsAdmin: PropTypes.bool
|
||||
};
|
||||
69
web/berry/src/views/Log/component/TableRow.js
Normal file
69
web/berry/src/views/Log/component/TableRow.js
Normal file
@@ -0,0 +1,69 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { TableRow, TableCell } from '@mui/material';
|
||||
|
||||
import { timestamp2string, renderQuota } from 'utils/common';
|
||||
import Label from 'ui-component/Label';
|
||||
import LogType from '../type/LogType';
|
||||
|
||||
function renderType(type) {
|
||||
const typeOption = LogType[type];
|
||||
if (typeOption) {
|
||||
return (
|
||||
<Label variant="filled" color={typeOption.color}>
|
||||
{' '}
|
||||
{typeOption.text}{' '}
|
||||
</Label>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Label variant="filled" color="error">
|
||||
{' '}
|
||||
未知{' '}
|
||||
</Label>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default function LogTableRow({ item, userIsAdmin }) {
|
||||
return (
|
||||
<>
|
||||
<TableRow tabIndex={item.id}>
|
||||
<TableCell>{timestamp2string(item.created_at)}</TableCell>
|
||||
|
||||
{userIsAdmin && <TableCell>{item.channel || ''}</TableCell>}
|
||||
{userIsAdmin && (
|
||||
<TableCell>
|
||||
<Label color="default" variant="outlined">
|
||||
{item.username}
|
||||
</Label>
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell>
|
||||
{item.token_name && (
|
||||
<Label color="default" variant="soft">
|
||||
{item.token_name}
|
||||
</Label>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>{renderType(item.type)}</TableCell>
|
||||
<TableCell>
|
||||
{item.model_name && (
|
||||
<Label color="primary" variant="outlined">
|
||||
{item.model_name}
|
||||
</Label>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>{item.prompt_tokens || ''}</TableCell>
|
||||
<TableCell>{item.completion_tokens || ''}</TableCell>
|
||||
<TableCell>{item.quota ? renderQuota(item.quota, 6) : ''}</TableCell>
|
||||
<TableCell>{item.content}</TableCell>
|
||||
</TableRow>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
LogTableRow.propTypes = {
|
||||
item: PropTypes.object,
|
||||
userIsAdmin: PropTypes.bool
|
||||
};
|
||||
239
web/berry/src/views/Log/component/TableToolBar.js
Normal file
239
web/berry/src/views/Log/component/TableToolBar.js
Normal file
@@ -0,0 +1,239 @@
|
||||
import PropTypes from "prop-types";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import {
|
||||
IconUser,
|
||||
IconKey,
|
||||
IconBrandGithubCopilot,
|
||||
IconSitemap,
|
||||
} from "@tabler/icons-react";
|
||||
import {
|
||||
InputAdornment,
|
||||
OutlinedInput,
|
||||
Stack,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
Select,
|
||||
MenuItem,
|
||||
} from "@mui/material";
|
||||
import { LocalizationProvider, DateTimePicker } from "@mui/x-date-pickers";
|
||||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
||||
import dayjs from "dayjs";
|
||||
import LogType from "../type/LogType";
|
||||
require("dayjs/locale/zh-cn");
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
export default function TableToolBar({
|
||||
filterName,
|
||||
handleFilterName,
|
||||
userIsAdmin,
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
const grey500 = theme.palette.grey[500];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack
|
||||
direction={{ xs: "column", sm: "row" }}
|
||||
spacing={{ xs: 3, sm: 2, md: 4 }}
|
||||
padding={"24px"}
|
||||
paddingBottom={"0px"}
|
||||
>
|
||||
<FormControl>
|
||||
<InputLabel htmlFor="channel-token_name-label">令牌名称</InputLabel>
|
||||
<OutlinedInput
|
||||
id="token_name"
|
||||
name="token_name"
|
||||
sx={{
|
||||
minWidth: "100%",
|
||||
}}
|
||||
label="令牌名称"
|
||||
value={filterName.token_name}
|
||||
onChange={handleFilterName}
|
||||
placeholder="令牌名称"
|
||||
startAdornment={
|
||||
<InputAdornment position="start">
|
||||
<IconKey stroke={1.5} size="20px" color={grey500} />
|
||||
</InputAdornment>
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormControl>
|
||||
<InputLabel htmlFor="channel-model_name-label">模型名称</InputLabel>
|
||||
<OutlinedInput
|
||||
id="model_name"
|
||||
name="model_name"
|
||||
sx={{
|
||||
minWidth: "100%",
|
||||
}}
|
||||
label="模型名称"
|
||||
value={filterName.model_name}
|
||||
onChange={handleFilterName}
|
||||
placeholder="模型名称"
|
||||
startAdornment={
|
||||
<InputAdornment position="start">
|
||||
<IconBrandGithubCopilot
|
||||
stroke={1.5}
|
||||
size="20px"
|
||||
color={grey500}
|
||||
/>
|
||||
</InputAdornment>
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<LocalizationProvider
|
||||
dateAdapter={AdapterDayjs}
|
||||
adapterLocale={"zh-cn"}
|
||||
>
|
||||
<DateTimePicker
|
||||
label="起始时间"
|
||||
ampm={false}
|
||||
name="start_timestamp"
|
||||
value={
|
||||
filterName.start_timestamp === 0
|
||||
? null
|
||||
: dayjs.unix(filterName.start_timestamp)
|
||||
}
|
||||
onChange={(value) => {
|
||||
if (value === null) {
|
||||
handleFilterName({
|
||||
target: { name: "start_timestamp", value: 0 },
|
||||
});
|
||||
return;
|
||||
}
|
||||
handleFilterName({
|
||||
target: { name: "start_timestamp", value: value.unix() },
|
||||
});
|
||||
}}
|
||||
slotProps={{
|
||||
actionBar: {
|
||||
actions: ["clear", "today", "accept"],
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</LocalizationProvider>
|
||||
</FormControl>
|
||||
|
||||
<FormControl>
|
||||
<LocalizationProvider
|
||||
dateAdapter={AdapterDayjs}
|
||||
adapterLocale={"zh-cn"}
|
||||
>
|
||||
<DateTimePicker
|
||||
label="结束时间"
|
||||
name="end_timestamp"
|
||||
ampm={false}
|
||||
value={
|
||||
filterName.end_timestamp === 0
|
||||
? null
|
||||
: dayjs.unix(filterName.end_timestamp)
|
||||
}
|
||||
onChange={(value) => {
|
||||
if (value === null) {
|
||||
handleFilterName({
|
||||
target: { name: "end_timestamp", value: 0 },
|
||||
});
|
||||
return;
|
||||
}
|
||||
handleFilterName({
|
||||
target: { name: "end_timestamp", value: value.unix() },
|
||||
});
|
||||
}}
|
||||
slotProps={{
|
||||
actionBar: {
|
||||
actions: ["clear", "today", "accept"],
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</LocalizationProvider>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
|
||||
<Stack
|
||||
direction={{ xs: "column", sm: "row" }}
|
||||
spacing={{ xs: 3, sm: 2, md: 4 }}
|
||||
padding={"24px"}
|
||||
>
|
||||
{userIsAdmin && (
|
||||
<FormControl>
|
||||
<InputLabel htmlFor="channel-channel-label">渠道ID</InputLabel>
|
||||
<OutlinedInput
|
||||
id="channel"
|
||||
name="channel"
|
||||
sx={{
|
||||
minWidth: "100%",
|
||||
}}
|
||||
label="渠道ID"
|
||||
value={filterName.channel}
|
||||
onChange={handleFilterName}
|
||||
placeholder="渠道ID"
|
||||
startAdornment={
|
||||
<InputAdornment position="start">
|
||||
<IconSitemap stroke={1.5} size="20px" color={grey500} />
|
||||
</InputAdornment>
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
{userIsAdmin && (
|
||||
<FormControl>
|
||||
<InputLabel htmlFor="channel-username-label">用户名称</InputLabel>
|
||||
<OutlinedInput
|
||||
id="username"
|
||||
name="username"
|
||||
sx={{
|
||||
minWidth: "100%",
|
||||
}}
|
||||
label="用户名称"
|
||||
value={filterName.username}
|
||||
onChange={handleFilterName}
|
||||
placeholder="用户名称"
|
||||
startAdornment={
|
||||
<InputAdornment position="start">
|
||||
<IconUser stroke={1.5} size="20px" color={grey500} />
|
||||
</InputAdornment>
|
||||
}
|
||||
/>
|
||||
</FormControl>
|
||||
)}
|
||||
|
||||
<FormControl sx={{ minWidth: "22%" }}>
|
||||
<InputLabel htmlFor="channel-type-label">类型</InputLabel>
|
||||
<Select
|
||||
id="channel-type-label"
|
||||
label="类型"
|
||||
value={filterName.type}
|
||||
name="type"
|
||||
onChange={handleFilterName}
|
||||
sx={{
|
||||
minWidth: "100%",
|
||||
}}
|
||||
MenuProps={{
|
||||
PaperProps: {
|
||||
style: {
|
||||
maxHeight: 200,
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
{Object.values(LogType).map((option) => {
|
||||
return (
|
||||
<MenuItem key={option.value} value={option.value}>
|
||||
{option.text}
|
||||
</MenuItem>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
</FormControl>
|
||||
</Stack>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
TableToolBar.propTypes = {
|
||||
filterName: PropTypes.object,
|
||||
handleFilterName: PropTypes.func,
|
||||
userIsAdmin: PropTypes.bool,
|
||||
};
|
||||
157
web/berry/src/views/Log/index.js
Normal file
157
web/berry/src/views/Log/index.js
Normal file
@@ -0,0 +1,157 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { showError } from 'utils/common';
|
||||
|
||||
import Table from '@mui/material/Table';
|
||||
import TableBody from '@mui/material/TableBody';
|
||||
import TableContainer from '@mui/material/TableContainer';
|
||||
import PerfectScrollbar from 'react-perfect-scrollbar';
|
||||
import TablePagination from '@mui/material/TablePagination';
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import ButtonGroup from '@mui/material/ButtonGroup';
|
||||
import Toolbar from '@mui/material/Toolbar';
|
||||
|
||||
import { Button, Card, Stack, Container, Typography, Box } from '@mui/material';
|
||||
import LogTableRow from './component/TableRow';
|
||||
import LogTableHead from './component/TableHead';
|
||||
import TableToolBar from './component/TableToolBar';
|
||||
import { API } from 'utils/api';
|
||||
import { isAdmin } from 'utils/common';
|
||||
import { ITEMS_PER_PAGE } from 'constants';
|
||||
import { IconRefresh, IconSearch } from '@tabler/icons-react';
|
||||
|
||||
export default function Log() {
|
||||
const originalKeyword = {
|
||||
p: 0,
|
||||
username: '',
|
||||
token_name: '',
|
||||
model_name: '',
|
||||
start_timestamp: 0,
|
||||
end_timestamp: new Date().getTime() / 1000 + 3600,
|
||||
type: 0,
|
||||
channel: ''
|
||||
};
|
||||
const [logs, setLogs] = useState([]);
|
||||
const [activePage, setActivePage] = useState(0);
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [searchKeyword, setSearchKeyword] = useState(originalKeyword);
|
||||
const [initPage, setInitPage] = useState(true);
|
||||
const userIsAdmin = isAdmin();
|
||||
|
||||
const loadLogs = async (startIdx) => {
|
||||
setSearching(true);
|
||||
const url = userIsAdmin ? '/api/log/' : '/api/log/self/';
|
||||
const query = searchKeyword;
|
||||
|
||||
query.p = startIdx;
|
||||
if (!userIsAdmin) {
|
||||
delete query.username;
|
||||
delete query.channel;
|
||||
}
|
||||
const res = await API.get(url, { params: query });
|
||||
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);
|
||||
}
|
||||
setSearching(false);
|
||||
};
|
||||
|
||||
const onPaginationChange = (event, activePage) => {
|
||||
(async () => {
|
||||
if (activePage === Math.ceil(logs.length / ITEMS_PER_PAGE)) {
|
||||
// In this case we have to load more data and then append them.
|
||||
await loadLogs(activePage);
|
||||
}
|
||||
setActivePage(activePage);
|
||||
})();
|
||||
};
|
||||
|
||||
const searchLogs = async (event) => {
|
||||
event.preventDefault();
|
||||
await loadLogs(0);
|
||||
setActivePage(0);
|
||||
return;
|
||||
};
|
||||
|
||||
const handleSearchKeyword = (event) => {
|
||||
setSearchKeyword({ ...searchKeyword, [event.target.name]: event.target.value });
|
||||
};
|
||||
|
||||
// 处理刷新
|
||||
const handleRefresh = () => {
|
||||
setInitPage(true);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSearchKeyword(originalKeyword);
|
||||
setActivePage(0);
|
||||
loadLogs(0)
|
||||
.then()
|
||||
.catch((reason) => {
|
||||
showError(reason);
|
||||
});
|
||||
setInitPage(false);
|
||||
}, [initPage]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between" mb={5}>
|
||||
<Typography variant="h4">日志</Typography>
|
||||
</Stack>
|
||||
<Card>
|
||||
<Box component="form" onSubmit={searchLogs} noValidate>
|
||||
<TableToolBar filterName={searchKeyword} handleFilterName={handleSearchKeyword} userIsAdmin={userIsAdmin} />
|
||||
</Box>
|
||||
<Toolbar
|
||||
sx={{
|
||||
textAlign: 'right',
|
||||
height: 50,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
p: (theme) => theme.spacing(0, 1, 0, 3)
|
||||
}}
|
||||
>
|
||||
<Container>
|
||||
<ButtonGroup variant="outlined" aria-label="outlined small primary button group">
|
||||
<Button onClick={handleRefresh} startIcon={<IconRefresh width={'18px'} />}>
|
||||
刷新/清除搜索条件
|
||||
</Button>
|
||||
|
||||
<Button onClick={searchLogs} startIcon={<IconSearch width={'18px'} />}>
|
||||
搜索
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Container>
|
||||
</Toolbar>
|
||||
{searching && <LinearProgress />}
|
||||
<PerfectScrollbar component="div">
|
||||
<TableContainer sx={{ overflow: 'unset' }}>
|
||||
<Table sx={{ minWidth: 800 }}>
|
||||
<LogTableHead userIsAdmin={userIsAdmin} />
|
||||
<TableBody>
|
||||
{logs.slice(activePage * ITEMS_PER_PAGE, (activePage + 1) * ITEMS_PER_PAGE).map((row, index) => (
|
||||
<LogTableRow item={row} key={`${row.id}_${index}`} userIsAdmin={userIsAdmin} />
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</PerfectScrollbar>
|
||||
<TablePagination
|
||||
page={activePage}
|
||||
component="div"
|
||||
count={logs.length + (logs.length % ITEMS_PER_PAGE === 0 ? 1 : 0)}
|
||||
rowsPerPage={ITEMS_PER_PAGE}
|
||||
onPageChange={onPaginationChange}
|
||||
rowsPerPageOptions={[ITEMS_PER_PAGE]}
|
||||
/>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
}
|
||||
9
web/berry/src/views/Log/type/LogType.js
Normal file
9
web/berry/src/views/Log/type/LogType.js
Normal file
@@ -0,0 +1,9 @@
|
||||
const LOG_TYPE = {
|
||||
0: { value: '0', text: '全部', color: '' },
|
||||
1: { value: '1', text: '充值', color: 'primary' },
|
||||
2: { value: '2', text: '消费', color: 'orange' },
|
||||
3: { value: '3', text: '管理', color: 'default' },
|
||||
4: { value: '4', text: '系统', color: 'secondary' }
|
||||
};
|
||||
|
||||
export default LOG_TYPE;
|
||||
Reference in New Issue
Block a user