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:
Buer
2024-01-07 14:20:07 +08:00
committed by GitHub
parent 6227eee5bc
commit 48989d4a0b
157 changed files with 13979 additions and 5 deletions

View 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
};

View 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
};

View 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,
};

View 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>
</>
);
}

View 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;