chore: lint fix

This commit is contained in:
CaIon
2024-03-23 21:24:39 +08:00
parent 15e7307320
commit 962dc984f4
55 changed files with 5263 additions and 3589 deletions

View File

@@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import { API, showError } from '../../helpers';
import { marked } from 'marked';
import {Layout} from "@douyinfe/semi-ui";
import { Layout } from '@douyinfe/semi-ui';
const About = () => {
const [about, setAbout] = useState('');
@@ -31,37 +31,42 @@ const About = () => {
return (
<>
{
aboutLoaded && about === '' ? <>
{aboutLoaded && about === '' ? (
<>
<Layout>
<Layout.Header>
<h3>关于</h3>
</Layout.Header>
<Layout.Content>
<p>
可在设置页面设置关于内容支持 HTML & Markdown
</p>
<p>可在设置页面设置关于内容支持 HTML & Markdown</p>
new-api项目仓库地址
<a href='https://github.com/Calcium-Ion/new-api'>
https://github.com/Calcium-Ion/new-api
</a>
<p>
NewAPI © 2023 CalciumIon | 基于 One API v0.5.4 © 2023 JustSong本项目根据MIT许可证授权
NewAPI © 2023 CalciumIon | 基于 One API v0.5.4 © 2023
JustSong本项目根据MIT许可证授权
</p>
</Layout.Content>
</Layout>
</> : <>
{
about.startsWith('https://') ? <iframe
</>
) : (
<>
{about.startsWith('https://') ? (
<iframe
src={about}
style={{ width: '100%', height: '100vh', border: 'none' }}
/> : <div style={{ fontSize: 'larger' }} dangerouslySetInnerHTML={{ __html: about }}></div>
}
/>
) : (
<div
style={{ fontSize: 'larger' }}
dangerouslySetInnerHTML={{ __html: about }}
></div>
)}
</>
}
)}
</>
);
};
export default About;

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +1,18 @@
import React from 'react';
import ChannelsTable from '../../components/ChannelsTable';
import {Layout} from "@douyinfe/semi-ui";
import { Layout } from '@douyinfe/semi-ui';
const File = () => (
<>
<Layout>
<Layout.Header>
<h3>管理渠道</h3>
</Layout.Header>
<Layout.Content>
<ChannelsTable/>
</Layout.Content>
</Layout>
</>
<>
<Layout>
<Layout.Header>
<h3>管理渠道</h3>
</Layout.Header>
<Layout.Content>
<ChannelsTable />
</Layout.Content>
</Layout>
</>
);
export default File;

View File

@@ -11,5 +11,4 @@ const Chat = () => {
);
};
export default Chat;

View File

@@ -1,364 +1,423 @@
import React, {useEffect, useRef, useState} from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { initVChartSemiTheme } from '@visactor/vchart-semi-theme';
import {Button, Col, Form, Layout, Row, Spin} from "@douyinfe/semi-ui";
import { Button, Col, Form, Layout, Row, Spin } from '@douyinfe/semi-ui';
import VChart from '@visactor/vchart';
import {API, isAdmin, showError, timestamp2string, timestamp2string1} from "../../helpers";
import {
getQuotaWithUnit, modelColorMap,
renderNumber,
renderQuota,
renderQuotaNumberWithDigit,
stringToColor
} from "../../helpers/render";
API,
isAdmin,
showError,
timestamp2string,
timestamp2string1,
} from '../../helpers';
import {
getQuotaWithUnit,
modelColorMap,
renderNumber,
renderQuota,
renderQuotaNumberWithDigit,
stringToColor,
} from '../../helpers/render';
const Detail = (props) => {
const formRef = useRef();
let now = new Date();
const [inputs, setInputs] = useState({
username: '',
token_name: '',
model_name: '',
start_timestamp: localStorage.getItem('data_export_default_time') === 'hour' ? timestamp2string(now.getTime() / 1000 - 86400) : (localStorage.getItem('data_export_default_time') === 'week' ? timestamp2string(now.getTime() / 1000 - 86400 * 30) : timestamp2string(now.getTime() / 1000 - 86400 * 7)),
end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
channel: '',
data_export_default_time: ''
});
const {username, model_name, start_timestamp, end_timestamp, channel} = inputs;
const isAdminUser = isAdmin();
const initialized = useRef(false)
const [modelDataChart, setModelDataChart] = useState(null);
const [modelDataPieChart, setModelDataPieChart] = useState(null);
const [loading, setLoading] = useState(false);
const [quotaData, setQuotaData] = useState([]);
const [consumeQuota, setConsumeQuota] = useState(0);
const [times, setTimes] = useState(0);
const [dataExportDefaultTime, setDataExportDefaultTime] = useState(localStorage.getItem('data_export_default_time') || 'hour');
const formRef = useRef();
let now = new Date();
const [inputs, setInputs] = useState({
username: '',
token_name: '',
model_name: '',
start_timestamp:
localStorage.getItem('data_export_default_time') === 'hour'
? timestamp2string(now.getTime() / 1000 - 86400)
: localStorage.getItem('data_export_default_time') === 'week'
? timestamp2string(now.getTime() / 1000 - 86400 * 30)
: timestamp2string(now.getTime() / 1000 - 86400 * 7),
end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
channel: '',
data_export_default_time: '',
});
const { username, model_name, start_timestamp, end_timestamp, channel } =
inputs;
const isAdminUser = isAdmin();
const initialized = useRef(false);
const [modelDataChart, setModelDataChart] = useState(null);
const [modelDataPieChart, setModelDataPieChart] = useState(null);
const [loading, setLoading] = useState(false);
const [quotaData, setQuotaData] = useState([]);
const [consumeQuota, setConsumeQuota] = useState(0);
const [times, setTimes] = useState(0);
const [dataExportDefaultTime, setDataExportDefaultTime] = useState(
localStorage.getItem('data_export_default_time') || 'hour',
);
const handleInputChange = (value, name) => {
if (name === 'data_export_default_time') {
setDataExportDefaultTime(value);
return
}
setInputs((inputs) => ({...inputs, [name]: value}));
};
const spec_line = {
type: 'bar',
data: [
{
id: 'barData',
values: []
}
],
xField: 'Time',
yField: 'Usage',
seriesField: 'Model',
stack: true,
legends: {
visible: true
},
title: {
visible: true,
text: '模型消耗分布',
subtext: '0'
},
bar: {
// The state style of bar
state: {
hover: {
stroke: '#000',
lineWidth: 1
}
}
},
tooltip: {
mark: {
content: [
{
key: datum => datum['Model'],
value: datum => renderQuotaNumberWithDigit(parseFloat(datum['Usage']), 4)
}
]
},
dimension: {
content: [
{
key: datum => datum['Model'],
value: datum => datum['Usage']
}
],
updateContent: array => {
// sort by value
array.sort((a, b) => b.value - a.value);
// add $
let sum = 0;
for (let i = 0; i < array.length; i++) {
sum += parseFloat(array[i].value);
array[i].value = renderQuotaNumberWithDigit(parseFloat(array[i].value), 4);
}
// add to first
array.unshift({
key: '总计',
value: renderQuotaNumberWithDigit(sum, 4)
});
return array;
}
}
},
color: {
specified: modelColorMap
}
};
const spec_pie = {
type: 'pie',
data: [
{
id: 'id0',
values: [
{type: 'null', value: '0'},
]
}
],
outerRadius: 0.8,
innerRadius: 0.5,
padAngle: 0.6,
valueField: 'value',
categoryField: 'type',
pie: {
style: {
cornerRadius: 10
},
state: {
hover: {
outerRadius: 0.85,
stroke: '#000',
lineWidth: 1
},
selected: {
outerRadius: 0.85,
stroke: '#000',
lineWidth: 1
}
}
},
title: {
visible: true,
text: '模型调用次数占比'
},
legends: {
visible: true,
orient: 'left'
},
label: {
visible: true
},
tooltip: {
mark: {
content: [
{
key: datum => datum['type'],
value: datum => renderNumber(datum['value'])
}
]
}
},
color: {
specified: modelColorMap
}
};
const loadQuotaData = async (lineChart, pieChart) => {
setLoading(true);
let url = '';
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
if (isAdminUser) {
url = `/api/data/?username=${username}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
} else {
url = `/api/data/self/?start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
}
const res = await API.get(url);
const {success, message, data} = res.data;
if (success) {
setQuotaData(data);
if (data.length === 0) {
data.push({
'count': 0,
'model_name': '无数据',
'quota': 0,
'created_at': now.getTime() / 1000
})
}
// 根据dataExportDefaultTime重制时间粒度
let timeGranularity = 3600;
if (dataExportDefaultTime === 'day') {
timeGranularity = 86400;
} else if (dataExportDefaultTime === 'week') {
timeGranularity = 604800;
}
data.forEach(item => {
item['created_at'] = Math.floor(item['created_at'] / timeGranularity) * timeGranularity;
});
updateChart(lineChart, pieChart, data);
} else {
showError(message);
}
setLoading(false);
};
const refresh = async () => {
await loadQuotaData(modelDataChart, modelDataPieChart);
};
const initChart = async () => {
let lineChart = modelDataChart
if (!modelDataChart) {
lineChart = new VChart(spec_line, {dom: 'model_data'});
setModelDataChart(lineChart);
lineChart.renderAsync();
}
let pieChart = modelDataPieChart
if (!modelDataPieChart) {
pieChart = new VChart(spec_pie, {dom: 'model_pie'});
setModelDataPieChart(pieChart);
pieChart.renderAsync();
}
console.log('init vchart');
await loadQuotaData(lineChart, pieChart)
const handleInputChange = (value, name) => {
if (name === 'data_export_default_time') {
setDataExportDefaultTime(value);
return;
}
setInputs((inputs) => ({ ...inputs, [name]: value }));
};
const updateChart = (lineChart, pieChart, data) => {
if (isAdminUser) {
// 将所有用户合并
}
let pieData = [];
let lineData = [];
let consumeQuota = 0;
let times = 0;
for (let i = 0; i < data.length; i++) {
const item = data[i];
consumeQuota += item.quota;
times += item.count;
// 合并model_name
let pieItem = pieData.find(it => it.type === item.model_name);
if (pieItem) {
pieItem.value += item.count;
} else {
pieData.push({
"type": item.model_name,
"value": item.count
});
}
// 合并created_at和model_name 为 lineData, created_at 数据类型是小时的时间戳
// 转换日期格式
let createTime = timestamp2string1(item.created_at, dataExportDefaultTime);
let lineItem = lineData.find(it => it.Time === createTime && it.Model === item.model_name);
if (lineItem) {
lineItem.Usage += parseFloat(getQuotaWithUnit(item.quota));
} else {
lineData.push({
"Time": createTime,
"Model": item.model_name,
"Usage": parseFloat(getQuotaWithUnit(item.quota))
});
}
}
setConsumeQuota(consumeQuota);
setTimes(times);
const spec_line = {
type: 'bar',
data: [
{
id: 'barData',
values: [],
},
],
xField: 'Time',
yField: 'Usage',
seriesField: 'Model',
stack: true,
legends: {
visible: true,
},
title: {
visible: true,
text: '模型消耗分布',
subtext: '0',
},
bar: {
// The state style of bar
state: {
hover: {
stroke: '#000',
lineWidth: 1,
},
},
},
tooltip: {
mark: {
content: [
{
key: (datum) => datum['Model'],
value: (datum) =>
renderQuotaNumberWithDigit(parseFloat(datum['Usage']), 4),
},
],
},
dimension: {
content: [
{
key: (datum) => datum['Model'],
value: (datum) => datum['Usage'],
},
],
updateContent: (array) => {
// sort by value
array.sort((a, b) => b.value - a.value);
// add $
let sum = 0;
for (let i = 0; i < array.length; i++) {
sum += parseFloat(array[i].value);
array[i].value = renderQuotaNumberWithDigit(
parseFloat(array[i].value),
4,
);
}
// add to first
array.unshift({
key: '总计',
value: renderQuotaNumberWithDigit(sum, 4),
});
return array;
},
},
},
color: {
specified: modelColorMap,
},
};
// sort by count
pieData.sort((a, b) => b.value - a.value);
spec_pie.title.subtext = `总计:${renderNumber(times)}`;
spec_pie.data[0].values = pieData;
const spec_pie = {
type: 'pie',
data: [
{
id: 'id0',
values: [{ type: 'null', value: '0' }],
},
],
outerRadius: 0.8,
innerRadius: 0.5,
padAngle: 0.6,
valueField: 'value',
categoryField: 'type',
pie: {
style: {
cornerRadius: 10,
},
state: {
hover: {
outerRadius: 0.85,
stroke: '#000',
lineWidth: 1,
},
selected: {
outerRadius: 0.85,
stroke: '#000',
lineWidth: 1,
},
},
},
title: {
visible: true,
text: '模型调用次数占比',
},
legends: {
visible: true,
orient: 'left',
},
label: {
visible: true,
},
tooltip: {
mark: {
content: [
{
key: (datum) => datum['type'],
value: (datum) => renderNumber(datum['value']),
},
],
},
},
color: {
specified: modelColorMap,
},
};
spec_line.title.subtext = `总计:${renderQuota(consumeQuota, 2)}`;
spec_line.data[0].values = lineData;
pieChart.updateSpec(spec_pie);
lineChart.updateSpec(spec_line);
const loadQuotaData = async (lineChart, pieChart) => {
setLoading(true);
// pieChart.updateData('id0', pieData);
// lineChart.updateData('barData', lineData);
pieChart.reLayout();
lineChart.reLayout();
let url = '';
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
if (isAdminUser) {
url = `/api/data/?username=${username}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
} else {
url = `/api/data/self/?start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
}
const res = await API.get(url);
const { success, message, data } = res.data;
if (success) {
setQuotaData(data);
if (data.length === 0) {
data.push({
count: 0,
model_name: '无数据',
quota: 0,
created_at: now.getTime() / 1000,
});
}
// 根据dataExportDefaultTime重制时间粒度
let timeGranularity = 3600;
if (dataExportDefaultTime === 'day') {
timeGranularity = 86400;
} else if (dataExportDefaultTime === 'week') {
timeGranularity = 604800;
}
data.forEach((item) => {
item['created_at'] =
Math.floor(item['created_at'] / timeGranularity) * timeGranularity;
});
updateChart(lineChart, pieChart, data);
} else {
showError(message);
}
setLoading(false);
};
useEffect(() => {
// setDataExportDefaultTime(localStorage.getItem('data_export_default_time'));
// if (dataExportDefaultTime === 'day') {
// // 设置开始时间为7天前
// let st = timestamp2string(now.getTime() / 1000 - 86400 * 7)
// inputs.start_timestamp = st;
// formRef.current.formApi.setValue('start_timestamp', st);
// }
if (!initialized.current) {
initVChartSemiTheme({
isWatchingThemeSwitch: true,
});
initialized.current = true;
initChart();
}
}, []);
const refresh = async () => {
await loadQuotaData(modelDataChart, modelDataPieChart);
};
return (
<>
<Layout>
<Layout.Header>
<h3>数据看板</h3>
</Layout.Header>
<Layout.Content>
<Form ref={formRef} layout='horizontal' style={{marginTop: 10}}>
<>
<Form.DatePicker field="start_timestamp" label='起始时间' style={{width: 272}}
initValue={start_timestamp}
value={start_timestamp} type='dateTime'
name='start_timestamp'
onChange={value => handleInputChange(value, 'start_timestamp')}/>
<Form.DatePicker field="end_timestamp" fluid label='结束时间' style={{width: 272}}
initValue={end_timestamp}
value={end_timestamp} type='dateTime'
name='end_timestamp'
onChange={value => handleInputChange(value, 'end_timestamp')}/>
<Form.Select field="data_export_default_time" label='时间粒度' style={{width: 176}}
initValue={dataExportDefaultTime}
placeholder={'时间粒度'} name='data_export_default_time'
optionList={
[
{label: '小时', value: 'hour'},
{label: '天', value: 'day'},
{label: '周', value: 'week'}
]
}
onChange={value => handleInputChange(value, 'data_export_default_time')}>
</Form.Select>
{
isAdminUser && <>
<Form.Input field="username" label='用户名称' style={{width: 176}} value={username}
placeholder={'可选值'} name='username'
onChange={value => handleInputChange(value, 'username')}/>
</>
}
<Form.Section>
<Button label='查询' type="primary" htmlType="submit" className="btn-margin-right"
onClick={refresh} loading={loading}>查询</Button>
</Form.Section>
</>
</Form>
<Spin spinning={loading}>
<div style={{height: 500}}>
<div id="model_pie" style={{width: '100%', minWidth: 100}}></div>
</div>
<div style={{height: 500}}>
<div id="model_data" style={{width: '100%', minWidth: 100}}></div>
</div>
</Spin>
</Layout.Content>
</Layout>
</>
);
const initChart = async () => {
let lineChart = modelDataChart;
if (!modelDataChart) {
lineChart = new VChart(spec_line, { dom: 'model_data' });
setModelDataChart(lineChart);
lineChart.renderAsync();
}
let pieChart = modelDataPieChart;
if (!modelDataPieChart) {
pieChart = new VChart(spec_pie, { dom: 'model_pie' });
setModelDataPieChart(pieChart);
pieChart.renderAsync();
}
console.log('init vchart');
await loadQuotaData(lineChart, pieChart);
};
const updateChart = (lineChart, pieChart, data) => {
if (isAdminUser) {
// 将所有用户合并
}
let pieData = [];
let lineData = [];
let consumeQuota = 0;
let times = 0;
for (let i = 0; i < data.length; i++) {
const item = data[i];
consumeQuota += item.quota;
times += item.count;
// 合并model_name
let pieItem = pieData.find((it) => it.type === item.model_name);
if (pieItem) {
pieItem.value += item.count;
} else {
pieData.push({
type: item.model_name,
value: item.count,
});
}
// 合并created_at和model_name 为 lineData, created_at 数据类型是小时的时间戳
// 转换日期格式
let createTime = timestamp2string1(
item.created_at,
dataExportDefaultTime,
);
let lineItem = lineData.find(
(it) => it.Time === createTime && it.Model === item.model_name,
);
if (lineItem) {
lineItem.Usage += parseFloat(getQuotaWithUnit(item.quota));
} else {
lineData.push({
Time: createTime,
Model: item.model_name,
Usage: parseFloat(getQuotaWithUnit(item.quota)),
});
}
}
setConsumeQuota(consumeQuota);
setTimes(times);
// sort by count
pieData.sort((a, b) => b.value - a.value);
spec_pie.title.subtext = `总计:${renderNumber(times)}`;
spec_pie.data[0].values = pieData;
spec_line.title.subtext = `总计:${renderQuota(consumeQuota, 2)}`;
spec_line.data[0].values = lineData;
pieChart.updateSpec(spec_pie);
lineChart.updateSpec(spec_line);
// pieChart.updateData('id0', pieData);
// lineChart.updateData('barData', lineData);
pieChart.reLayout();
lineChart.reLayout();
};
useEffect(() => {
// setDataExportDefaultTime(localStorage.getItem('data_export_default_time'));
// if (dataExportDefaultTime === 'day') {
// // 设置开始时间为7天前
// let st = timestamp2string(now.getTime() / 1000 - 86400 * 7)
// inputs.start_timestamp = st;
// formRef.current.formApi.setValue('start_timestamp', st);
// }
if (!initialized.current) {
initVChartSemiTheme({
isWatchingThemeSwitch: true,
});
initialized.current = true;
initChart();
}
}, []);
return (
<>
<Layout>
<Layout.Header>
<h3>数据看板</h3>
</Layout.Header>
<Layout.Content>
<Form ref={formRef} layout='horizontal' style={{ marginTop: 10 }}>
<>
<Form.DatePicker
field='start_timestamp'
label='起始时间'
style={{ width: 272 }}
initValue={start_timestamp}
value={start_timestamp}
type='dateTime'
name='start_timestamp'
onChange={(value) =>
handleInputChange(value, 'start_timestamp')
}
/>
<Form.DatePicker
field='end_timestamp'
fluid
label='结束时间'
style={{ width: 272 }}
initValue={end_timestamp}
value={end_timestamp}
type='dateTime'
name='end_timestamp'
onChange={(value) => handleInputChange(value, 'end_timestamp')}
/>
<Form.Select
field='data_export_default_time'
label='时间粒度'
style={{ width: 176 }}
initValue={dataExportDefaultTime}
placeholder={'时间粒度'}
name='data_export_default_time'
optionList={[
{ label: '小时', value: 'hour' },
{ label: '天', value: 'day' },
{ label: '周', value: 'week' },
]}
onChange={(value) =>
handleInputChange(value, 'data_export_default_time')
}
></Form.Select>
{isAdminUser && (
<>
<Form.Input
field='username'
label='用户名称'
style={{ width: 176 }}
value={username}
placeholder={'可选值'}
name='username'
onChange={(value) => handleInputChange(value, 'username')}
/>
</>
)}
<Form.Section>
<Button
label='查询'
type='primary'
htmlType='submit'
className='btn-margin-right'
onClick={refresh}
loading={loading}
>
查询
</Button>
</Form.Section>
</>
</Form>
<Spin spinning={loading}>
<div style={{ height: 500 }}>
<div
id='model_pie'
style={{ width: '100%', minWidth: 100 }}
></div>
</div>
<div style={{ height: 500 }}>
<div
id='model_data'
style={{ width: '100%', minWidth: 100 }}
></div>
</div>
</Spin>
</Layout.Content>
</Layout>
</>
);
};
export default Detail;

View File

@@ -53,78 +53,115 @@ const Home = () => {
}, []);
return (
<>
{
homePageContentLoaded && homePageContent === '' ?
<>
<Card
bordered={false}
headerLine={false}
title='系统状况'
bodyStyle={{ padding: '10px 20px' }}
>
<Row gutter={16}>
<Col span={12}>
<Card
title='系统信息'
headerExtraContent={<span
style={{ fontSize: '12px', color: 'var(--semi-color-text-1)' }}>系统信息总览</span>}>
<p>名称{statusState?.status?.system_name}</p>
<p>版本{statusState?.status?.version ? statusState?.status?.version : 'unknown'}</p>
<p>
源码
<a
href='https://github.com/songquanpeng/one-api'
target='_blank' rel='noreferrer'
>
https://github.com/songquanpeng/one-api
</a>
</p>
<p>启动时间{getStartTimeString()}</p>
</Card>
</Col>
<Col span={12}>
<Card
title='系统配置'
headerExtraContent={<span
style={{ fontSize: '12px', color: 'var(--semi-color-text-1)' }}>系统配置总览</span>}>
<p>
邮箱验证
{statusState?.status?.email_verification === true ? '已启用' : '未启用'}
</p>
<p>
GitHub 身份验证
{statusState?.status?.github_oauth === true ? '已启用' : '未启用'}
</p>
<p>
微信身份验证
{statusState?.status?.wechat_login === true ? '已启用' : '未启用'}
</p>
<p>
Turnstile 用户校验
{statusState?.status?.turnstile_check === true ? '已启用' : '未启用'}
</p>
<p>
Telegram 身份验证
{statusState?.status?.telegram_oauth === true
? '已启用' : '未启用'}
</p>
</Card>
</Col>
</Row>
</Card>
</>
: <>
{
homePageContent.startsWith('https://') ?
<iframe src={homePageContent} style={{ width: '100%', height: '100vh', border: 'none' }} /> :
<div style={{ fontSize: 'larger' }} dangerouslySetInnerHTML={{ __html: homePageContent }}></div>
}
</>
}
{homePageContentLoaded && homePageContent === '' ? (
<>
<Card
bordered={false}
headerLine={false}
title='系统状况'
bodyStyle={{ padding: '10px 20px' }}
>
<Row gutter={16}>
<Col span={12}>
<Card
title='系统信息'
headerExtraContent={
<span
style={{
fontSize: '12px',
color: 'var(--semi-color-text-1)',
}}
>
系统信息总览
</span>
}
>
<p>名称{statusState?.status?.system_name}</p>
<p>
版本
{statusState?.status?.version
? statusState?.status?.version
: 'unknown'}
</p>
<p>
源码
<a
href='https://github.com/songquanpeng/one-api'
target='_blank'
rel='noreferrer'
>
https://github.com/songquanpeng/one-api
</a>
</p>
<p>启动时间{getStartTimeString()}</p>
</Card>
</Col>
<Col span={12}>
<Card
title='系统配置'
headerExtraContent={
<span
style={{
fontSize: '12px',
color: 'var(--semi-color-text-1)',
}}
>
系统配置总览
</span>
}
>
<p>
邮箱验证
{statusState?.status?.email_verification === true
? '已启用'
: '未启用'}
</p>
<p>
GitHub 身份验证
{statusState?.status?.github_oauth === true
? '已启用'
: '未启用'}
</p>
<p>
微信身份验证
{statusState?.status?.wechat_login === true
? '已启用'
: '未启用'}
</p>
<p>
Turnstile 用户校验
{statusState?.status?.turnstile_check === true
? '已启用'
: '未启用'}
</p>
<p>
Telegram 身份验证
{statusState?.status?.telegram_oauth === true
? '已启用'
: '未启用'}
</p>
</Card>
</Col>
</Row>
</Card>
</>
) : (
<>
{homePageContent.startsWith('https://') ? (
<iframe
src={homePageContent}
style={{ width: '100%', height: '100vh', border: 'none' }}
/>
) : (
<div
style={{ fontSize: 'larger' }}
dangerouslySetInnerHTML={{ __html: homePageContent }}
></div>
)}
</>
)}
</>
);
};
export default Home;
export default Home;

View File

@@ -1,8 +1,23 @@
import React, { useEffect, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { API, downloadTextAsFile, isMobile, showError, showSuccess } from '../../helpers';
import {
API,
downloadTextAsFile,
isMobile,
showError,
showSuccess,
} from '../../helpers';
import { renderQuotaWithPrompt } from '../../helpers/render';
import { AutoComplete, Button, Input, Modal, SideSheet, Space, Spin, Typography } from '@douyinfe/semi-ui';
import {
AutoComplete,
Button,
Input,
Modal,
SideSheet,
Space,
Spin,
Typography,
} from '@douyinfe/semi-ui';
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
import { Divider } from 'semantic-ui-react';
@@ -15,7 +30,7 @@ const EditRedemption = (props) => {
const originInputs = {
name: '',
quota: 100000,
count: 1
count: 1,
};
const [inputs, setInputs] = useState(originInputs);
const { name, quota, count } = inputs;
@@ -42,11 +57,9 @@ const EditRedemption = (props) => {
useEffect(() => {
if (isEdit) {
loadRedemption().then(
() => {
// console.log(inputs);
}
);
loadRedemption().then(() => {
// console.log(inputs);
});
} else {
setInputs(originInputs);
}
@@ -60,10 +73,13 @@ const EditRedemption = (props) => {
localInputs.quota = parseInt(localInputs.quota);
let res;
if (isEdit) {
res = await API.put(`/api/redemption/`, { ...localInputs, id: parseInt(props.editingRedemption.id) });
res = await API.put(`/api/redemption/`, {
...localInputs,
id: parseInt(props.editingRedemption.id),
});
} else {
res = await API.post(`/api/redemption/`, {
...localInputs
...localInputs,
});
}
const { success, message, data } = res.data;
@@ -97,7 +113,7 @@ const EditRedemption = (props) => {
),
onOk: () => {
downloadTextAsFile(text, `${inputs.name}.txt`);
}
},
});
}
setLoading(false);
@@ -107,15 +123,28 @@ const EditRedemption = (props) => {
<>
<SideSheet
placement={isEdit ? 'right' : 'left'}
title={<Title level={3}>{isEdit ? '更新兑换码信息' : '创建新的兑换码'}</Title>}
title={
<Title level={3}>
{isEdit ? '更新兑换码信息' : '创建新的兑换码'}
</Title>
}
headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
visible={props.visiable}
footer={
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Space>
<Button theme="solid" size={'large'} onClick={submit}>提交</Button>
<Button theme="solid" size={'large'} type={'tertiary'} onClick={handleCancel}>取消</Button>
<Button theme='solid' size={'large'} onClick={submit}>
提交
</Button>
<Button
theme='solid'
size={'large'}
type={'tertiary'}
onClick={handleCancel}
>
取消
</Button>
</Space>
</div>
}
@@ -126,12 +155,12 @@ const EditRedemption = (props) => {
<Spin spinning={loading}>
<Input
style={{ marginTop: 20 }}
label="名称"
name="name"
label='名称'
name='name'
placeholder={'请输入名称'}
onChange={value => handleInputChange('name', value)}
onChange={(value) => handleInputChange('name', value)}
value={name}
autoComplete="new-password"
autoComplete='new-password'
required={!isEdit}
/>
<Divider />
@@ -140,12 +169,12 @@ const EditRedemption = (props) => {
</div>
<AutoComplete
style={{ marginTop: 8 }}
name="quota"
name='quota'
placeholder={'请输入额度'}
onChange={(value) => handleInputChange('quota', value)}
value={quota}
autoComplete="new-password"
type="number"
autoComplete='new-password'
type='number'
position={'bottom'}
data={[
{ value: 500000, label: '1$' },
@@ -153,25 +182,25 @@ const EditRedemption = (props) => {
{ value: 25000000, label: '50$' },
{ value: 50000000, label: '100$' },
{ value: 250000000, label: '500$' },
{ value: 500000000, label: '1000$' }
{ value: 500000000, label: '1000$' },
]}
/>
{
!isEdit && <>
{!isEdit && (
<>
<Divider />
<Typography.Text>生成数量</Typography.Text>
<Input
style={{ marginTop: 8 }}
label="生成数量"
name="count"
label='生成数量'
name='count'
placeholder={'请输入生成数量'}
onChange={value => handleInputChange('count', value)}
onChange={(value) => handleInputChange('count', value)}
value={count}
autoComplete="new-password"
type="number"
autoComplete='new-password'
type='number'
/>
</>
}
)}
</Spin>
</SideSheet>
</>

View File

@@ -1,17 +1,17 @@
import React from 'react';
import RedemptionsTable from '../../components/RedemptionsTable';
import {Layout} from "@douyinfe/semi-ui";
import { Layout } from '@douyinfe/semi-ui';
const Redemption = () => (
<>
<Layout>
<Layout.Header>
<h3>管理兑换码</h3>
</Layout.Header>
<Layout.Content>
<RedemptionsTable/>
</Layout.Content>
</Layout>
<Layout>
<Layout.Header>
<h3>管理兑换码</h3>
</Layout.Header>
<Layout.Content>
<RedemptionsTable />
</Layout.Content>
</Layout>
</>
);

View File

@@ -1,53 +1,53 @@
import React from 'react';
import SystemSetting from '../../components/SystemSetting';
import {isRoot} from '../../helpers';
import { isRoot } from '../../helpers';
import OtherSetting from '../../components/OtherSetting';
import PersonalSetting from '../../components/PersonalSetting';
import OperationSetting from '../../components/OperationSetting';
import {Layout, TabPane, Tabs} from "@douyinfe/semi-ui";
import { Layout, TabPane, Tabs } from '@douyinfe/semi-ui';
const Setting = () => {
let panes = [
{
tab: '个人设置',
content: <PersonalSetting/>,
itemKey: '1'
}
];
let panes = [
{
tab: '个人设置',
content: <PersonalSetting />,
itemKey: '1',
},
];
if (isRoot()) {
panes.push({
tab: '运营设置',
content: <OperationSetting/>,
itemKey: '2'
});
panes.push({
tab: '系统设置',
content: <SystemSetting/>,
itemKey: '3'
});
panes.push({
tab: '其他设置',
content: <OtherSetting/>,
itemKey: '4'
});
}
if (isRoot()) {
panes.push({
tab: '运营设置',
content: <OperationSetting />,
itemKey: '2',
});
panes.push({
tab: '系统设置',
content: <SystemSetting />,
itemKey: '3',
});
panes.push({
tab: '其他设置',
content: <OtherSetting />,
itemKey: '4',
});
}
return (
<div>
<Layout>
<Layout.Content>
<Tabs type="line" defaultActiveKey="1">
{panes.map(pane => (
<TabPane itemKey={pane.itemKey} tab={pane.tab}>
{pane.content}
</TabPane>
))}
</Tabs>
</Layout.Content>
</Layout>
</div>
);
return (
<div>
<Layout>
<Layout.Content>
<Tabs type='line' defaultActiveKey='1'>
{panes.map((pane) => (
<TabPane itemKey={pane.itemKey} tab={pane.tab}>
{pane.content}
</TabPane>
))}
</Tabs>
</Layout.Content>
</Layout>
</div>
);
};
export default Setting;

View File

@@ -1,19 +1,25 @@
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { API, isMobile, showError, showSuccess, timestamp2string } from '../../helpers';
import {
API,
isMobile,
showError,
showSuccess,
timestamp2string,
} from '../../helpers';
import { renderQuotaWithPrompt } from '../../helpers/render';
import {
AutoComplete,
Banner,
Button,
Checkbox,
DatePicker,
Input,
Select,
SideSheet,
Space,
Spin,
Typography
AutoComplete,
Banner,
Button,
Checkbox,
DatePicker,
Input,
Select,
SideSheet,
Space,
Spin,
Typography,
} from '@douyinfe/semi-ui';
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
import { Divider } from 'semantic-ui-react';
@@ -27,10 +33,17 @@ const EditToken = (props) => {
expired_time: -1,
unlimited_quota: false,
model_limits_enabled: false,
model_limits: []
model_limits: [],
};
const [inputs, setInputs] = useState(originInputs);
const { name, remain_quota, expired_time, unlimited_quota, model_limits_enabled, model_limits } = inputs;
const {
name,
remain_quota,
expired_time,
unlimited_quota,
model_limits_enabled,
model_limits,
} = inputs;
// const [visible, setVisible] = useState(false);
const [models, setModels] = useState({});
const navigate = useNavigate();
@@ -65,7 +78,7 @@ const EditToken = (props) => {
if (success) {
let localModelOptions = data.map((model) => ({
label: model,
value: model
value: model,
}));
setModels(localModelOptions);
} else {
@@ -100,11 +113,9 @@ const EditToken = (props) => {
if (!isEdit) {
setInputs(originInputs);
} else {
loadToken().then(
() => {
// console.log(inputs);
}
);
loadToken().then(() => {
// console.log(inputs);
});
}
loadModels();
}, [isEdit]);
@@ -123,10 +134,13 @@ const EditToken = (props) => {
// 生成一个随机的四位字母数字字符串
const generateRandomSuffix = () => {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
const characters =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < 6; i++) {
result += characters.charAt(Math.floor(Math.random() * characters.length));
result += characters.charAt(
Math.floor(Math.random() * characters.length),
);
}
return result;
};
@@ -147,7 +161,10 @@ const EditToken = (props) => {
localInputs.expired_time = Math.ceil(time / 1000);
}
localInputs.model_limits = localInputs.model_limits.join(',');
let res = await API.put(`/api/token/`, { ...localInputs, id: parseInt(props.editingToken.id) });
let res = await API.put(`/api/token/`, {
...localInputs,
id: parseInt(props.editingToken.id),
});
const { success, message } = res.data;
if (success) {
showSuccess('令牌更新成功!');
@@ -189,7 +206,9 @@ const EditToken = (props) => {
}
if (successCount > 0) {
showSuccess(`${successCount}个令牌创建成功,请在列表页面点击复制获取令牌!`);
showSuccess(
`${successCount}个令牌创建成功,请在列表页面点击复制获取令牌!`,
);
props.refresh();
props.handleClose();
}
@@ -199,20 +218,30 @@ const EditToken = (props) => {
setTokenCount(1); // 重置数量为默认值
};
return (
<>
<SideSheet
placement={isEdit ? 'right' : 'left'}
title={<Title level={3}>{isEdit ? '更新令牌信息' : '创建新的令牌'}</Title>}
title={
<Title level={3}>{isEdit ? '更新令牌信息' : '创建新的令牌'}</Title>
}
headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
visible={props.visiable}
footer={
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Space>
<Button theme="solid" size={'large'} onClick={submit}>提交</Button>
<Button theme="solid" size={'large'} type={'tertiary'} onClick={handleCancel}>取消</Button>
<Button theme='solid' size={'large'} onClick={submit}>
提交
</Button>
<Button
theme='solid'
size={'large'}
type={'tertiary'}
onClick={handleCancel}
>
取消
</Button>
</Space>
</div>
}
@@ -223,55 +252,79 @@ const EditToken = (props) => {
<Spin spinning={loading}>
<Input
style={{ marginTop: 20 }}
label="名称"
name="name"
label='名称'
name='name'
placeholder={'请输入名称'}
onChange={(value) => handleInputChange('name', value)}
value={name}
autoComplete="new-password"
autoComplete='new-password'
required={!isEdit}
/>
<Divider />
<DatePicker
label="过期时间"
name="expired_time"
label='过期时间'
name='expired_time'
placeholder={'请选择过期时间'}
onChange={(value) => handleInputChange('expired_time', value)}
value={expired_time}
autoComplete="new-password"
type="dateTime"
autoComplete='new-password'
type='dateTime'
/>
<div style={{ marginTop: 20 }}>
<Space>
<Button type={'tertiary'} onClick={() => {
setExpiredTime(0, 0, 0, 0);
}}>永不过期</Button>
<Button type={'tertiary'} onClick={() => {
setExpiredTime(0, 0, 1, 0);
}}>一小时</Button>
<Button type={'tertiary'} onClick={() => {
setExpiredTime(1, 0, 0, 0);
}}>一个月</Button>
<Button type={'tertiary'} onClick={() => {
setExpiredTime(0, 1, 0, 0);
}}>一天</Button>
<Button
type={'tertiary'}
onClick={() => {
setExpiredTime(0, 0, 0, 0);
}}
>
永不过期
</Button>
<Button
type={'tertiary'}
onClick={() => {
setExpiredTime(0, 0, 1, 0);
}}
>
一小时
</Button>
<Button
type={'tertiary'}
onClick={() => {
setExpiredTime(1, 0, 0, 0);
}}
>
一个月
</Button>
<Button
type={'tertiary'}
onClick={() => {
setExpiredTime(0, 1, 0, 0);
}}
>
一天
</Button>
</Space>
</div>
<Divider />
<Banner type={'warning'}
description={'注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。'}></Banner>
<Banner
type={'warning'}
description={
'注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。'
}
></Banner>
<div style={{ marginTop: 20 }}>
<Typography.Text>{`额度${renderQuotaWithPrompt(remain_quota)}`}</Typography.Text>
</div>
<AutoComplete
style={{ marginTop: 8 }}
name="remain_quota"
name='remain_quota'
placeholder={'请输入额度'}
onChange={(value) => handleInputChange('remain_quota', value)}
value={remain_quota}
autoComplete="new-password"
type="number"
autoComplete='new-password'
type='number'
// position={'top'}
data={[
{ value: 500000, label: '1$' },
@@ -279,7 +332,7 @@ const EditToken = (props) => {
{ value: 25000000, label: '50$' },
{ value: 50000000, label: '100$' },
{ value: 250000000, label: '500$' },
{ value: 500000000, label: '1000$' }
{ value: 500000000, label: '1000$' },
]}
disabled={unlimited_quota}
/>
@@ -291,18 +344,18 @@ const EditToken = (props) => {
</div>
<AutoComplete
style={{ marginTop: 8 }}
label="数量"
label='数量'
placeholder={'请选择或输入创建令牌的数量'}
onChange={(value) => handleTokenCountChange(value)}
onSelect={(value) => handleTokenCountChange(value)}
value={tokenCount.toString()}
autoComplete="off"
type="number"
autoComplete='off'
type='number'
data={[
{ value: 10, label: '10个' },
{ value: 20, label: '20个' },
{ value: 30, label: '30个' },
{ value: 100, label: '100个' }
{ value: 100, label: '100个' },
]}
disabled={unlimited_quota}
/>
@@ -310,35 +363,44 @@ const EditToken = (props) => {
)}
<div>
<Button style={{ marginTop: 8 }} type={'warning'} onClick={() => {
setUnlimitedQuota();
}}>{unlimited_quota ? '取消无限额度' : '设为无限额度'}</Button>
<Button
style={{ marginTop: 8 }}
type={'warning'}
onClick={() => {
setUnlimitedQuota();
}}
>
{unlimited_quota ? '取消无限额度' : '设为无限额度'}
</Button>
</div>
<Divider />
<div style={{ marginTop: 10, display: 'flex' }}>
<Space>
<Checkbox
name="model_limits_enabled"
name='model_limits_enabled'
checked={model_limits_enabled}
onChange={(e) => handleInputChange('model_limits_enabled', e.target.checked)}
>
</Checkbox>
<Typography.Text>启用模型限制非必要不建议启用</Typography.Text>
onChange={(e) =>
handleInputChange('model_limits_enabled', e.target.checked)
}
></Checkbox>
<Typography.Text>
启用模型限制非必要不建议启用
</Typography.Text>
</Space>
</div>
<Select
style={{ marginTop: 8 }}
placeholder={'请选择该渠道所支持的模型'}
name="models"
name='models'
required
multiple
selection
onChange={value => {
onChange={(value) => {
handleInputChange('model_limits', value);
}}
value={inputs.model_limits}
autoComplete="new-password"
autoComplete='new-password'
optionList={models}
disabled={!model_limits_enabled}
/>

View File

@@ -1,14 +1,14 @@
import React from 'react';
import TokensTable from '../../components/TokensTable';
import {Layout} from "@douyinfe/semi-ui";
import { Layout } from '@douyinfe/semi-ui';
const Token = () => (
<>
<Layout>
<Layout.Header>
<h3>我的令牌</h3>
<h3>我的令牌</h3>
</Layout.Header>
<Layout.Content>
<TokensTable/>
<TokensTable />
</Layout.Content>
</Layout>
</>

View File

@@ -1,314 +1,338 @@
import React, {useEffect, useState} from 'react';
import {API, isMobile, showError, showInfo, showSuccess} from '../../helpers';
import {renderNumber, renderQuota} from '../../helpers/render';
import {Col, Layout, Row, Typography, Card, Button, Form, Divider, Space, Modal} from "@douyinfe/semi-ui";
import Title from "@douyinfe/semi-ui/lib/es/typography/title";
import React, { useEffect, useState } from 'react';
import { API, isMobile, showError, showInfo, showSuccess } from '../../helpers';
import { renderNumber, renderQuota } from '../../helpers/render';
import {
Col,
Layout,
Row,
Typography,
Card,
Button,
Form,
Divider,
Space,
Modal,
} from '@douyinfe/semi-ui';
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
import { Link } from 'react-router-dom';
const TopUp = () => {
const [redemptionCode, setRedemptionCode] = useState('');
const [topUpCode, setTopUpCode] = useState('');
const [topUpCount, setTopUpCount] = useState(10);
const [minTopupCount, setMinTopUpCount] = useState(1);
const [amount, setAmount] = useState(0.0);
const [minTopUp, setMinTopUp] = useState(1);
const [topUpLink, setTopUpLink] = useState('');
const [enableOnlineTopUp, setEnableOnlineTopUp] = useState(false);
const [userQuota, setUserQuota] = useState(0);
const [isSubmitting, setIsSubmitting] = useState(false);
const [open, setOpen] = useState(false);
const [payWay, setPayWay] = useState('');
const [redemptionCode, setRedemptionCode] = useState('');
const [topUpCode, setTopUpCode] = useState('');
const [topUpCount, setTopUpCount] = useState(10);
const [minTopupCount, setMinTopUpCount] = useState(1);
const [amount, setAmount] = useState(0.0);
const [minTopUp, setMinTopUp] = useState(1);
const [topUpLink, setTopUpLink] = useState('');
const [enableOnlineTopUp, setEnableOnlineTopUp] = useState(false);
const [userQuota, setUserQuota] = useState(0);
const [isSubmitting, setIsSubmitting] = useState(false);
const [open, setOpen] = useState(false);
const [payWay, setPayWay] = useState('');
const topUp = async () => {
if (redemptionCode === '') {
showInfo('请输入兑换码!')
return;
}
setIsSubmitting(true);
try {
const res = await API.post('/api/user/topup', {
key: redemptionCode
});
const {success, message, data} = res.data;
if (success) {
showSuccess('兑换成功!');
Modal.success({title: '兑换成功!', content: '成功兑换额度:' + renderQuota(data), centered: true});
setUserQuota((quota) => {
return quota + data;
});
setRedemptionCode('');
} else {
showError(message);
}
} catch (err) {
showError('请求失败');
} finally {
setIsSubmitting(false);
}
};
const openTopUpLink = () => {
if (!topUpLink) {
showError('超级管理员未设置充值链接!');
return;
}
window.open(topUpLink, '_blank');
};
const preTopUp = async (payment) => {
if (!enableOnlineTopUp) {
showError('管理员未开启在线充值!');
return;
}
if (amount === 0) {
await getAmount();
}
if (topUpCount < minTopUp) {
showInfo('充值数量不能小于' + minTopUp);
return;
}
setPayWay(payment)
setOpen(true);
const topUp = async () => {
if (redemptionCode === '') {
showInfo('请输入兑换码!');
return;
}
const onlineTopUp = async () => {
if (amount === 0) {
await getAmount();
}
if (topUpCount < minTopUp) {
showInfo('充值数量不能小于' + minTopUp);
return;
}
setOpen(false);
try {
const res = await API.post('/api/user/pay', {
amount: parseInt(topUpCount),
top_up_code: topUpCode,
payment_method: payWay
});
if (res !== undefined) {
const {message, data} = res.data;
// showInfo(message);
if (message === 'success') {
let params = data
let url = res.data.url
let form = document.createElement('form')
form.action = url
form.method = 'POST'
// 判断是否为safari浏览器
let isSafari = navigator.userAgent.indexOf("Safari") > -1 && navigator.userAgent.indexOf("Chrome") < 1;
if (!isSafari) {
form.target = '_blank'
}
for (let key in params) {
let input = document.createElement('input')
input.type = 'hidden'
input.name = key
input.value = params[key]
form.appendChild(input)
}
document.body.appendChild(form)
form.submit()
document.body.removeChild(form)
} else {
showError(data);
// setTopUpCount(parseInt(res.data.count));
// setAmount(parseInt(data));
}
} else {
showError(res);
}
} catch (err) {
console.log(err);
} finally {
}
setIsSubmitting(true);
try {
const res = await API.post('/api/user/topup', {
key: redemptionCode,
});
const { success, message, data } = res.data;
if (success) {
showSuccess('兑换成功!');
Modal.success({
title: '兑换成功!',
content: '成功兑换额度:' + renderQuota(data),
centered: true,
});
setUserQuota((quota) => {
return quota + data;
});
setRedemptionCode('');
} else {
showError(message);
}
} catch (err) {
showError('请求失败');
} finally {
setIsSubmitting(false);
}
};
const getUserQuota = async () => {
let res = await API.get(`/api/user/self`);
const {success, message, data} = res.data;
if (success) {
setUserQuota(data.quota);
const openTopUpLink = () => {
if (!topUpLink) {
showError('超级管理员未设置充值链接!');
return;
}
window.open(topUpLink, '_blank');
};
const preTopUp = async (payment) => {
if (!enableOnlineTopUp) {
showError('管理员未开启在线充值!');
return;
}
if (amount === 0) {
await getAmount();
}
if (topUpCount < minTopUp) {
showInfo('充值数量不能小于' + minTopUp);
return;
}
setPayWay(payment);
setOpen(true);
};
const onlineTopUp = async () => {
if (amount === 0) {
await getAmount();
}
if (topUpCount < minTopUp) {
showInfo('充值数量不能小于' + minTopUp);
return;
}
setOpen(false);
try {
const res = await API.post('/api/user/pay', {
amount: parseInt(topUpCount),
top_up_code: topUpCode,
payment_method: payWay,
});
if (res !== undefined) {
const { message, data } = res.data;
// showInfo(message);
if (message === 'success') {
let params = data;
let url = res.data.url;
let form = document.createElement('form');
form.action = url;
form.method = 'POST';
// 判断是否为safari浏览器
let isSafari =
navigator.userAgent.indexOf('Safari') > -1 &&
navigator.userAgent.indexOf('Chrome') < 1;
if (!isSafari) {
form.target = '_blank';
}
for (let key in params) {
let input = document.createElement('input');
input.type = 'hidden';
input.name = key;
input.value = params[key];
form.appendChild(input);
}
document.body.appendChild(form);
form.submit();
document.body.removeChild(form);
} else {
showError(message);
showError(data);
// setTopUpCount(parseInt(res.data.count));
// setAmount(parseInt(data));
}
} else {
showError(res);
}
} catch (err) {
console.log(err);
} finally {
}
};
useEffect(() => {
let status = localStorage.getItem('status');
if (status) {
status = JSON.parse(status);
if (status.top_up_link) {
setTopUpLink(status.top_up_link);
}
if (status.min_topup) {
setMinTopUp(status.min_topup);
}
if (status.enable_online_topup) {
setEnableOnlineTopUp(status.enable_online_topup);
}
const getUserQuota = async () => {
let res = await API.get(`/api/user/self`);
const { success, message, data } = res.data;
if (success) {
setUserQuota(data.quota);
} else {
showError(message);
}
};
useEffect(() => {
let status = localStorage.getItem('status');
if (status) {
status = JSON.parse(status);
if (status.top_up_link) {
setTopUpLink(status.top_up_link);
}
if (status.min_topup) {
setMinTopUp(status.min_topup);
}
if (status.enable_online_topup) {
setEnableOnlineTopUp(status.enable_online_topup);
}
}
getUserQuota().then();
}, []);
const renderAmount = () => {
// console.log(amount);
return amount + '元';
};
const getAmount = async (value) => {
if (value === undefined) {
value = topUpCount;
}
try {
const res = await API.post('/api/user/amount', {
amount: parseFloat(value),
top_up_code: topUpCode,
});
if (res !== undefined) {
const { message, data } = res.data;
// showInfo(message);
if (message === 'success') {
setAmount(parseFloat(data));
} else {
showError(data);
// setTopUpCount(parseInt(res.data.count));
// setAmount(parseInt(data));
}
getUserQuota().then();
}, []);
const renderAmount = () => {
// console.log(amount);
return amount + '元';
} else {
showError(res);
}
} catch (err) {
console.log(err);
} finally {
}
};
const getAmount = async (value) => {
if (value === undefined) {
value = topUpCount;
}
try {
const res = await API.post('/api/user/amount', {
amount: parseFloat(value),
top_up_code: topUpCode
});
if (res !== undefined) {
const {message, data} = res.data;
// showInfo(message);
if (message === 'success') {
setAmount(parseFloat(data));
} else {
showError(data);
// setTopUpCount(parseInt(res.data.count));
// setAmount(parseInt(data));
}
} else {
showError(res);
}
} catch (err) {
console.log(err);
} finally {
}
}
const handleCancel = () => {
setOpen(false);
};
const handleCancel = () => {
setOpen(false);
}
return (
<div>
<Layout>
<Layout.Header>
<h3>我的钱包</h3>
</Layout.Header>
<Layout.Content>
<Modal
title="确定要充值吗"
visible={open}
onOk={onlineTopUp}
onCancel={handleCancel}
maskClosable={false}
size={'small'}
centered={true}
return (
<div>
<Layout>
<Layout.Header>
<h3>我的钱包</h3>
</Layout.Header>
<Layout.Content>
<Modal
title='确定要充值吗'
visible={open}
onOk={onlineTopUp}
onCancel={handleCancel}
maskClosable={false}
size={'small'}
centered={true}
>
<p>充值数量{topUpCount}$</p>
<p>实付金额{renderAmount()}</p>
<p>是否确认充值</p>
</Modal>
<div
style={{ marginTop: 20, display: 'flex', justifyContent: 'center' }}
>
<Card style={{ width: '500px', padding: '20px' }}>
<Title level={3} style={{ textAlign: 'center' }}>
余额 {renderQuota(userQuota)}
</Title>
<div style={{ marginTop: 20 }}>
<Divider>兑换余额</Divider>
<Form>
<Form.Input
field={'redemptionCode'}
label={'兑换码'}
placeholder='兑换码'
name='redemptionCode'
value={redemptionCode}
onChange={(value) => {
setRedemptionCode(value);
}}
/>
<Space>
{topUpLink ? (
<Button
type={'primary'}
theme={'solid'}
onClick={openTopUpLink}
>
获取兑换码
</Button>
) : null}
<Button
type={'warning'}
theme={'solid'}
onClick={topUp}
disabled={isSubmitting}
>
<p>充值数量{topUpCount}$</p>
<p>实付金额{renderAmount()}</p>
<p>是否确认充值</p>
</Modal>
<div style={{marginTop: 20, display: 'flex', justifyContent: 'center'}}>
<Card
style={{width: '500px', padding: '20px'}}
>
<Title level={3} style={{textAlign: 'center'}}>余额 {renderQuota(userQuota)}</Title>
<div style={{marginTop: 20}}>
<Divider>
兑换余额
</Divider>
<Form>
<Form.Input
field={'redemptionCode'}
label={'兑换码'}
placeholder='兑换码'
name='redemptionCode'
value={redemptionCode}
onChange={(value) => {
setRedemptionCode(value);
}}
/>
<Space>
{
topUpLink ?
<Button type={'primary'} theme={'solid'} onClick={openTopUpLink}>
获取兑换码
</Button> : null
}
<Button type={"warning"} theme={'solid'} onClick={topUp}
disabled={isSubmitting}>
{isSubmitting ? '兑换中...' : '兑换'}
</Button>
</Space>
</Form>
</div>
<div style={{marginTop: 20}}>
<Divider>
在线充值
</Divider>
<Form>
<Form.Input
disabled={!enableOnlineTopUp}
field={'redemptionCount'}
label={'实付金额:' + renderAmount()}
placeholder={'充值数量,最低' + minTopUp + '$'}
name='redemptionCount'
type={'number'}
value={topUpCount}
suffix={'$'}
min={minTopUp}
defaultValue={minTopUp}
max={100000}
onChange={async (value) => {
if (value < 1) {
value = 1;
}
if (value > 100000) {
value = 100000;
}
setTopUpCount(value);
await getAmount(value);
}}
/>
<Space>
<Button type={'primary'} theme={'solid'} onClick={
async () => {
preTopUp('zfb')
}
}>
支付宝
</Button>
<Button style={{backgroundColor: 'rgba(var(--semi-green-5), 1)'}}
type={'primary'}
theme={'solid'} onClick={
async () => {
preTopUp('wx')
}
}>
微信
</Button>
</Space>
</Form>
</div>
{/*<div style={{ display: 'flex', justifyContent: 'right' }}>*/}
{/* <Text>*/}
{/* <Link onClick={*/}
{/* async () => {*/}
{/* window.location.href = '/topup/history'*/}
{/* }*/}
{/* }>充值记录</Link>*/}
{/* </Text>*/}
{/*</div>*/}
</Card>
</div>
</Layout.Content>
</Layout>
</div>
);
{isSubmitting ? '兑换中...' : '兑换'}
</Button>
</Space>
</Form>
</div>
<div style={{ marginTop: 20 }}>
<Divider>在线充值</Divider>
<Form>
<Form.Input
disabled={!enableOnlineTopUp}
field={'redemptionCount'}
label={'实付金额:' + renderAmount()}
placeholder={'充值数量,最低' + minTopUp + '$'}
name='redemptionCount'
type={'number'}
value={topUpCount}
suffix={'$'}
min={minTopUp}
defaultValue={minTopUp}
max={100000}
onChange={async (value) => {
if (value < 1) {
value = 1;
}
if (value > 100000) {
value = 100000;
}
setTopUpCount(value);
await getAmount(value);
}}
/>
<Space>
<Button
type={'primary'}
theme={'solid'}
onClick={async () => {
preTopUp('zfb');
}}
>
支付宝
</Button>
<Button
style={{
backgroundColor: 'rgba(var(--semi-green-5), 1)',
}}
type={'primary'}
theme={'solid'}
onClick={async () => {
preTopUp('wx');
}}
>
微信
</Button>
</Space>
</Form>
</div>
{/*<div style={{ display: 'flex', justifyContent: 'right' }}>*/}
{/* <Text>*/}
{/* <Link onClick={*/}
{/* async () => {*/}
{/* window.location.href = '/topup/history'*/}
{/* }*/}
{/* }>充值记录</Link>*/}
{/* </Text>*/}
{/*</div>*/}
</Card>
</div>
</Layout.Content>
</Layout>
</div>
);
};
export default TopUp;
export default TopUp;

View File

@@ -7,7 +7,7 @@ const AddUser = (props) => {
const originInputs = {
username: '',
display_name: '',
password: ''
password: '',
};
const [inputs, setInputs] = useState(originInputs);
const [loading, setLoading] = useState(false);
@@ -48,8 +48,17 @@ const AddUser = (props) => {
footer={
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Space>
<Button theme="solid" size={'large'} onClick={submit}>提交</Button>
<Button theme="solid" size={'large'} type={'tertiary'} onClick={handleCancel}>取消</Button>
<Button theme='solid' size={'large'} onClick={submit}>
提交
</Button>
<Button
theme='solid'
size={'large'}
type={'tertiary'}
onClick={handleCancel}
>
取消
</Button>
</Space>
</div>
}
@@ -60,34 +69,34 @@ const AddUser = (props) => {
<Spin spinning={loading}>
<Input
style={{ marginTop: 20 }}
label="用户名"
name="username"
label='用户名'
name='username'
addonBefore={'用户名'}
placeholder={'请输入用户名'}
onChange={value => handleInputChange('username', value)}
onChange={(value) => handleInputChange('username', value)}
value={username}
autoComplete="off"
autoComplete='off'
/>
<Input
style={{ marginTop: 20 }}
addonBefore={'显示名'}
label="显示名称"
name="display_name"
autoComplete="off"
label='显示名称'
name='display_name'
autoComplete='off'
placeholder={'请输入显示名称'}
onChange={value => handleInputChange('display_name', value)}
onChange={(value) => handleInputChange('display_name', value)}
value={display_name}
/>
<Input
style={{ marginTop: 20 }}
label="密 码"
name="password"
label='密 码'
name='password'
type={'password'}
addonBefore={'密码'}
placeholder={'请输入密码'}
onChange={value => handleInputChange('password', value)}
onChange={(value) => handleInputChange('password', value)}
value={password}
autoComplete="off"
autoComplete='off'
/>
</Spin>
</SideSheet>

View File

@@ -3,7 +3,16 @@ import { useNavigate } from 'react-router-dom';
import { API, isMobile, showError, showSuccess } from '../../helpers';
import { renderQuotaWithPrompt } from '../../helpers/render';
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
import { Button, Divider, Input, Select, SideSheet, Space, Spin, Typography } from '@douyinfe/semi-ui';
import {
Button,
Divider,
Input,
Select,
SideSheet,
Space,
Spin,
Typography,
} from '@douyinfe/semi-ui';
const EditUser = (props) => {
const userId = props.editingUser.id;
@@ -16,21 +25,32 @@ const EditUser = (props) => {
wechat_id: '',
email: '',
quota: 0,
group: 'default'
group: 'default',
});
const [groupOptions, setGroupOptions] = useState([]);
const { username, display_name, password, github_id, wechat_id, telegram_id, email, quota, group } =
inputs;
const {
username,
display_name,
password,
github_id,
wechat_id,
telegram_id,
email,
quota,
group,
} = inputs;
const handleInputChange = (name, value) => {
setInputs((inputs) => ({ ...inputs, [name]: value }));
};
const fetchGroups = async () => {
try {
let res = await API.get(`/api/group/`);
setGroupOptions(res.data.data.map((group) => ({
label: group,
value: group
})));
setGroupOptions(
res.data.data.map((group) => ({
label: group,
value: group,
})),
);
} catch (error) {
showError(error.message);
}
@@ -98,8 +118,17 @@ const EditUser = (props) => {
footer={
<div style={{ display: 'flex', justifyContent: 'flex-end' }}>
<Space>
<Button theme="solid" size={'large'} onClick={submit}>提交</Button>
<Button theme="solid" size={'large'} type={'tertiary'} onClick={handleCancel}>取消</Button>
<Button theme='solid' size={'large'} onClick={submit}>
提交
</Button>
<Button
theme='solid'
size={'large'}
type={'tertiary'}
onClick={handleCancel}
>
取消
</Button>
</Space>
</div>
}
@@ -112,103 +141,103 @@ const EditUser = (props) => {
<Typography.Text>用户名</Typography.Text>
</div>
<Input
label="用户名"
name="username"
label='用户名'
name='username'
placeholder={'请输入新的用户名'}
onChange={value => handleInputChange('username', value)}
onChange={(value) => handleInputChange('username', value)}
value={username}
autoComplete="new-password"
autoComplete='new-password'
/>
<div style={{ marginTop: 20 }}>
<Typography.Text>密码</Typography.Text>
</div>
<Input
label="密码"
name="password"
label='密码'
name='password'
type={'password'}
placeholder={'请输入新的密码,最短 8 位'}
onChange={value => handleInputChange('password', value)}
onChange={(value) => handleInputChange('password', value)}
value={password}
autoComplete="new-password"
autoComplete='new-password'
/>
<div style={{ marginTop: 20 }}>
<Typography.Text>显示名称</Typography.Text>
</div>
<Input
label="显示名称"
name="display_name"
label='显示名称'
name='display_name'
placeholder={'请输入新的显示名称'}
onChange={value => handleInputChange('display_name', value)}
onChange={(value) => handleInputChange('display_name', value)}
value={display_name}
autoComplete="new-password"
autoComplete='new-password'
/>
{
userId && <>
{userId && (
<>
<div style={{ marginTop: 20 }}>
<Typography.Text>分组</Typography.Text>
</div>
<Select
placeholder={'请选择分组'}
name="group"
name='group'
fluid
search
selection
allowAdditions
additionLabel={'请在系统设置页面编辑分组倍率以添加新的分组:'}
onChange={value => handleInputChange('group', value)}
onChange={(value) => handleInputChange('group', value)}
value={inputs.group}
autoComplete="new-password"
autoComplete='new-password'
optionList={groupOptions}
/>
<div style={{ marginTop: 20 }}>
<Typography.Text>{`剩余额度${renderQuotaWithPrompt(quota)}`}</Typography.Text>
</div>
<Input
name="quota"
name='quota'
placeholder={'请输入新的剩余额度'}
onChange={value => handleInputChange('quota', value)}
onChange={(value) => handleInputChange('quota', value)}
value={quota}
type={'number'}
autoComplete="new-password"
autoComplete='new-password'
/>
</>
}
)}
<Divider style={{ marginTop: 20 }}>以下信息不可修改</Divider>
<div style={{ marginTop: 20 }}>
<Typography.Text>已绑定的 GitHub 账户</Typography.Text>
</div>
<Input
name="github_id"
name='github_id'
value={github_id}
autoComplete="new-password"
placeholder="此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改"
autoComplete='new-password'
placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
readonly
/>
<div style={{ marginTop: 20 }}>
<Typography.Text>已绑定的微信账户</Typography.Text>
</div>
<Input
name="wechat_id"
name='wechat_id'
value={wechat_id}
autoComplete="new-password"
placeholder="此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改"
autoComplete='new-password'
placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
readonly
/>
<Input
name="telegram_id"
name='telegram_id'
value={telegram_id}
autoComplete="new-password"
placeholder="此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改"
autoComplete='new-password'
placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
readonly
/>
<div style={{ marginTop: 20 }}>
<Typography.Text>已绑定的邮箱账户</Typography.Text>
</div>
<Input
name="email"
name='email'
value={email}
autoComplete="new-password"
placeholder="此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改"
autoComplete='new-password'
placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
readonly
/>
</Spin>

View File

@@ -1,16 +1,16 @@
import React from 'react';
import UsersTable from '../../components/UsersTable';
import {Layout} from "@douyinfe/semi-ui";
import { Layout } from '@douyinfe/semi-ui';
const User = () => (
<>
<Layout>
<Layout.Header>
<h3>管理用户</h3>
</Layout.Header>
<Layout.Content>
<UsersTable/>
</Layout.Content>
<Layout.Header>
<h3>管理用户</h3>
</Layout.Header>
<Layout.Content>
<UsersTable />
</Layout.Content>
</Layout>
</>
);