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:
26
web/berry/src/utils/api.js
Normal file
26
web/berry/src/utils/api.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { showError } from './common';
|
||||
import axios from 'axios';
|
||||
import { store } from 'store/index';
|
||||
import { LOGIN } from 'store/actions';
|
||||
import config from 'config';
|
||||
|
||||
export const API = axios.create({
|
||||
baseURL: process.env.REACT_APP_SERVER ? process.env.REACT_APP_SERVER : '/'
|
||||
});
|
||||
|
||||
API.interceptors.response.use(
|
||||
(response) => response,
|
||||
(error) => {
|
||||
if (error.response?.status === 401) {
|
||||
localStorage.removeItem('user');
|
||||
store.dispatch({ type: LOGIN, payload: null });
|
||||
window.location.href = config.basename + '/login';
|
||||
}
|
||||
|
||||
if (error.response?.data?.message) {
|
||||
error.message = error.response.data.message;
|
||||
}
|
||||
|
||||
showError(error);
|
||||
}
|
||||
);
|
||||
96
web/berry/src/utils/chart.js
Normal file
96
web/berry/src/utils/chart.js
Normal file
@@ -0,0 +1,96 @@
|
||||
export function getLastSevenDays() {
|
||||
const dates = [];
|
||||
for (let i = 6; i >= 0; i--) {
|
||||
const d = new Date();
|
||||
d.setDate(d.getDate() - i);
|
||||
const month = '' + (d.getMonth() + 1);
|
||||
const day = '' + d.getDate();
|
||||
const year = d.getFullYear();
|
||||
|
||||
const formattedDate = [year, month.padStart(2, '0'), day.padStart(2, '0')].join('-');
|
||||
dates.push(formattedDate);
|
||||
}
|
||||
return dates;
|
||||
}
|
||||
|
||||
export function getTodayDay() {
|
||||
let today = new Date();
|
||||
return today.toISOString().slice(0, 10);
|
||||
}
|
||||
|
||||
export function generateChartOptions(data, unit) {
|
||||
const dates = data.map((item) => item.date);
|
||||
const values = data.map((item) => item.value);
|
||||
|
||||
const minDate = dates[0];
|
||||
const maxDate = dates[dates.length - 1];
|
||||
|
||||
const minValue = Math.min(...values);
|
||||
const maxValue = Math.max(...values);
|
||||
|
||||
return {
|
||||
series: [
|
||||
{
|
||||
data: values
|
||||
}
|
||||
],
|
||||
type: 'line',
|
||||
height: 90,
|
||||
options: {
|
||||
chart: {
|
||||
sparkline: {
|
||||
enabled: true
|
||||
}
|
||||
},
|
||||
dataLabels: {
|
||||
enabled: false
|
||||
},
|
||||
colors: ['#fff'],
|
||||
fill: {
|
||||
type: 'solid',
|
||||
opacity: 1
|
||||
},
|
||||
stroke: {
|
||||
curve: 'smooth',
|
||||
width: 3
|
||||
},
|
||||
xaxis: {
|
||||
categories: dates,
|
||||
labels: {
|
||||
show: false
|
||||
},
|
||||
min: minDate,
|
||||
max: maxDate
|
||||
},
|
||||
yaxis: {
|
||||
min: minValue,
|
||||
max: maxValue,
|
||||
labels: {
|
||||
show: false
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
theme: 'dark',
|
||||
fixed: {
|
||||
enabled: false
|
||||
},
|
||||
x: {
|
||||
format: 'yyyy-MM-dd'
|
||||
},
|
||||
y: {
|
||||
formatter: function (val) {
|
||||
return val + ` ${unit}`;
|
||||
},
|
||||
title: {
|
||||
formatter: function () {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
},
|
||||
marker: {
|
||||
show: false
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
188
web/berry/src/utils/common.js
Normal file
188
web/berry/src/utils/common.js
Normal file
@@ -0,0 +1,188 @@
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import { snackbarConstants } from 'constants/SnackbarConstants';
|
||||
import { API } from './api';
|
||||
|
||||
export function getSystemName() {
|
||||
let system_name = localStorage.getItem('system_name');
|
||||
if (!system_name) return 'One API';
|
||||
return system_name;
|
||||
}
|
||||
|
||||
export function isMobile() {
|
||||
return window.innerWidth <= 600;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
export function SnackbarHTMLContent({ htmlContent }) {
|
||||
return <div dangerouslySetInnerHTML={{ __html: htmlContent }} />;
|
||||
}
|
||||
|
||||
export function getSnackbarOptions(variant) {
|
||||
let options = snackbarConstants.Common[variant];
|
||||
if (isMobile()) {
|
||||
// 合并 options 和 snackbarConstants.Mobile
|
||||
options = { ...options, ...snackbarConstants.Mobile };
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
export function showError(error) {
|
||||
if (error.message) {
|
||||
if (error.name === 'AxiosError') {
|
||||
switch (error.response.status) {
|
||||
case 429:
|
||||
enqueueSnackbar('错误:请求次数过多,请稍后再试!', getSnackbarOptions('ERROR'));
|
||||
break;
|
||||
case 500:
|
||||
enqueueSnackbar('错误:服务器内部错误,请联系管理员!', getSnackbarOptions('ERROR'));
|
||||
break;
|
||||
case 405:
|
||||
enqueueSnackbar('本站仅作演示之用,无服务端!', getSnackbarOptions('INFO'));
|
||||
break;
|
||||
default:
|
||||
enqueueSnackbar('错误:' + error.message, getSnackbarOptions('ERROR'));
|
||||
}
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
enqueueSnackbar('错误:' + error, getSnackbarOptions('ERROR'));
|
||||
}
|
||||
}
|
||||
|
||||
export function showNotice(message, isHTML = false) {
|
||||
if (isHTML) {
|
||||
enqueueSnackbar(<SnackbarHTMLContent htmlContent={message} />, getSnackbarOptions('INFO'));
|
||||
} else {
|
||||
enqueueSnackbar(message, getSnackbarOptions('INFO'));
|
||||
}
|
||||
}
|
||||
|
||||
export function showWarning(message) {
|
||||
enqueueSnackbar(message, getSnackbarOptions('WARNING'));
|
||||
}
|
||||
|
||||
export function showSuccess(message) {
|
||||
enqueueSnackbar(message, getSnackbarOptions('SUCCESS'));
|
||||
}
|
||||
|
||||
export function showInfo(message) {
|
||||
enqueueSnackbar(message, getSnackbarOptions('INFO'));
|
||||
}
|
||||
|
||||
export async function getOAuthState() {
|
||||
const res = await API.get('/api/oauth/state');
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
return data;
|
||||
} else {
|
||||
showError(message);
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export async function onGitHubOAuthClicked(github_client_id, openInNewTab = false) {
|
||||
const state = await getOAuthState();
|
||||
if (!state) return;
|
||||
let url = `https://github.com/login/oauth/authorize?client_id=${github_client_id}&state=${state}&scope=user:email`;
|
||||
if (openInNewTab) {
|
||||
window.open(url);
|
||||
} else {
|
||||
window.location.href = url;
|
||||
}
|
||||
}
|
||||
|
||||
export function isAdmin() {
|
||||
let user = localStorage.getItem('user');
|
||||
if (!user) return false;
|
||||
user = JSON.parse(user);
|
||||
return user.role >= 10;
|
||||
}
|
||||
|
||||
export function timestamp2string(timestamp) {
|
||||
let date = new Date(timestamp * 1000);
|
||||
let year = date.getFullYear().toString();
|
||||
let month = (date.getMonth() + 1).toString();
|
||||
let day = date.getDate().toString();
|
||||
let hour = date.getHours().toString();
|
||||
let minute = date.getMinutes().toString();
|
||||
let second = date.getSeconds().toString();
|
||||
if (month.length === 1) {
|
||||
month = '0' + month;
|
||||
}
|
||||
if (day.length === 1) {
|
||||
day = '0' + day;
|
||||
}
|
||||
if (hour.length === 1) {
|
||||
hour = '0' + hour;
|
||||
}
|
||||
if (minute.length === 1) {
|
||||
minute = '0' + minute;
|
||||
}
|
||||
if (second.length === 1) {
|
||||
second = '0' + second;
|
||||
}
|
||||
return year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second;
|
||||
}
|
||||
|
||||
export function calculateQuota(quota, digits = 2) {
|
||||
let quotaPerUnit = localStorage.getItem('quota_per_unit');
|
||||
quotaPerUnit = parseFloat(quotaPerUnit);
|
||||
|
||||
return (quota / quotaPerUnit).toFixed(digits);
|
||||
}
|
||||
|
||||
export function renderQuota(quota, digits = 2) {
|
||||
let displayInCurrency = localStorage.getItem('display_in_currency');
|
||||
displayInCurrency = displayInCurrency === 'true';
|
||||
if (displayInCurrency) {
|
||||
return '$' + calculateQuota(quota, digits);
|
||||
}
|
||||
return renderNumber(quota);
|
||||
}
|
||||
|
||||
export const verifyJSON = (str) => {
|
||||
try {
|
||||
JSON.parse(str);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
export function renderNumber(num) {
|
||||
if (num >= 1000000000) {
|
||||
return (num / 1000000000).toFixed(1) + 'B';
|
||||
} else if (num >= 1000000) {
|
||||
return (num / 1000000).toFixed(1) + 'M';
|
||||
} else if (num >= 10000) {
|
||||
return (num / 1000).toFixed(1) + 'k';
|
||||
} else {
|
||||
return num;
|
||||
}
|
||||
}
|
||||
|
||||
export function renderQuotaWithPrompt(quota, digits) {
|
||||
let displayInCurrency = localStorage.getItem('display_in_currency');
|
||||
displayInCurrency = displayInCurrency === 'true';
|
||||
if (displayInCurrency) {
|
||||
return `(等价金额:${renderQuota(quota, digits)})`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
export function downloadTextAsFile(text, filename) {
|
||||
let blob = new Blob([text], { type: 'text/plain;charset=utf-8' });
|
||||
let url = URL.createObjectURL(blob);
|
||||
let a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
}
|
||||
|
||||
export function removeTrailingSlash(url) {
|
||||
if (url.endsWith('/')) {
|
||||
return url.slice(0, -1);
|
||||
} else {
|
||||
return url;
|
||||
}
|
||||
}
|
||||
34
web/berry/src/utils/password-strength.js
Normal file
34
web/berry/src/utils/password-strength.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/**
|
||||
* Password validator for login pages
|
||||
*/
|
||||
import value from 'assets/scss/_themes-vars.module.scss';
|
||||
|
||||
// has number
|
||||
const hasNumber = (number) => new RegExp(/[0-9]/).test(number);
|
||||
|
||||
// has mix of small and capitals
|
||||
const hasMixed = (number) => new RegExp(/[a-z]/).test(number) && new RegExp(/[A-Z]/).test(number);
|
||||
|
||||
// has special chars
|
||||
const hasSpecial = (number) => new RegExp(/[!#@$%^&*)(+=._-]/).test(number);
|
||||
|
||||
// set color based on password strength
|
||||
export const strengthColor = (count) => {
|
||||
if (count < 2) return { label: 'Poor', color: value.errorMain };
|
||||
if (count < 3) return { label: 'Weak', color: value.warningDark };
|
||||
if (count < 4) return { label: 'Normal', color: value.orangeMain };
|
||||
if (count < 5) return { label: 'Good', color: value.successMain };
|
||||
if (count < 6) return { label: 'Strong', color: value.successDark };
|
||||
return { label: 'Poor', color: value.errorMain };
|
||||
};
|
||||
|
||||
// password strength indicator
|
||||
export const strengthIndicator = (number) => {
|
||||
let strengths = 0;
|
||||
if (number.length > 5) strengths += 1;
|
||||
if (number.length > 7) strengths += 1;
|
||||
if (hasNumber(number)) strengths += 1;
|
||||
if (hasSpecial(number)) strengths += 1;
|
||||
if (hasMixed(number)) strengths += 1;
|
||||
return strengths;
|
||||
};
|
||||
20
web/berry/src/utils/route-guard/AuthGuard.js
Normal file
20
web/berry/src/utils/route-guard/AuthGuard.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import { useEffect, useContext } from 'react';
|
||||
import { UserContext } from 'contexts/UserContext';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
|
||||
const AuthGuard = ({ children }) => {
|
||||
const account = useSelector((state) => state.account);
|
||||
const { isUserLoaded } = useContext(UserContext);
|
||||
const navigate = useNavigate();
|
||||
useEffect(() => {
|
||||
if (isUserLoaded && !account.user) {
|
||||
navigate('/login');
|
||||
return;
|
||||
}
|
||||
}, [account, navigate, isUserLoaded]);
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
export default AuthGuard;
|
||||
Reference in New Issue
Block a user