mirror of
https://github.com/linux-do/new-api.git
synced 2025-11-10 08:03:41 +08:00
chore: lint fix
This commit is contained in:
@@ -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
@@ -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;
|
||||
|
||||
@@ -11,5 +11,4 @@ const Chat = () => {
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
export default Chat;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user