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:
190
web/berry/src/views/Redemption/component/EditModal.js
Normal file
190
web/berry/src/views/Redemption/component/EditModal.js
Normal file
@@ -0,0 +1,190 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import * as Yup from 'yup';
|
||||
import { Formik } from 'formik';
|
||||
import { useTheme } from '@mui/material/styles';
|
||||
import { useState, useEffect } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
Button,
|
||||
Divider,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
OutlinedInput,
|
||||
InputAdornment,
|
||||
FormHelperText
|
||||
} from '@mui/material';
|
||||
|
||||
import { renderQuotaWithPrompt, showSuccess, showError, downloadTextAsFile } from 'utils/common';
|
||||
import { API } from 'utils/api';
|
||||
|
||||
const validationSchema = Yup.object().shape({
|
||||
is_edit: Yup.boolean(),
|
||||
name: Yup.string().required('名称 不能为空'),
|
||||
quota: Yup.number().min(0, '必须大于等于0'),
|
||||
count: Yup.number().when('is_edit', {
|
||||
is: false,
|
||||
then: Yup.number().min(1, '必须大于等于1'),
|
||||
otherwise: Yup.number()
|
||||
})
|
||||
});
|
||||
|
||||
const originInputs = {
|
||||
is_edit: false,
|
||||
name: '',
|
||||
quota: 100000,
|
||||
count: 1
|
||||
};
|
||||
|
||||
const EditModal = ({ open, redemptiondId, onCancel, onOk }) => {
|
||||
const theme = useTheme();
|
||||
const [inputs, setInputs] = useState(originInputs);
|
||||
|
||||
const submit = async (values, { setErrors, setStatus, setSubmitting }) => {
|
||||
setSubmitting(true);
|
||||
|
||||
let res;
|
||||
if (values.is_edit) {
|
||||
res = await API.put(`/api/redemption/`, { ...values, id: parseInt(redemptiondId) });
|
||||
} else {
|
||||
res = await API.post(`/api/redemption/`, values);
|
||||
}
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
if (values.is_edit) {
|
||||
showSuccess('兑换码更新成功!');
|
||||
} else {
|
||||
showSuccess('兑换码创建成功!');
|
||||
if (data.length > 1) {
|
||||
let text = '';
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
text += data[i] + '\n';
|
||||
}
|
||||
downloadTextAsFile(text, `${values.name}.txt`);
|
||||
}
|
||||
}
|
||||
setSubmitting(false);
|
||||
setStatus({ success: true });
|
||||
onOk(true);
|
||||
} else {
|
||||
showError(message);
|
||||
setErrors({ submit: message });
|
||||
}
|
||||
};
|
||||
|
||||
const loadRedemptiond = async () => {
|
||||
let res = await API.get(`/api/redemption/${redemptiondId}`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
data.is_edit = true;
|
||||
setInputs(data);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (redemptiondId) {
|
||||
loadRedemptiond().then();
|
||||
} else {
|
||||
setInputs(originInputs);
|
||||
}
|
||||
}, [redemptiondId]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onCancel} fullWidth maxWidth={'md'}>
|
||||
<DialogTitle sx={{ margin: '0px', fontWeight: 700, lineHeight: '1.55556', padding: '24px', fontSize: '1.125rem' }}>
|
||||
{redemptiondId ? '编辑兑换码' : '新建兑换码'}
|
||||
</DialogTitle>
|
||||
<Divider />
|
||||
<DialogContent>
|
||||
<Formik initialValues={inputs} enableReinitialize validationSchema={validationSchema} onSubmit={submit}>
|
||||
{({ errors, handleBlur, handleChange, handleSubmit, touched, values, isSubmitting }) => (
|
||||
<form noValidate onSubmit={handleSubmit}>
|
||||
<FormControl fullWidth error={Boolean(touched.name && errors.name)} sx={{ ...theme.typography.otherInput }}>
|
||||
<InputLabel htmlFor="channel-name-label">名称</InputLabel>
|
||||
<OutlinedInput
|
||||
id="channel-name-label"
|
||||
label="名称"
|
||||
type="text"
|
||||
value={values.name}
|
||||
name="name"
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
inputProps={{ autoComplete: 'name' }}
|
||||
aria-describedby="helper-text-channel-name-label"
|
||||
/>
|
||||
{touched.name && errors.name && (
|
||||
<FormHelperText error id="helper-tex-channel-name-label">
|
||||
{errors.name}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
<FormControl fullWidth error={Boolean(touched.quota && errors.quota)} sx={{ ...theme.typography.otherInput }}>
|
||||
<InputLabel htmlFor="channel-quota-label">额度</InputLabel>
|
||||
<OutlinedInput
|
||||
id="channel-quota-label"
|
||||
label="额度"
|
||||
type="number"
|
||||
value={values.quota}
|
||||
name="quota"
|
||||
endAdornment={<InputAdornment position="end">{renderQuotaWithPrompt(values.quota)}</InputAdornment>}
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
aria-describedby="helper-text-channel-quota-label"
|
||||
disabled={values.unlimited_quota}
|
||||
/>
|
||||
|
||||
{touched.quota && errors.quota && (
|
||||
<FormHelperText error id="helper-tex-channel-quota-label">
|
||||
{errors.quota}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
|
||||
{!values.is_edit && (
|
||||
<FormControl fullWidth error={Boolean(touched.count && errors.count)} sx={{ ...theme.typography.otherInput }}>
|
||||
<InputLabel htmlFor="channel-count-label">数量</InputLabel>
|
||||
<OutlinedInput
|
||||
id="channel-count-label"
|
||||
label="数量"
|
||||
type="number"
|
||||
value={values.count}
|
||||
name="count"
|
||||
onBlur={handleBlur}
|
||||
onChange={handleChange}
|
||||
aria-describedby="helper-text-channel-count-label"
|
||||
/>
|
||||
|
||||
{touched.count && errors.count && (
|
||||
<FormHelperText error id="helper-tex-channel-count-label">
|
||||
{errors.count}
|
||||
</FormHelperText>
|
||||
)}
|
||||
</FormControl>
|
||||
)}
|
||||
<DialogActions>
|
||||
<Button onClick={onCancel}>取消</Button>
|
||||
<Button disableElevation disabled={isSubmitting} type="submit" variant="contained" color="primary">
|
||||
提交
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</form>
|
||||
)}
|
||||
</Formik>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditModal;
|
||||
|
||||
EditModal.propTypes = {
|
||||
open: PropTypes.bool,
|
||||
redemptiondId: PropTypes.number,
|
||||
onCancel: PropTypes.func,
|
||||
onOk: PropTypes.func
|
||||
};
|
||||
19
web/berry/src/views/Redemption/component/TableHead.js
Normal file
19
web/berry/src/views/Redemption/component/TableHead.js
Normal file
@@ -0,0 +1,19 @@
|
||||
import { TableCell, TableHead, TableRow } from '@mui/material';
|
||||
|
||||
const RedemptionTableHead = () => {
|
||||
return (
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableCell>ID</TableCell>
|
||||
<TableCell>名称</TableCell>
|
||||
<TableCell>状态</TableCell>
|
||||
<TableCell>额度</TableCell>
|
||||
<TableCell>创建时间</TableCell>
|
||||
<TableCell>兑换时间</TableCell>
|
||||
<TableCell>操作</TableCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
);
|
||||
};
|
||||
|
||||
export default RedemptionTableHead;
|
||||
147
web/berry/src/views/Redemption/component/TableRow.js
Normal file
147
web/berry/src/views/Redemption/component/TableRow.js
Normal file
@@ -0,0 +1,147 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import { useState } from 'react';
|
||||
|
||||
import {
|
||||
Popover,
|
||||
TableRow,
|
||||
MenuItem,
|
||||
TableCell,
|
||||
IconButton,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogTitle,
|
||||
Button,
|
||||
Stack
|
||||
} from '@mui/material';
|
||||
|
||||
import Label from 'ui-component/Label';
|
||||
import TableSwitch from 'ui-component/Switch';
|
||||
import { timestamp2string, renderQuota, showSuccess } from 'utils/common';
|
||||
|
||||
import { IconDotsVertical, IconEdit, IconTrash } from '@tabler/icons-react';
|
||||
|
||||
export default function RedemptionTableRow({ item, manageRedemption, handleOpenModal, setModalRedemptionId }) {
|
||||
const [open, setOpen] = useState(null);
|
||||
const [openDelete, setOpenDelete] = useState(false);
|
||||
const [statusSwitch, setStatusSwitch] = useState(item.status);
|
||||
|
||||
const handleDeleteOpen = () => {
|
||||
handleCloseMenu();
|
||||
setOpenDelete(true);
|
||||
};
|
||||
|
||||
const handleDeleteClose = () => {
|
||||
setOpenDelete(false);
|
||||
};
|
||||
|
||||
const handleOpenMenu = (event) => {
|
||||
setOpen(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleCloseMenu = () => {
|
||||
setOpen(null);
|
||||
};
|
||||
|
||||
const handleStatus = async () => {
|
||||
const switchVlue = statusSwitch === 1 ? 2 : 1;
|
||||
const { success } = await manageRedemption(item.id, 'status', switchVlue);
|
||||
if (success) {
|
||||
setStatusSwitch(switchVlue);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async () => {
|
||||
handleCloseMenu();
|
||||
await manageRedemption(item.id, 'delete', '');
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TableRow tabIndex={item.id}>
|
||||
<TableCell>{item.id}</TableCell>
|
||||
|
||||
<TableCell>{item.name}</TableCell>
|
||||
|
||||
<TableCell>
|
||||
{item.status !== 1 && item.status !== 2 ? (
|
||||
<Label variant="filled" color={item.status === 3 ? 'success' : 'orange'}>
|
||||
{item.status === 3 ? '已使用' : '未知'}
|
||||
</Label>
|
||||
) : (
|
||||
<TableSwitch id={`switch-${item.id}`} checked={statusSwitch === 1} onChange={handleStatus} />
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
<TableCell>{renderQuota(item.quota)}</TableCell>
|
||||
<TableCell>{timestamp2string(item.created_time)}</TableCell>
|
||||
<TableCell>{item.redeemed_time ? timestamp2string(item.redeemed_time) : '尚未兑换'}</TableCell>
|
||||
<TableCell>
|
||||
<Stack direction="row" spacing={1}>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(item.key);
|
||||
showSuccess('已复制到剪贴板!');
|
||||
}}
|
||||
>
|
||||
复制
|
||||
</Button>
|
||||
<IconButton onClick={handleOpenMenu} sx={{ color: 'rgb(99, 115, 129)' }}>
|
||||
<IconDotsVertical />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
||||
<Popover
|
||||
open={!!open}
|
||||
anchorEl={open}
|
||||
onClose={handleCloseMenu}
|
||||
anchorOrigin={{ vertical: 'top', horizontal: 'left' }}
|
||||
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
|
||||
PaperProps={{
|
||||
sx: { width: 140 }
|
||||
}}
|
||||
>
|
||||
<MenuItem
|
||||
disabled={item.status !== 1 && item.status !== 2}
|
||||
onClick={() => {
|
||||
handleCloseMenu();
|
||||
handleOpenModal();
|
||||
setModalRedemptionId(item.id);
|
||||
}}
|
||||
>
|
||||
<IconEdit style={{ marginRight: '16px' }} />
|
||||
编辑
|
||||
</MenuItem>
|
||||
<MenuItem onClick={handleDeleteOpen} sx={{ color: 'error.main' }}>
|
||||
<IconTrash style={{ marginRight: '16px' }} />
|
||||
删除
|
||||
</MenuItem>
|
||||
</Popover>
|
||||
|
||||
<Dialog open={openDelete} onClose={handleDeleteClose}>
|
||||
<DialogTitle>删除兑换码</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText>是否删除兑换码 {item.name}?</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={handleDeleteClose}>关闭</Button>
|
||||
<Button onClick={handleDelete} sx={{ color: 'error.main' }} autoFocus>
|
||||
删除
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
RedemptionTableRow.propTypes = {
|
||||
item: PropTypes.object,
|
||||
manageRedemption: PropTypes.func,
|
||||
handleOpenModal: PropTypes.func,
|
||||
setModalRedemptionId: PropTypes.func
|
||||
};
|
||||
203
web/berry/src/views/Redemption/index.js
Normal file
203
web/berry/src/views/Redemption/index.js
Normal file
@@ -0,0 +1,203 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { showError, showSuccess } 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, Box, Stack, Container, Typography } from '@mui/material';
|
||||
import RedemptionTableRow from './component/TableRow';
|
||||
import RedemptionTableHead from './component/TableHead';
|
||||
import TableToolBar from 'ui-component/TableToolBar';
|
||||
import { API } from 'utils/api';
|
||||
import { ITEMS_PER_PAGE } from 'constants';
|
||||
import { IconRefresh, IconPlus } from '@tabler/icons-react';
|
||||
import EditeModal from './component/EditModal';
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
export default function Redemption() {
|
||||
const [redemptions, setRedemptions] = useState([]);
|
||||
const [activePage, setActivePage] = useState(0);
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [searchKeyword, setSearchKeyword] = useState('');
|
||||
const [openModal, setOpenModal] = useState(false);
|
||||
const [editRedemptionId, setEditRedemptionId] = useState(0);
|
||||
|
||||
const loadRedemptions = async (startIdx) => {
|
||||
setSearching(true);
|
||||
const res = await API.get(`/api/redemption/?p=${startIdx}`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
if (startIdx === 0) {
|
||||
setRedemptions(data);
|
||||
} else {
|
||||
let newRedemptions = [...redemptions];
|
||||
newRedemptions.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data);
|
||||
setRedemptions(newRedemptions);
|
||||
}
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
setSearching(false);
|
||||
};
|
||||
|
||||
const onPaginationChange = (event, activePage) => {
|
||||
(async () => {
|
||||
if (activePage === Math.ceil(redemptions.length / ITEMS_PER_PAGE)) {
|
||||
// In this case we have to load more data and then append them.
|
||||
await loadRedemptions(activePage);
|
||||
}
|
||||
setActivePage(activePage);
|
||||
})();
|
||||
};
|
||||
|
||||
const searchRedemptions = async (event) => {
|
||||
event.preventDefault();
|
||||
if (searchKeyword === '') {
|
||||
await loadRedemptions(0);
|
||||
setActivePage(0);
|
||||
return;
|
||||
}
|
||||
setSearching(true);
|
||||
const res = await API.get(`/api/redemption/search?keyword=${searchKeyword}`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
setRedemptions(data);
|
||||
setActivePage(0);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
setSearching(false);
|
||||
};
|
||||
|
||||
const handleSearchKeyword = (event) => {
|
||||
setSearchKeyword(event.target.value);
|
||||
};
|
||||
|
||||
const manageRedemptions = async (id, action, value) => {
|
||||
const url = '/api/redemption/';
|
||||
let data = { id };
|
||||
let res;
|
||||
switch (action) {
|
||||
case 'delete':
|
||||
res = await API.delete(url + id);
|
||||
break;
|
||||
case 'status':
|
||||
res = await API.put(url + '?status_only=true', {
|
||||
...data,
|
||||
status: value
|
||||
});
|
||||
break;
|
||||
}
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
showSuccess('操作成功完成!');
|
||||
if (action === 'delete') {
|
||||
await loadRedemptions(0);
|
||||
}
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
|
||||
return res.data;
|
||||
};
|
||||
|
||||
// 处理刷新
|
||||
const handleRefresh = async () => {
|
||||
await loadRedemptions(0);
|
||||
setActivePage(0);
|
||||
setSearchKeyword('');
|
||||
};
|
||||
|
||||
const handleOpenModal = (redemptionId) => {
|
||||
setEditRedemptionId(redemptionId);
|
||||
setOpenModal(true);
|
||||
};
|
||||
|
||||
const handleCloseModal = () => {
|
||||
setOpenModal(false);
|
||||
setEditRedemptionId(0);
|
||||
};
|
||||
|
||||
const handleOkModal = (status) => {
|
||||
if (status === true) {
|
||||
handleCloseModal();
|
||||
handleRefresh();
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadRedemptions(0)
|
||||
.then()
|
||||
.catch((reason) => {
|
||||
showError(reason);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between" mb={5}>
|
||||
<Typography variant="h4">兑换</Typography>
|
||||
|
||||
<Button variant="contained" color="primary" startIcon={<IconPlus />} onClick={() => handleOpenModal(0)}>
|
||||
新建兑换码
|
||||
</Button>
|
||||
</Stack>
|
||||
<Card>
|
||||
<Box component="form" onSubmit={searchRedemptions} noValidate>
|
||||
<TableToolBar filterName={searchKeyword} handleFilterName={handleSearchKeyword} placeholder={'搜索兑换码的ID和名称...'} />
|
||||
</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>
|
||||
</ButtonGroup>
|
||||
</Container>
|
||||
</Toolbar>
|
||||
{searching && <LinearProgress />}
|
||||
<PerfectScrollbar component="div">
|
||||
<TableContainer sx={{ overflow: 'unset' }}>
|
||||
<Table sx={{ minWidth: 800 }}>
|
||||
<RedemptionTableHead />
|
||||
<TableBody>
|
||||
{redemptions.slice(activePage * ITEMS_PER_PAGE, (activePage + 1) * ITEMS_PER_PAGE).map((row) => (
|
||||
<RedemptionTableRow
|
||||
item={row}
|
||||
manageRedemption={manageRedemptions}
|
||||
key={row.id}
|
||||
handleOpenModal={handleOpenModal}
|
||||
setModalRedemptionId={setEditRedemptionId}
|
||||
/>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableContainer>
|
||||
</PerfectScrollbar>
|
||||
<TablePagination
|
||||
page={activePage}
|
||||
component="div"
|
||||
count={redemptions.length + (redemptions.length % ITEMS_PER_PAGE === 0 ? 1 : 0)}
|
||||
rowsPerPage={ITEMS_PER_PAGE}
|
||||
onPageChange={onPaginationChange}
|
||||
rowsPerPageOptions={[ITEMS_PER_PAGE]}
|
||||
/>
|
||||
</Card>
|
||||
<EditeModal open={openModal} onCancel={handleCloseModal} onOk={handleOkModal} redemptiondId={editRedemptionId} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user