fix: copy fails in http environment (#43)

* 🐛 fix: copy fails in http environment

* 🎨 optimize: web UI
This commit is contained in:
Buer 2024-01-19 18:48:01 +08:00 committed by GitHub
parent 45fd814d77
commit 0be687905f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 88 additions and 36 deletions

View File

@ -15,6 +15,7 @@
<link <link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Poppins:wght@400;500;600;700&family=Roboto:wght@400;500;700&display=swap" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Poppins:wght@400;500;600;700&family=Roboto:wght@400;500;700&display=swap"
rel="stylesheet" rel="stylesheet"
media="print" onload="this.media='all'"
/> />
</head> </head>

View File

@ -16,6 +16,7 @@ import NavigationScroll from 'layout/NavigationScroll';
import UserProvider from 'contexts/UserContext'; import UserProvider from 'contexts/UserContext';
import StatusProvider from 'contexts/StatusContext'; import StatusProvider from 'contexts/StatusContext';
import { SnackbarProvider } from 'notistack'; import { SnackbarProvider } from 'notistack';
import CopySnackbar from 'ui-component/Snackbar';
// ==============================|| APP ||============================== // // ==============================|| APP ||============================== //
@ -27,7 +28,12 @@ const App = () => {
<ThemeProvider theme={themes(customization)}> <ThemeProvider theme={themes(customization)}>
<CssBaseline /> <CssBaseline />
<NavigationScroll> <NavigationScroll>
<SnackbarProvider autoHideDuration={5000} maxSnack={3} anchorOrigin={{ vertical: 'top', horizontal: 'right' }}> <SnackbarProvider
autoHideDuration={5000}
maxSnack={3}
anchorOrigin={{ vertical: 'top', horizontal: 'right' }}
Components={{ copy: CopySnackbar }}
>
<UserProvider> <UserProvider>
<StatusProvider> <StatusProvider>
<Routes /> <Routes />

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -2,23 +2,34 @@ export const snackbarConstants = {
Common: { Common: {
ERROR: { ERROR: {
variant: 'error', variant: 'error',
autoHideDuration: 5000 autoHideDuration: 5000,
preventDuplicate: true
}, },
WARNING: { WARNING: {
variant: 'warning', variant: 'warning',
autoHideDuration: 10000 autoHideDuration: 10000,
preventDuplicate: true
}, },
SUCCESS: { SUCCESS: {
variant: 'success', variant: 'success',
autoHideDuration: 1500 autoHideDuration: 1500,
preventDuplicate: true
}, },
INFO: { INFO: {
variant: 'info', variant: 'info',
autoHideDuration: 3000 autoHideDuration: 3000,
preventDuplicate: true
}, },
NOTICE: { NOTICE: {
variant: 'info', variant: 'info',
autoHideDuration: 20000 autoHideDuration: 20000,
preventDuplicate: true
},
COPY: {
variant: 'copy',
persist: true,
preventDuplicate: true,
allowDownload: true
} }
}, },
Mobile: { Mobile: {

View File

@ -0,0 +1,33 @@
import PropTypes from 'prop-types';
import { forwardRef } from 'react';
import { SnackbarContent, useSnackbar } from 'notistack';
import { Alert } from '@mui/material';
const CopySnackbar = forwardRef((props, ref) => {
const { closeSnackbar } = useSnackbar();
return (
<SnackbarContent ref={ref}>
<Alert
severity="info"
variant="filled"
sx={{ width: '100%', whiteSpace: 'normal', overflowWrap: 'break-word' }}
onClose={() => {
closeSnackbar(props.id);
}}
>
{props.message}
</Alert>
</SnackbarContent>
);
});
CopySnackbar.displayName = 'ReportComplete';
CopySnackbar.propTypes = {
props: PropTypes.object,
id: PropTypes.number.isRequired, // 添加 id 的验证
message: PropTypes.any.isRequired // 添加 message 的验证
};
export default CopySnackbar;

View File

@ -33,7 +33,7 @@ import { Box, Avatar } from '@mui/material';
import { alpha } from '@mui/material/styles'; import { alpha } from '@mui/material/styles';
import Card from '@mui/material/Card'; import Card from '@mui/material/Card';
import shapeAvatar from 'assets/images/icons/shape-avatar.svg'; import shapeAvatar from 'assets/images/icons/shape-avatar.svg';
import coverAvatar from 'assets/images/invite/cover.jpg'; import coverAvatar from 'assets/images/invite/cover.webp';
import userAvatar from 'assets/images/users/user-round.svg'; import userAvatar from 'assets/images/users/user-round.svg';
import SvgColor from 'ui-component/SvgColor'; import SvgColor from 'ui-component/SvgColor';

View File

@ -69,6 +69,17 @@ export function showInfo(message) {
enqueueSnackbar(message, getSnackbarOptions('INFO')); enqueueSnackbar(message, getSnackbarOptions('INFO'));
} }
export function copy(text, name = '') {
try {
navigator.clipboard.writeText(text);
} catch (error) {
text = `复制${name}失败,请手动复制:<br /><br />${text}`;
enqueueSnackbar(<SnackbarHTMLContent htmlContent={text} />, getSnackbarOptions('COPY'));
return;
}
showSuccess(`复制${name}成功!`);
}
export async function getOAuthState() { export async function getOAuthState() {
try { try {
const res = await API.get('/api/oauth/state'); const res = await API.get('/api/oauth/state');

View File

@ -161,8 +161,8 @@ const LoginForm = ({ ...others }) => {
submit: null submit: null
}} }}
validationSchema={Yup.object().shape({ validationSchema={Yup.object().shape({
username: Yup.string().max(255).required('Username is required'), username: Yup.string().max(255).required('用户名/邮箱是必填项'),
password: Yup.string().max(255).required('Password is required') password: Yup.string().max(255).required('密码是必填项')
})} })}
onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => { onSubmit={async (values, { setErrors, setStatus, setSubmitting }) => {
const { success, message } = await login(values.username, values.password); const { success, message } = await login(values.username, values.password);

View File

@ -296,7 +296,7 @@ const RegisterForm = ({ ...others }) => {
<Box sx={{ mt: 2 }}> <Box sx={{ mt: 2 }}>
<AnimateButton> <AnimateButton>
<Button disableElevation disabled={isSubmitting} fullWidth size="large" type="submit" variant="contained" color="primary"> <Button disableElevation disabled={isSubmitting} fullWidth size="large" type="submit" variant="contained" color="primary">
Sign up 注册
</Button> </Button>
</AnimateButton> </AnimateButton>
</Box> </Box>

View File

@ -5,7 +5,7 @@ import { useSearchParams } from 'react-router-dom';
import { Button, Stack, Typography, Alert } from '@mui/material'; import { Button, Stack, Typography, Alert } from '@mui/material';
// assets // assets
import { showError, showInfo } from 'utils/common'; import { showError, copy } from 'utils/common';
import { API } from 'utils/api'; import { API } from 'utils/api';
// ===========================|| FIREBASE - REGISTER ||=========================== // // ===========================|| FIREBASE - REGISTER ||=========================== //
@ -25,8 +25,7 @@ const ResetPasswordForm = () => {
if (success) { if (success) {
let password = res.data.data; let password = res.data.data;
setNewPassword(password); setNewPassword(password);
navigator.clipboard.writeText(password); copy(password, '新密码');
showInfo(`新密码已复制到剪贴板:${password}`);
} else { } else {
showError(message); showError(message);
} }

View File

@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import { Tooltip, Stack, Container } from '@mui/material'; import { Tooltip, Stack, Container } from '@mui/material';
import Label from 'ui-component/Label'; import Label from 'ui-component/Label';
import { styled } from '@mui/material/styles'; import { styled } from '@mui/material/styles';
import { showSuccess } from 'utils/common'; import { copy } from 'utils/common';
const TooltipContainer = styled(Container)({ const TooltipContainer = styled(Container)({
maxHeight: '250px', maxHeight: '250px',
@ -28,8 +28,7 @@ const NameLabel = ({ name, models }) => {
variant="ghost" variant="ghost"
key={index} key={index}
onClick={() => { onClick={() => {
navigator.clipboard.writeText(item); copy(item, '模型名称');
showSuccess('复制模型名称成功!');
}} }}
> >
{item} {item}

View File

@ -2,7 +2,7 @@ import { useState, useEffect } from 'react';
import SubCard from 'ui-component/cards/SubCard'; import SubCard from 'ui-component/cards/SubCard';
// import { gridSpacing } from 'store/constant'; // import { gridSpacing } from 'store/constant';
import { API } from 'utils/api'; import { API } from 'utils/api';
import { showError, showSuccess } from 'utils/common'; import { showError, copy } from 'utils/common';
import { Typography, Accordion, AccordionSummary, AccordionDetails, Box, Stack } from '@mui/material'; import { Typography, Accordion, AccordionSummary, AccordionDetails, Box, Stack } from '@mui/material';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import Label from 'ui-component/Label'; import Label from 'ui-component/Label';
@ -50,8 +50,7 @@ const SupportModels = () => {
color="primary" color="primary"
key={model} key={model}
onClick={() => { onClick={() => {
navigator.clipboard.writeText(model); copy(model, '模型名称');
showSuccess('复制模型名称成功!');
}} }}
> >
{model} {model}

View File

@ -19,8 +19,7 @@ import SubCard from 'ui-component/cards/SubCard';
import { IconBrandWechat, IconBrandGithub, IconMail } from '@tabler/icons-react'; import { IconBrandWechat, IconBrandGithub, IconMail } from '@tabler/icons-react';
import Label from 'ui-component/Label'; import Label from 'ui-component/Label';
import { API } from 'utils/api'; import { API } from 'utils/api';
import { showError, showSuccess } from 'utils/common'; import { showError, showSuccess, onGitHubOAuthClicked, copy } from 'utils/common';
import { onGitHubOAuthClicked } from 'utils/common';
import * as Yup from 'yup'; import * as Yup from 'yup';
import WechatModal from 'views/Authentication/AuthForms/WechatModal'; import WechatModal from 'views/Authentication/AuthForms/WechatModal';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
@ -93,8 +92,7 @@ export default function Profile() {
const { success, message, data } = res.data; const { success, message, data } = res.data;
if (success) { if (success) {
setInputs((inputs) => ({ ...inputs, access_token: data })); setInputs((inputs) => ({ ...inputs, access_token: data }));
navigator.clipboard.writeText(data); copy(data, '令牌');
showSuccess(`令牌已重置并已复制到剪贴板`);
} else { } else {
showError(message); showError(message);
} }

View File

@ -18,7 +18,7 @@ import {
import Label from 'ui-component/Label'; import Label from 'ui-component/Label';
import TableSwitch from 'ui-component/Switch'; import TableSwitch from 'ui-component/Switch';
import { timestamp2string, renderQuota, showSuccess } from 'utils/common'; import { timestamp2string, renderQuota, copy } from 'utils/common';
import { IconDotsVertical, IconEdit, IconTrash } from '@tabler/icons-react'; import { IconDotsVertical, IconEdit, IconTrash } from '@tabler/icons-react';
@ -83,8 +83,7 @@ export default function RedemptionTableRow({ item, manageRedemption, handleOpenM
variant="contained" variant="contained"
color="primary" color="primary"
onClick={() => { onClick={() => {
navigator.clipboard.writeText(item.key); copy(item.key, '兑换码');
showSuccess('已复制到剪贴板!');
}} }}
> >
复制 复制

View File

@ -20,7 +20,7 @@ import {
} from '@mui/material'; } from '@mui/material';
import TableSwitch from 'ui-component/Switch'; import TableSwitch from 'ui-component/Switch';
import { renderQuota, showSuccess, timestamp2string } from 'utils/common'; import { renderQuota, timestamp2string, copy } from 'utils/common';
import { IconDotsVertical, IconEdit, IconTrash, IconCaretDownFilled } from '@tabler/icons-react'; import { IconDotsVertical, IconEdit, IconTrash, IconCaretDownFilled } from '@tabler/icons-react';
@ -141,8 +141,7 @@ export default function TokensTableRow({ item, manageToken, handleOpenModal, set
if (type === 'link') { if (type === 'link') {
window.open(text); window.open(text);
} else { } else {
navigator.clipboard.writeText(text); copy(text, '链接');
showSuccess('已复制到剪贴板!');
} }
handleCloseMenu(); handleCloseMenu();
}; };
@ -211,8 +210,7 @@ export default function TokensTableRow({ item, manageToken, handleOpenModal, set
<Button <Button
color="primary" color="primary"
onClick={() => { onClick={() => {
navigator.clipboard.writeText(`sk-${item.key}`); copy(`sk-${item.key}`, '令牌');
showSuccess('已复制到剪贴板!');
}} }}
> >
复制 复制

View File

@ -4,7 +4,7 @@ import SubCard from 'ui-component/cards/SubCard';
import inviteImage from 'assets/images/invite/cwok_casual_19.webp'; import inviteImage from 'assets/images/invite/cwok_casual_19.webp';
import { useState } from 'react'; import { useState } from 'react';
import { API } from 'utils/api'; import { API } from 'utils/api';
import { showError, showSuccess } from 'utils/common'; import { showError, copy } from 'utils/common';
const InviteCard = () => { const InviteCard = () => {
const theme = useTheme(); const theme = useTheme();
@ -12,8 +12,7 @@ const InviteCard = () => {
const handleInviteUrl = async () => { const handleInviteUrl = async () => {
if (inviteUl) { if (inviteUl) {
navigator.clipboard.writeText(inviteUl); copy(inviteUl, '邀请链接');
showSuccess(`邀请链接已复制到剪切板`);
return; return;
} }
@ -23,8 +22,7 @@ const InviteCard = () => {
if (success) { if (success) {
let link = `${window.location.origin}/register?aff=${data}`; let link = `${window.location.origin}/register?aff=${data}`;
setInviteUrl(link); setInviteUrl(link);
navigator.clipboard.writeText(link); copy(link, '邀请链接');
showSuccess(`邀请链接已复制到剪切板`);
} else { } else {
showError(message); showError(message);
} }