mirror of
https://github.com/linux-do/new-api.git
synced 2025-11-11 16:43:41 +08:00
add epay
This commit is contained in:
@@ -4,7 +4,7 @@ import { Link } from 'react-router-dom';
|
||||
import { API, showError, showInfo, showSuccess, timestamp2string } from '../helpers';
|
||||
|
||||
import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../constants';
|
||||
import { renderGroup, renderNumber } from '../helpers/render';
|
||||
import {renderGroup, renderNumber, renderQuota} from '../helpers/render';
|
||||
|
||||
function renderTimestamp(timestamp) {
|
||||
return (
|
||||
@@ -299,6 +299,7 @@ const ChannelsTable = () => {
|
||||
onClick={() => {
|
||||
sortChannel('group');
|
||||
}}
|
||||
width={1}
|
||||
>
|
||||
分组
|
||||
</Table.HeaderCell>
|
||||
@@ -307,6 +308,7 @@ const ChannelsTable = () => {
|
||||
onClick={() => {
|
||||
sortChannel('type');
|
||||
}}
|
||||
width={2}
|
||||
>
|
||||
类型
|
||||
</Table.HeaderCell>
|
||||
@@ -315,6 +317,7 @@ const ChannelsTable = () => {
|
||||
onClick={() => {
|
||||
sortChannel('status');
|
||||
}}
|
||||
width={2}
|
||||
>
|
||||
状态
|
||||
</Table.HeaderCell>
|
||||
@@ -326,6 +329,15 @@ const ChannelsTable = () => {
|
||||
>
|
||||
响应时间
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortChannel('used_quota');
|
||||
}}
|
||||
width={1}
|
||||
>
|
||||
已使用
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
@@ -361,6 +373,7 @@ const ChannelsTable = () => {
|
||||
basic
|
||||
/>
|
||||
</Table.Cell>
|
||||
<Table.Cell>{renderQuota(channel.used_quota)}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Popup
|
||||
content={channel.balance_updated_time ? renderTimestamp(channel.balance_updated_time) : '未更新'}
|
||||
|
||||
@@ -46,6 +46,11 @@ let headerButtons = [
|
||||
to: '/log',
|
||||
icon: 'book'
|
||||
},
|
||||
{
|
||||
name: 'Midjourney',
|
||||
to: '/midjourney',
|
||||
icon: 'images outline'
|
||||
},
|
||||
{
|
||||
name: '设置',
|
||||
to: '/setting',
|
||||
|
||||
385
web/src/components/MjLogsTable.js
Normal file
385
web/src/components/MjLogsTable.js
Normal file
@@ -0,0 +1,385 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Button, Form, Header, Label, Pagination, Segment, Select, Table } from 'semantic-ui-react';
|
||||
import { API, isAdmin, showError, timestamp2string } from '../helpers';
|
||||
|
||||
import { ITEMS_PER_PAGE } from '../constants';
|
||||
import { renderQuota } from '../helpers/render';
|
||||
import {Link} from "react-router-dom";
|
||||
|
||||
function renderTimestamp(timestamp) {
|
||||
return (
|
||||
<>
|
||||
{timestamp2string(timestamp)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const MODE_OPTIONS = [
|
||||
{ key: 'all', text: '全部用户', value: 'all' },
|
||||
{ key: 'self', text: '当前用户', value: 'self' }
|
||||
];
|
||||
|
||||
const LOG_OPTIONS = [
|
||||
{ key: '0', text: '全部', value: 0 },
|
||||
// { key: '1', text: '绘图', value: 1 },
|
||||
// { key: '2', text: '放大', value: 2 },
|
||||
// { key: '3', text: '变换', value: 3 },
|
||||
// { key: '4', text: '图生文', value: 4 },
|
||||
// { key: '5', text: '图片混合', value: 5 }
|
||||
];
|
||||
|
||||
function renderType(type) {
|
||||
switch (type) {
|
||||
case 'IMAGINE':
|
||||
return <Label basic color='blue'> 绘图 </Label>;
|
||||
case 'UPSCALE':
|
||||
return <Label basic color='orange'> 放大 </Label>;
|
||||
case 'VARIATION':
|
||||
return <Label basic color='purple'> 变换 </Label>;
|
||||
case 'DESCRIBE':
|
||||
return <Label basic color='yellow'> 图生文 </Label>;
|
||||
case 'BLEAND':
|
||||
return <Label basic color='olive'> 图混合 </Label>;
|
||||
default:
|
||||
return <Label basic color='black'> 未知 </Label>;
|
||||
}
|
||||
}
|
||||
|
||||
function renderCode(type) {
|
||||
switch (type) {
|
||||
case 1:
|
||||
return <Label basic color='green'> 已提交 </Label>;
|
||||
case 21:
|
||||
return <Label basic color='olive'> 排队中 </Label>;
|
||||
case 22:
|
||||
return <Label basic color='orange'> 重复提交 </Label>;
|
||||
default:
|
||||
return <Label basic color='black'> 未知 </Label>;
|
||||
}
|
||||
}
|
||||
|
||||
function renderStatus(type) {
|
||||
switch (type) {
|
||||
case 'SUCCESS':
|
||||
return <Label basic color='green'> 成功 </Label>;
|
||||
case 'NOT_START':
|
||||
return <Label basic color='black'> 未启动 </Label>;
|
||||
case 'SUBMITTED':
|
||||
return <Label basic color='yellow'> 队列中 </Label>;
|
||||
case 'IN_PROGRESS':
|
||||
return <Label basic color='blue'> 执行中 </Label>;
|
||||
case 'FAILURE':
|
||||
return <Label basic color='red'> 失败 </Label>;
|
||||
default:
|
||||
return <Label basic color='black'> 未知 </Label>;
|
||||
}
|
||||
}
|
||||
|
||||
const LogsTable = () => {
|
||||
const [logs, setLogs] = useState([
|
||||
|
||||
]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [activePage, setActivePage] = useState(1);
|
||||
const [searchKeyword, setSearchKeyword] = useState('');
|
||||
const [searching, setSearching] = useState(false);
|
||||
const [logType, setLogType] = useState(0);
|
||||
const isAdminUser = isAdmin();
|
||||
let now = new Date();
|
||||
const [inputs, setInputs] = useState({
|
||||
username: '',
|
||||
token_name: '',
|
||||
model_name: '',
|
||||
start_timestamp: timestamp2string(0),
|
||||
end_timestamp: timestamp2string(now.getTime() / 1000 + 3600)
|
||||
});
|
||||
const { username, token_name, model_name, start_timestamp, end_timestamp } = inputs;
|
||||
|
||||
const [stat, setStat] = useState({
|
||||
quota: 0,
|
||||
token: 0
|
||||
});
|
||||
|
||||
const handleInputChange = (e, { name, value }) => {
|
||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||
};
|
||||
|
||||
const getLogSelfStat = async () => {
|
||||
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
||||
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
||||
let res = await API.get(`/api/log/self/stat?type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
setStat(data);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
const getLogStat = async () => {
|
||||
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
||||
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
||||
let res = await API.get(`/api/log/stat?type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
setStat(data);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
const loadLogs = async (startIdx) => {
|
||||
let url = '';
|
||||
let localStartTimestamp = Date.parse(start_timestamp) / 1000;
|
||||
let localEndTimestamp = Date.parse(end_timestamp) / 1000;
|
||||
if (isAdminUser) {
|
||||
url = `/api/mj/?p=${startIdx}&username=${username}&token_name=${token_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
|
||||
} else {
|
||||
url = `/api/mj/self/?p=${startIdx}&token_name=${token_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`;
|
||||
}
|
||||
const res = await API.get(url);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
if (startIdx === 0) {
|
||||
setLogs(data);
|
||||
} else {
|
||||
let newLogs = [...logs];
|
||||
newLogs.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data);
|
||||
setLogs(newLogs);
|
||||
}
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const onPaginationChange = (e, { activePage }) => {
|
||||
(async () => {
|
||||
if (activePage === Math.ceil(logs.length / ITEMS_PER_PAGE) + 1) {
|
||||
// In this case we have to load more data and then append them.
|
||||
await loadLogs(activePage - 1);
|
||||
}
|
||||
setActivePage(activePage);
|
||||
})();
|
||||
};
|
||||
|
||||
const refresh = async () => {
|
||||
setLoading(true);
|
||||
setActivePage(1)
|
||||
await loadLogs(0);
|
||||
// if (isAdminUser) {
|
||||
// getLogStat().then();
|
||||
// } else {
|
||||
// getLogSelfStat().then();
|
||||
// }
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
refresh().then();
|
||||
}, [logType]);
|
||||
|
||||
const searchLogs = async () => {
|
||||
if (searchKeyword === '') {
|
||||
// if keyword is blank, load files instead.
|
||||
await loadLogs(0);
|
||||
setActivePage(1);
|
||||
return;
|
||||
}
|
||||
setSearching(true);
|
||||
const res = await API.get(`/api/log/self/search?keyword=${searchKeyword}`);
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
setLogs(data);
|
||||
setActivePage(1);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
setSearching(false);
|
||||
};
|
||||
|
||||
const handleKeywordChange = async (e, { value }) => {
|
||||
setSearchKeyword(value.trim());
|
||||
};
|
||||
|
||||
const sortLog = (key) => {
|
||||
if (logs.length === 0) return;
|
||||
setLoading(true);
|
||||
let sortedLogs = [...logs];
|
||||
if (typeof sortedLogs[0][key] === 'string'){
|
||||
sortedLogs.sort((a, b) => {
|
||||
return ('' + a[key]).localeCompare(b[key]);
|
||||
});
|
||||
} else {
|
||||
sortedLogs.sort((a, b) => {
|
||||
if (a[key] === b[key]) return 0;
|
||||
if (a[key] > b[key]) return -1;
|
||||
if (a[key] < b[key]) return 1;
|
||||
});
|
||||
}
|
||||
if (sortedLogs[0].id === logs[0].id) {
|
||||
sortedLogs.reverse();
|
||||
}
|
||||
setLogs(sortedLogs);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Segment>
|
||||
<Table basic compact size='small'>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortLog('submit_time');
|
||||
}}
|
||||
width={2}
|
||||
>
|
||||
提交时间
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortLog('action');
|
||||
}}
|
||||
width={1}
|
||||
>
|
||||
类型
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortLog('mj_id');
|
||||
}}
|
||||
width={2}
|
||||
>
|
||||
任务ID
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortLog('code');
|
||||
}}
|
||||
width={1}
|
||||
>
|
||||
提交结果
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortLog('status');
|
||||
}}
|
||||
width={1}
|
||||
>
|
||||
任务状态
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortLog('progress');
|
||||
}}
|
||||
width={1}
|
||||
>
|
||||
进度
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortLog('image_url');
|
||||
}}
|
||||
width={1}
|
||||
>
|
||||
结果图片
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortLog('prompt');
|
||||
}}
|
||||
width={3}
|
||||
>
|
||||
Prompt
|
||||
</Table.HeaderCell>
|
||||
<Table.HeaderCell
|
||||
style={{ cursor: 'pointer' }}
|
||||
onClick={() => {
|
||||
sortLog('fail_reason');
|
||||
}}
|
||||
width={1}
|
||||
>
|
||||
失败原因
|
||||
</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
|
||||
<Table.Body>
|
||||
{logs
|
||||
.slice(
|
||||
(activePage - 1) * ITEMS_PER_PAGE,
|
||||
activePage * ITEMS_PER_PAGE
|
||||
)
|
||||
.map((log, idx) => {
|
||||
if (log.deleted) return <></>;
|
||||
return (
|
||||
<Table.Row key={log.created_at}>
|
||||
<Table.Cell>{renderTimestamp(log.submit_time/1000)}</Table.Cell>
|
||||
{/*{*/}
|
||||
{/* isAdminUser && (*/}
|
||||
{/* <Table.Cell>{log.username ? <Label>{log.username}</Label> : ''}</Table.Cell>*/}
|
||||
{/* )*/}
|
||||
{/*}*/}
|
||||
<Table.Cell>{renderType(log.action)}</Table.Cell>
|
||||
<Table.Cell>{log.mj_id}</Table.Cell>
|
||||
<Table.Cell>{renderCode(log.code)}</Table.Cell>
|
||||
<Table.Cell>{renderStatus(log.status)}</Table.Cell>
|
||||
<Table.Cell>{log.progress ? <Label basic>{log.progress}</Label> : ''}</Table.Cell>
|
||||
<Table.Cell>
|
||||
{
|
||||
log.image_url ? (
|
||||
<Link to={log.image_url} target='_blank'>点击查看</Link>
|
||||
) : '暂未生成图片'
|
||||
}
|
||||
</Table.Cell>
|
||||
<Table.Cell>{log.prompt}</Table.Cell>
|
||||
<Table.Cell>{log.fail_reason ? log.fail_reason : '无'}</Table.Cell>
|
||||
</Table.Row>
|
||||
);
|
||||
})}
|
||||
</Table.Body>
|
||||
|
||||
<Table.Footer>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell colSpan={'9'}>
|
||||
<Select
|
||||
placeholder='选择明细分类'
|
||||
options={LOG_OPTIONS}
|
||||
style={{ marginRight: '8px' }}
|
||||
name='logType'
|
||||
value={logType}
|
||||
onChange={(e, { name, value }) => {
|
||||
setLogType(value);
|
||||
}}
|
||||
/>
|
||||
<Button size='small' onClick={refresh} loading={loading}>刷新</Button>
|
||||
<Pagination
|
||||
floated='right'
|
||||
activePage={activePage}
|
||||
onPageChange={onPaginationChange}
|
||||
size='small'
|
||||
siblingRange={1}
|
||||
totalPages={
|
||||
Math.ceil(logs.length / ITEMS_PER_PAGE) +
|
||||
(logs.length % ITEMS_PER_PAGE === 0 ? 1 : 0)
|
||||
}
|
||||
/>
|
||||
</Table.HeaderCell>
|
||||
</Table.Row>
|
||||
</Table.Footer>
|
||||
</Table>
|
||||
</Segment>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default LogsTable;
|
||||
@@ -1,331 +1,372 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { Divider, Form, Grid, Header } from 'semantic-ui-react';
|
||||
import { API, showError, verifyJSON } from '../helpers';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Divider, Form, Grid, Header} from 'semantic-ui-react';
|
||||
import {API, showError, verifyJSON} from '../helpers';
|
||||
|
||||
const OperationSetting = () => {
|
||||
let [inputs, setInputs] = useState({
|
||||
QuotaForNewUser: 0,
|
||||
QuotaForInviter: 0,
|
||||
QuotaForInvitee: 0,
|
||||
QuotaRemindThreshold: 0,
|
||||
PreConsumedQuota: 0,
|
||||
ModelRatio: '',
|
||||
GroupRatio: '',
|
||||
TopUpLink: '',
|
||||
ChatLink: '',
|
||||
QuotaPerUnit: 0,
|
||||
AutomaticDisableChannelEnabled: '',
|
||||
ChannelDisableThreshold: 0,
|
||||
LogConsumeEnabled: '',
|
||||
DisplayInCurrencyEnabled: '',
|
||||
DisplayTokenStatEnabled: '',
|
||||
ApproximateTokenEnabled: '',
|
||||
RetryTimes: 0,
|
||||
});
|
||||
const [originInputs, setOriginInputs] = useState({});
|
||||
let [loading, setLoading] = useState(false);
|
||||
|
||||
const getOptions = async () => {
|
||||
const res = await API.get('/api/option/');
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
let newInputs = {};
|
||||
data.forEach((item) => {
|
||||
if (item.key === 'ModelRatio' || item.key === 'GroupRatio') {
|
||||
item.value = JSON.stringify(JSON.parse(item.value), null, 2);
|
||||
}
|
||||
newInputs[item.key] = item.value;
|
||||
});
|
||||
setInputs(newInputs);
|
||||
setOriginInputs(newInputs);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getOptions().then();
|
||||
}, []);
|
||||
|
||||
const updateOption = async (key, value) => {
|
||||
setLoading(true);
|
||||
if (key.endsWith('Enabled')) {
|
||||
value = inputs[key] === 'true' ? 'false' : 'true';
|
||||
}
|
||||
const res = await API.put('/api/option/', {
|
||||
key,
|
||||
value
|
||||
let [inputs, setInputs] = useState({
|
||||
QuotaForNewUser: 0,
|
||||
QuotaForInviter: 0,
|
||||
QuotaForInvitee: 0,
|
||||
QuotaRemindThreshold: 0,
|
||||
PreConsumedQuota: 0,
|
||||
ModelRatio: '',
|
||||
GroupRatio: '',
|
||||
TopUpLink: '',
|
||||
ChatLink: '',
|
||||
QuotaPerUnit: 0,
|
||||
AutomaticDisableChannelEnabled: '',
|
||||
ChannelDisableThreshold: 0,
|
||||
LogConsumeEnabled: '',
|
||||
DisplayInCurrencyEnabled: '',
|
||||
DisplayTokenStatEnabled: '',
|
||||
ApproximateTokenEnabled: '',
|
||||
RetryTimes: 0,
|
||||
StablePrice: 6,
|
||||
NormalPrice: 1.5,
|
||||
BasePrice: 1.5,
|
||||
});
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
setInputs((inputs) => ({ ...inputs, [key]: value }));
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
const [originInputs, setOriginInputs] = useState({});
|
||||
let [loading, setLoading] = useState(false);
|
||||
|
||||
const handleInputChange = async (e, { name, value }) => {
|
||||
if (name.endsWith('Enabled')) {
|
||||
await updateOption(name, value);
|
||||
} else {
|
||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||
}
|
||||
};
|
||||
const getOptions = async () => {
|
||||
const res = await API.get('/api/option/');
|
||||
const {success, message, data} = res.data;
|
||||
if (success) {
|
||||
let newInputs = {};
|
||||
data.forEach((item) => {
|
||||
if (item.key === 'ModelRatio' || item.key === 'GroupRatio') {
|
||||
item.value = JSON.stringify(JSON.parse(item.value), null, 2);
|
||||
}
|
||||
newInputs[item.key] = item.value;
|
||||
});
|
||||
setInputs(newInputs);
|
||||
setOriginInputs(newInputs);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
const submitConfig = async (group) => {
|
||||
switch (group) {
|
||||
case 'monitor':
|
||||
if (originInputs['ChannelDisableThreshold'] !== inputs.ChannelDisableThreshold) {
|
||||
await updateOption('ChannelDisableThreshold', inputs.ChannelDisableThreshold);
|
||||
}
|
||||
if (originInputs['QuotaRemindThreshold'] !== inputs.QuotaRemindThreshold) {
|
||||
await updateOption('QuotaRemindThreshold', inputs.QuotaRemindThreshold);
|
||||
}
|
||||
break;
|
||||
case 'ratio':
|
||||
if (originInputs['ModelRatio'] !== inputs.ModelRatio) {
|
||||
if (!verifyJSON(inputs.ModelRatio)) {
|
||||
showError('模型倍率不是合法的 JSON 字符串');
|
||||
return;
|
||||
}
|
||||
await updateOption('ModelRatio', inputs.ModelRatio);
|
||||
}
|
||||
if (originInputs['GroupRatio'] !== inputs.GroupRatio) {
|
||||
if (!verifyJSON(inputs.GroupRatio)) {
|
||||
showError('分组倍率不是合法的 JSON 字符串');
|
||||
return;
|
||||
}
|
||||
await updateOption('GroupRatio', inputs.GroupRatio);
|
||||
}
|
||||
break;
|
||||
case 'quota':
|
||||
if (originInputs['QuotaForNewUser'] !== inputs.QuotaForNewUser) {
|
||||
await updateOption('QuotaForNewUser', inputs.QuotaForNewUser);
|
||||
}
|
||||
if (originInputs['QuotaForInvitee'] !== inputs.QuotaForInvitee) {
|
||||
await updateOption('QuotaForInvitee', inputs.QuotaForInvitee);
|
||||
}
|
||||
if (originInputs['QuotaForInviter'] !== inputs.QuotaForInviter) {
|
||||
await updateOption('QuotaForInviter', inputs.QuotaForInviter);
|
||||
}
|
||||
if (originInputs['PreConsumedQuota'] !== inputs.PreConsumedQuota) {
|
||||
await updateOption('PreConsumedQuota', inputs.PreConsumedQuota);
|
||||
}
|
||||
break;
|
||||
case 'general':
|
||||
if (originInputs['TopUpLink'] !== inputs.TopUpLink) {
|
||||
await updateOption('TopUpLink', inputs.TopUpLink);
|
||||
}
|
||||
if (originInputs['ChatLink'] !== inputs.ChatLink) {
|
||||
await updateOption('ChatLink', inputs.ChatLink);
|
||||
}
|
||||
if (originInputs['QuotaPerUnit'] !== inputs.QuotaPerUnit) {
|
||||
await updateOption('QuotaPerUnit', inputs.QuotaPerUnit);
|
||||
}
|
||||
if (originInputs['RetryTimes'] !== inputs.RetryTimes) {
|
||||
await updateOption('RetryTimes', inputs.RetryTimes);
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
getOptions().then();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Grid columns={1}>
|
||||
<Grid.Column>
|
||||
<Form loading={loading}>
|
||||
<Header as='h3'>
|
||||
通用设置
|
||||
</Header>
|
||||
<Form.Group widths={4}>
|
||||
<Form.Input
|
||||
label='充值链接'
|
||||
name='TopUpLink'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.TopUpLink}
|
||||
type='link'
|
||||
placeholder='例如发卡网站的购买链接'
|
||||
/>
|
||||
<Form.Input
|
||||
label='聊天页面链接'
|
||||
name='ChatLink'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.ChatLink}
|
||||
type='link'
|
||||
placeholder='例如 ChatGPT Next Web 的部署地址'
|
||||
/>
|
||||
<Form.Input
|
||||
label='单位美元额度'
|
||||
name='QuotaPerUnit'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.QuotaPerUnit}
|
||||
type='number'
|
||||
step='0.01'
|
||||
placeholder='一单位货币能兑换的额度'
|
||||
/>
|
||||
<Form.Input
|
||||
label='失败重试次数'
|
||||
name='RetryTimes'
|
||||
type={'number'}
|
||||
step='1'
|
||||
min='0'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.RetryTimes}
|
||||
placeholder='失败重试次数'
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group inline>
|
||||
<Form.Checkbox
|
||||
checked={inputs.LogConsumeEnabled === 'true'}
|
||||
label='启用额度消费日志记录'
|
||||
name='LogConsumeEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Checkbox
|
||||
checked={inputs.DisplayInCurrencyEnabled === 'true'}
|
||||
label='以货币形式显示额度'
|
||||
name='DisplayInCurrencyEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Checkbox
|
||||
checked={inputs.DisplayTokenStatEnabled === 'true'}
|
||||
label='Billing 相关 API 显示令牌额度而非用户额度'
|
||||
name='DisplayTokenStatEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Checkbox
|
||||
checked={inputs.ApproximateTokenEnabled === 'true'}
|
||||
label='使用近似的方式估算 token 数以减少计算量'
|
||||
name='ApproximateTokenEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={() => {
|
||||
submitConfig('general').then();
|
||||
}}>保存通用设置</Form.Button>
|
||||
<Divider />
|
||||
<Header as='h3'>
|
||||
监控设置
|
||||
</Header>
|
||||
<Form.Group widths={3}>
|
||||
<Form.Input
|
||||
label='最长响应时间'
|
||||
name='ChannelDisableThreshold'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.ChannelDisableThreshold}
|
||||
type='number'
|
||||
min='0'
|
||||
placeholder='单位秒,当运行通道全部测试时,超过此时间将自动禁用通道'
|
||||
/>
|
||||
<Form.Input
|
||||
label='额度提醒阈值'
|
||||
name='QuotaRemindThreshold'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.QuotaRemindThreshold}
|
||||
type='number'
|
||||
min='0'
|
||||
placeholder='低于此额度时将发送邮件提醒用户'
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group inline>
|
||||
<Form.Checkbox
|
||||
checked={inputs.AutomaticDisableChannelEnabled === 'true'}
|
||||
label='失败时自动禁用通道'
|
||||
name='AutomaticDisableChannelEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={() => {
|
||||
submitConfig('monitor').then();
|
||||
}}>保存监控设置</Form.Button>
|
||||
<Divider />
|
||||
<Header as='h3'>
|
||||
额度设置
|
||||
</Header>
|
||||
<Form.Group widths={4}>
|
||||
<Form.Input
|
||||
label='新用户初始额度'
|
||||
name='QuotaForNewUser'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.QuotaForNewUser}
|
||||
type='number'
|
||||
min='0'
|
||||
placeholder='例如:100'
|
||||
/>
|
||||
<Form.Input
|
||||
label='请求预扣费额度'
|
||||
name='PreConsumedQuota'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.PreConsumedQuota}
|
||||
type='number'
|
||||
min='0'
|
||||
placeholder='请求结束后多退少补'
|
||||
/>
|
||||
<Form.Input
|
||||
label='邀请新用户奖励额度'
|
||||
name='QuotaForInviter'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.QuotaForInviter}
|
||||
type='number'
|
||||
min='0'
|
||||
placeholder='例如:2000'
|
||||
/>
|
||||
<Form.Input
|
||||
label='新用户使用邀请码奖励额度'
|
||||
name='QuotaForInvitee'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.QuotaForInvitee}
|
||||
type='number'
|
||||
min='0'
|
||||
placeholder='例如:1000'
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={() => {
|
||||
submitConfig('quota').then();
|
||||
}}>保存额度设置</Form.Button>
|
||||
<Divider />
|
||||
<Header as='h3'>
|
||||
倍率设置
|
||||
</Header>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.TextArea
|
||||
label='模型倍率'
|
||||
name='ModelRatio'
|
||||
onChange={handleInputChange}
|
||||
style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
|
||||
autoComplete='new-password'
|
||||
value={inputs.ModelRatio}
|
||||
placeholder='为一个 JSON 文本,键为模型名称,值为倍率'
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.TextArea
|
||||
label='分组倍率'
|
||||
name='GroupRatio'
|
||||
onChange={handleInputChange}
|
||||
style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
|
||||
autoComplete='new-password'
|
||||
value={inputs.GroupRatio}
|
||||
placeholder='为一个 JSON 文本,键为分组名称,值为倍率'
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={() => {
|
||||
submitConfig('ratio').then();
|
||||
}}>保存倍率设置</Form.Button>
|
||||
</Form>
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
);
|
||||
const updateOption = async (key, value) => {
|
||||
setLoading(true);
|
||||
if (key.endsWith('Enabled')) {
|
||||
value = inputs[key] === 'true' ? 'false' : 'true';
|
||||
}
|
||||
const res = await API.put('/api/option/', {
|
||||
key,
|
||||
value
|
||||
});
|
||||
const {success, message} = res.data;
|
||||
if (success) {
|
||||
setInputs((inputs) => ({...inputs, [key]: value}));
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const handleInputChange = async (e, {name, value}) => {
|
||||
if (name.endsWith('Enabled')) {
|
||||
await updateOption(name, value);
|
||||
} else {
|
||||
setInputs((inputs) => ({...inputs, [name]: value}));
|
||||
}
|
||||
};
|
||||
|
||||
const submitConfig = async (group) => {
|
||||
switch (group) {
|
||||
case 'monitor':
|
||||
if (originInputs['ChannelDisableThreshold'] !== inputs.ChannelDisableThreshold) {
|
||||
await updateOption('ChannelDisableThreshold', inputs.ChannelDisableThreshold);
|
||||
}
|
||||
if (originInputs['QuotaRemindThreshold'] !== inputs.QuotaRemindThreshold) {
|
||||
await updateOption('QuotaRemindThreshold', inputs.QuotaRemindThreshold);
|
||||
}
|
||||
break;
|
||||
case 'stable':
|
||||
await updateOption('StablePrice', inputs.StablePrice);
|
||||
await updateOption('NormalPrice', inputs.NormalPrice);
|
||||
await updateOption('BasePrice', inputs.BasePrice);
|
||||
localStorage.setItem('stable_price', inputs.StablePrice);
|
||||
localStorage.setItem('normal_price', inputs.NormalPrice);
|
||||
localStorage.setItem('base_price', inputs.BasePrice);
|
||||
break;
|
||||
case 'ratio':
|
||||
if (originInputs['ModelRatio'] !== inputs.ModelRatio) {
|
||||
if (!verifyJSON(inputs.ModelRatio)) {
|
||||
showError('模型倍率不是合法的 JSON 字符串');
|
||||
return;
|
||||
}
|
||||
await updateOption('ModelRatio', inputs.ModelRatio);
|
||||
}
|
||||
if (originInputs['GroupRatio'] !== inputs.GroupRatio) {
|
||||
if (!verifyJSON(inputs.GroupRatio)) {
|
||||
showError('分组倍率不是合法的 JSON 字符串');
|
||||
return;
|
||||
}
|
||||
await updateOption('GroupRatio', inputs.GroupRatio);
|
||||
}
|
||||
break;
|
||||
case 'quota':
|
||||
if (originInputs['QuotaForNewUser'] !== inputs.QuotaForNewUser) {
|
||||
await updateOption('QuotaForNewUser', inputs.QuotaForNewUser);
|
||||
}
|
||||
if (originInputs['QuotaForInvitee'] !== inputs.QuotaForInvitee) {
|
||||
await updateOption('QuotaForInvitee', inputs.QuotaForInvitee);
|
||||
}
|
||||
if (originInputs['QuotaForInviter'] !== inputs.QuotaForInviter) {
|
||||
await updateOption('QuotaForInviter', inputs.QuotaForInviter);
|
||||
}
|
||||
if (originInputs['PreConsumedQuota'] !== inputs.PreConsumedQuota) {
|
||||
await updateOption('PreConsumedQuota', inputs.PreConsumedQuota);
|
||||
}
|
||||
break;
|
||||
case 'general':
|
||||
if (originInputs['TopUpLink'] !== inputs.TopUpLink) {
|
||||
await updateOption('TopUpLink', inputs.TopUpLink);
|
||||
}
|
||||
if (originInputs['ChatLink'] !== inputs.ChatLink) {
|
||||
await updateOption('ChatLink', inputs.ChatLink);
|
||||
}
|
||||
if (originInputs['QuotaPerUnit'] !== inputs.QuotaPerUnit) {
|
||||
await updateOption('QuotaPerUnit', inputs.QuotaPerUnit);
|
||||
}
|
||||
if (originInputs['RetryTimes'] !== inputs.RetryTimes) {
|
||||
await updateOption('RetryTimes', inputs.RetryTimes);
|
||||
}
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Grid columns={1}>
|
||||
<Grid.Column>
|
||||
<Form loading={loading}>
|
||||
<Header as='h3'>
|
||||
通用设置
|
||||
</Header>
|
||||
<Form.Group widths={4}>
|
||||
<Form.Input
|
||||
label='充值链接'
|
||||
name='TopUpLink'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.TopUpLink}
|
||||
type='link'
|
||||
placeholder='例如发卡网站的购买链接'
|
||||
/>
|
||||
<Form.Input
|
||||
label='聊天页面链接'
|
||||
name='ChatLink'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.ChatLink}
|
||||
type='link'
|
||||
placeholder='例如 ChatGPT Next Web 的部署地址'
|
||||
/>
|
||||
<Form.Input
|
||||
label='单位美元额度'
|
||||
name='QuotaPerUnit'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.QuotaPerUnit}
|
||||
type='number'
|
||||
step='0.01'
|
||||
placeholder='一单位货币能兑换的额度'
|
||||
/>
|
||||
<Form.Input
|
||||
label='失败重试次数'
|
||||
name='RetryTimes'
|
||||
type={'number'}
|
||||
step='1'
|
||||
min='0'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.RetryTimes}
|
||||
placeholder='失败重试次数'
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group inline>
|
||||
<Form.Checkbox
|
||||
checked={inputs.LogConsumeEnabled === 'true'}
|
||||
label='启用额度消费日志记录'
|
||||
name='LogConsumeEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Checkbox
|
||||
checked={inputs.DisplayInCurrencyEnabled === 'true'}
|
||||
label='以货币形式显示额度'
|
||||
name='DisplayInCurrencyEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Checkbox
|
||||
checked={inputs.DisplayTokenStatEnabled === 'true'}
|
||||
label='Billing 相关 API 显示令牌额度而非用户额度'
|
||||
name='DisplayTokenStatEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Form.Checkbox
|
||||
checked={inputs.ApproximateTokenEnabled === 'true'}
|
||||
label='使用近似的方式估算 token 数以减少计算量'
|
||||
name='ApproximateTokenEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={() => {
|
||||
submitConfig('general').then();
|
||||
}}>保存通用设置</Form.Button>
|
||||
<Divider/>
|
||||
<Header as='h3'>
|
||||
监控设置
|
||||
</Header>
|
||||
<Form.Group widths={3}>
|
||||
<Form.Input
|
||||
label='最长响应时间'
|
||||
name='ChannelDisableThreshold'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.ChannelDisableThreshold}
|
||||
type='number'
|
||||
min='0'
|
||||
placeholder='单位秒,当运行通道全部测试时,超过此时间将自动禁用通道'
|
||||
/>
|
||||
<Form.Input
|
||||
label='额度提醒阈值'
|
||||
name='QuotaRemindThreshold'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.QuotaRemindThreshold}
|
||||
type='number'
|
||||
min='0'
|
||||
placeholder='低于此额度时将发送邮件提醒用户'
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group inline>
|
||||
<Form.Checkbox
|
||||
checked={inputs.AutomaticDisableChannelEnabled === 'true'}
|
||||
label='失败时自动禁用通道'
|
||||
name='AutomaticDisableChannelEnabled'
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={() => {
|
||||
submitConfig('monitor').then();
|
||||
}}>保存监控设置</Form.Button>
|
||||
<Divider/>
|
||||
<Header as='h3'>
|
||||
通道设置
|
||||
</Header>
|
||||
<Form.Group widths={3}>
|
||||
<Form.Input
|
||||
label='普通渠道价格'
|
||||
name='NormalPrice'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.NormalPrice}
|
||||
type='number'
|
||||
// min='1.5'
|
||||
placeholder='n元/刀'
|
||||
/>
|
||||
<Form.Input
|
||||
label='稳定渠道价格'
|
||||
name='StablePrice'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.StablePrice}
|
||||
type='number'
|
||||
// min='1.5'
|
||||
placeholder='n元/刀'
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={() => {
|
||||
submitConfig('stable').then();
|
||||
}}>保存通道设置</Form.Button>
|
||||
<Divider/>
|
||||
<Header as='h3'>
|
||||
额度设置
|
||||
</Header>
|
||||
<Form.Group widths={4}>
|
||||
<Form.Input
|
||||
label='新用户初始额度'
|
||||
name='QuotaForNewUser'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.QuotaForNewUser}
|
||||
type='number'
|
||||
min='0'
|
||||
placeholder='例如:100'
|
||||
/>
|
||||
<Form.Input
|
||||
label='请求预扣费额度'
|
||||
name='PreConsumedQuota'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.PreConsumedQuota}
|
||||
type='number'
|
||||
min='0'
|
||||
placeholder='请求结束后多退少补'
|
||||
/>
|
||||
<Form.Input
|
||||
label='邀请新用户奖励额度'
|
||||
name='QuotaForInviter'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.QuotaForInviter}
|
||||
type='number'
|
||||
min='0'
|
||||
placeholder='例如:2000'
|
||||
/>
|
||||
<Form.Input
|
||||
label='新用户使用邀请码奖励额度'
|
||||
name='QuotaForInvitee'
|
||||
onChange={handleInputChange}
|
||||
autoComplete='new-password'
|
||||
value={inputs.QuotaForInvitee}
|
||||
type='number'
|
||||
min='0'
|
||||
placeholder='例如:1000'
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={() => {
|
||||
submitConfig('quota').then();
|
||||
}}>保存额度设置</Form.Button>
|
||||
<Divider/>
|
||||
<Header as='h3'>
|
||||
倍率设置
|
||||
</Header>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.TextArea
|
||||
label='模型倍率'
|
||||
name='ModelRatio'
|
||||
onChange={handleInputChange}
|
||||
style={{minHeight: 250, fontFamily: 'JetBrains Mono, Consolas'}}
|
||||
autoComplete='new-password'
|
||||
value={inputs.ModelRatio}
|
||||
placeholder='为一个 JSON 文本,键为模型名称,值为倍率'
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Group widths='equal'>
|
||||
<Form.TextArea
|
||||
label='分组倍率'
|
||||
name='GroupRatio'
|
||||
onChange={handleInputChange}
|
||||
style={{minHeight: 250, fontFamily: 'JetBrains Mono, Consolas'}}
|
||||
autoComplete='new-password'
|
||||
value={inputs.GroupRatio}
|
||||
placeholder='为一个 JSON 文本,键为分组名称,值为倍率'
|
||||
/>
|
||||
</Form.Group>
|
||||
<Form.Button onClick={() => {
|
||||
submitConfig('ratio').then();
|
||||
}}>保存倍率设置</Form.Button>
|
||||
</Form>
|
||||
</Grid.Column>
|
||||
</Grid>
|
||||
)
|
||||
;
|
||||
};
|
||||
|
||||
export default OperationSetting;
|
||||
|
||||
@@ -1,323 +1,410 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Button, Divider, Form, Header, Image, Message, Modal } from 'semantic-ui-react';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { API, copy, showError, showInfo, showNotice, showSuccess } from '../helpers';
|
||||
import React, {useContext, useEffect, useState} from 'react';
|
||||
import {Button, Checkbox, Divider, Form, Header, Image, Input, Message, Modal} from 'semantic-ui-react';
|
||||
import {Link, useNavigate} from 'react-router-dom';
|
||||
import {API, copy, showError, showInfo, showNotice, showSuccess} from '../helpers';
|
||||
import Turnstile from 'react-turnstile';
|
||||
import { UserContext } from '../context/User';
|
||||
import {UserContext} from '../context/User';
|
||||
|
||||
const PersonalSetting = () => {
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
let navigate = useNavigate();
|
||||
const [userState, userDispatch] = useContext(UserContext);
|
||||
let navigate = useNavigate();
|
||||
|
||||
const [inputs, setInputs] = useState({
|
||||
wechat_verification_code: '',
|
||||
email_verification_code: '',
|
||||
email: '',
|
||||
self_account_deletion_confirmation: ''
|
||||
});
|
||||
const [status, setStatus] = useState({});
|
||||
const [showWeChatBindModal, setShowWeChatBindModal] = useState(false);
|
||||
const [showEmailBindModal, setShowEmailBindModal] = useState(false);
|
||||
const [showAccountDeleteModal, setShowAccountDeleteModal] = useState(false);
|
||||
const [turnstileEnabled, setTurnstileEnabled] = useState(false);
|
||||
const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
|
||||
const [turnstileToken, setTurnstileToken] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [disableButton, setDisableButton] = useState(false);
|
||||
const [countdown, setCountdown] = useState(30);
|
||||
const [inputs, setInputs] = useState({
|
||||
wechat_verification_code: '',
|
||||
email_verification_code: '',
|
||||
email: '',
|
||||
self_account_deletion_confirmation: ''
|
||||
});
|
||||
const [stableMode, setStableMode] = useState({
|
||||
stableMode: false,
|
||||
maxPrice: 7,
|
||||
});
|
||||
const [status, setStatus] = useState({});
|
||||
const [showWeChatBindModal, setShowWeChatBindModal] = useState(false);
|
||||
const [showEmailBindModal, setShowEmailBindModal] = useState(false);
|
||||
const [showAccountDeleteModal, setShowAccountDeleteModal] = useState(false);
|
||||
const [turnstileEnabled, setTurnstileEnabled] = useState(false);
|
||||
const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
|
||||
const [turnstileToken, setTurnstileToken] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [disableButton, setDisableButton] = useState(false);
|
||||
const [countdown, setCountdown] = useState(30);
|
||||
|
||||
useEffect(() => {
|
||||
let status = localStorage.getItem('status');
|
||||
if (status) {
|
||||
status = JSON.parse(status);
|
||||
setStatus(status);
|
||||
if (status.turnstile_check) {
|
||||
setTurnstileEnabled(true);
|
||||
setTurnstileSiteKey(status.turnstile_site_key);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
// setStableMode(userState.user.stableMode, userState.user.maxPrice);
|
||||
console.log(userState.user)
|
||||
|
||||
useEffect(() => {
|
||||
let countdownInterval = null;
|
||||
if (disableButton && countdown > 0) {
|
||||
countdownInterval = setInterval(() => {
|
||||
setCountdown(countdown - 1);
|
||||
}, 1000);
|
||||
} else if (countdown === 0) {
|
||||
setDisableButton(false);
|
||||
setCountdown(30);
|
||||
}
|
||||
return () => clearInterval(countdownInterval); // Clean up on unmount
|
||||
}, [disableButton, countdown]);
|
||||
|
||||
const handleInputChange = (e, { name, value }) => {
|
||||
setInputs((inputs) => ({ ...inputs, [name]: value }));
|
||||
};
|
||||
useEffect(() => {
|
||||
let status = localStorage.getItem('status');
|
||||
if (status) {
|
||||
status = JSON.parse(status);
|
||||
setStatus(status);
|
||||
if (status.turnstile_check) {
|
||||
setTurnstileEnabled(true);
|
||||
setTurnstileSiteKey(status.turnstile_site_key);
|
||||
}
|
||||
}
|
||||
// if (userState.user !== undefined) {
|
||||
// setStableMode(userState.user.stable_mode, userState.user.max_price);
|
||||
// }
|
||||
}, []);
|
||||
|
||||
const generateAccessToken = async () => {
|
||||
const res = await API.get('/api/user/token');
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
await copy(data);
|
||||
showSuccess(`令牌已重置并已复制到剪贴板:${data}`);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
let countdownInterval = null;
|
||||
if (disableButton && countdown > 0) {
|
||||
countdownInterval = setInterval(() => {
|
||||
setCountdown(countdown - 1);
|
||||
}, 1000);
|
||||
} else if (countdown === 0) {
|
||||
setDisableButton(false);
|
||||
setCountdown(30);
|
||||
}
|
||||
return () => clearInterval(countdownInterval); // Clean up on unmount
|
||||
}, [disableButton, countdown]);
|
||||
|
||||
const getAffLink = async () => {
|
||||
const res = await API.get('/api/user/aff');
|
||||
const { success, message, data } = res.data;
|
||||
if (success) {
|
||||
let link = `${window.location.origin}/register?aff=${data}`;
|
||||
await copy(link);
|
||||
showNotice(`邀请链接已复制到剪切板:${link}`);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
if (userState.user !== undefined) {
|
||||
setStableMode({
|
||||
stableMode: userState.user.stable_mode,
|
||||
maxPrice: userState.user.max_price
|
||||
})
|
||||
// if (stableMode.localMaxPrice !== userState.user.max_price) {
|
||||
// setStableMode({
|
||||
// localMaxPrice: userState.user.max_price
|
||||
// })
|
||||
// }
|
||||
}
|
||||
}, [userState]);
|
||||
|
||||
const deleteAccount = async () => {
|
||||
if (inputs.self_account_deletion_confirmation !== userState.user.username) {
|
||||
showError('请输入你的账户名以确认删除!');
|
||||
return;
|
||||
}
|
||||
const handleInputChange = (e, {name, value}) => {
|
||||
setInputs((inputs) => ({...inputs, [name]: value}));
|
||||
};
|
||||
|
||||
const res = await API.delete('/api/user/self');
|
||||
const { success, message } = res.data;
|
||||
const generateAccessToken = async () => {
|
||||
const res = await API.get('/api/user/token');
|
||||
const {success, message, data} = res.data;
|
||||
if (success) {
|
||||
await copy(data);
|
||||
showSuccess(`令牌已重置并已复制到剪贴板:${data}`);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
if (success) {
|
||||
showSuccess('账户已删除!');
|
||||
await API.get('/api/user/logout');
|
||||
userDispatch({ type: 'logout' });
|
||||
localStorage.removeItem('user');
|
||||
navigate('/login');
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
const getAffLink = async () => {
|
||||
const res = await API.get('/api/user/aff');
|
||||
const {success, message, data} = res.data;
|
||||
if (success) {
|
||||
let link = `${window.location.origin}/register?aff=${data}`;
|
||||
await copy(link);
|
||||
showNotice(`邀请链接已复制到剪切板:${link}`);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
const bindWeChat = async () => {
|
||||
if (inputs.wechat_verification_code === '') return;
|
||||
const res = await API.get(
|
||||
`/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`
|
||||
);
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
showSuccess('微信账户绑定成功!');
|
||||
setShowWeChatBindModal(false);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
const deleteAccount = async () => {
|
||||
if (inputs.self_account_deletion_confirmation !== userState.user.username) {
|
||||
showError('请输入你的账户名以确认删除!');
|
||||
return;
|
||||
}
|
||||
|
||||
const openGitHubOAuth = () => {
|
||||
window.open(
|
||||
`https://github.com/login/oauth/authorize?client_id=${status.github_client_id}&scope=user:email`
|
||||
);
|
||||
};
|
||||
const res = await API.delete('/api/user/self');
|
||||
const {success, message} = res.data;
|
||||
|
||||
const sendVerificationCode = async () => {
|
||||
setDisableButton(true);
|
||||
if (inputs.email === '') return;
|
||||
if (turnstileEnabled && turnstileToken === '') {
|
||||
showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
const res = await API.get(
|
||||
`/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`
|
||||
);
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
showSuccess('验证码发送成功,请检查邮箱!');
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
if (success) {
|
||||
showSuccess('账户已删除!');
|
||||
await API.get('/api/user/logout');
|
||||
userDispatch({type: 'logout'});
|
||||
localStorage.removeItem('user');
|
||||
navigate('/login');
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
const bindEmail = async () => {
|
||||
if (inputs.email_verification_code === '') return;
|
||||
setLoading(true);
|
||||
const res = await API.get(
|
||||
`/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`
|
||||
);
|
||||
const { success, message } = res.data;
|
||||
if (success) {
|
||||
showSuccess('邮箱账户绑定成功!');
|
||||
setShowEmailBindModal(false);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
const bindWeChat = async () => {
|
||||
if (inputs.wechat_verification_code === '') return;
|
||||
const res = await API.get(
|
||||
`/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`
|
||||
);
|
||||
const {success, message} = res.data;
|
||||
if (success) {
|
||||
showSuccess('微信账户绑定成功!');
|
||||
setShowWeChatBindModal(false);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ lineHeight: '40px' }}>
|
||||
<Header as='h3'>通用设置</Header>
|
||||
<Message>
|
||||
注意,此处生成的令牌用于系统管理,而非用于请求 OpenAI 相关的服务,请知悉。
|
||||
</Message>
|
||||
<Button as={Link} to={`/user/edit/`}>
|
||||
更新个人信息
|
||||
</Button>
|
||||
<Button onClick={generateAccessToken}>生成系统访问令牌</Button>
|
||||
<Button onClick={getAffLink}>复制邀请链接</Button>
|
||||
<Button onClick={() => {
|
||||
setShowAccountDeleteModal(true);
|
||||
}}>删除个人账户</Button>
|
||||
<Divider />
|
||||
<Header as='h3'>账号绑定</Header>
|
||||
{
|
||||
status.wechat_login && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowWeChatBindModal(true);
|
||||
}}
|
||||
>
|
||||
绑定微信账号
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
<Modal
|
||||
onClose={() => setShowWeChatBindModal(false)}
|
||||
onOpen={() => setShowWeChatBindModal(true)}
|
||||
open={showWeChatBindModal}
|
||||
size={'mini'}
|
||||
>
|
||||
<Modal.Content>
|
||||
<Modal.Description>
|
||||
<Image src={status.wechat_qrcode} fluid />
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<p>
|
||||
微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
|
||||
</p>
|
||||
</div>
|
||||
<Form size='large'>
|
||||
<Form.Input
|
||||
fluid
|
||||
placeholder='验证码'
|
||||
name='wechat_verification_code'
|
||||
value={inputs.wechat_verification_code}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Button color='' fluid size='large' onClick={bindWeChat}>
|
||||
绑定
|
||||
</Button>
|
||||
const openGitHubOAuth = () => {
|
||||
window.open(
|
||||
`https://github.com/login/oauth/authorize?client_id=${status.github_client_id}&scope=user:email`
|
||||
);
|
||||
};
|
||||
|
||||
const sendVerificationCode = async () => {
|
||||
setDisableButton(true);
|
||||
if (inputs.email === '') return;
|
||||
if (turnstileEnabled && turnstileToken === '') {
|
||||
showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
const res = await API.get(
|
||||
`/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`
|
||||
);
|
||||
const {success, message} = res.data;
|
||||
if (success) {
|
||||
showSuccess('验证码发送成功,请检查邮箱!');
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
const bindEmail = async () => {
|
||||
if (inputs.email_verification_code === '') return;
|
||||
setLoading(true);
|
||||
const res = await API.get(
|
||||
`/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`
|
||||
);
|
||||
const {success, message} = res.data;
|
||||
if (success) {
|
||||
showSuccess('邮箱账户绑定成功!');
|
||||
setShowEmailBindModal(false);
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
// const setStableMod = ;
|
||||
|
||||
return (
|
||||
<div style={{lineHeight: '40px'}}>
|
||||
<Header as='h3'>通用设置</Header>
|
||||
<Message>
|
||||
注意,此处生成的令牌用于系统管理,而非用于请求 OpenAI 相关的服务,请知悉。
|
||||
</Message>
|
||||
<Button as={Link} to={`/user/edit/`}>
|
||||
更新个人信息
|
||||
</Button>
|
||||
<Button onClick={generateAccessToken}>生成系统访问令牌</Button>
|
||||
<Button onClick={getAffLink}>复制邀请链接</Button>
|
||||
<Button onClick={() => {
|
||||
setShowAccountDeleteModal(true);
|
||||
}}>删除个人账户</Button>
|
||||
<Divider/>
|
||||
<Header as='h3'>GPT-4消费设置</Header>
|
||||
<Form>
|
||||
<Form.Field>
|
||||
<Checkbox label="启用稳定模式(当低价渠道宕机时,自动选择已开启的渠道,以保证稳定性,仅影响GPT-4)"
|
||||
checked={stableMode.stableMode}
|
||||
onChange={
|
||||
(e, data) => {
|
||||
setStableMode({
|
||||
...stableMode,
|
||||
stableMode: data.checked
|
||||
})
|
||||
}
|
||||
}
|
||||
></Checkbox>
|
||||
</Form.Field>
|
||||
<Form.Field
|
||||
control={Input}
|
||||
label='最高接受价格(n元/刀)'
|
||||
placeholder='7'
|
||||
type={'number'}
|
||||
value={stableMode.maxPrice}
|
||||
onChange={
|
||||
(e, data) => {
|
||||
setStableMode({
|
||||
...stableMode,
|
||||
maxPrice: data.value
|
||||
})
|
||||
}
|
||||
}
|
||||
>
|
||||
{/*<label></label>*/}
|
||||
{/*<input placeholder='7' value= {stableMode.maxPrice}/>*/}
|
||||
</Form.Field>
|
||||
<Button type='submit' onClick={
|
||||
async (e, data) => {
|
||||
if (stableMode.localMaxPrice === '') return;
|
||||
// console.log(data)
|
||||
// post to /api/user/set_stable_mode
|
||||
const res = await API.post(`/api/user/set_stable_mode`, stableMode)
|
||||
const {success, message} = res.data;
|
||||
if (success) {
|
||||
// userDispatch({type: 'stable_mode', payload: stableMode})
|
||||
userState.user.stable_mode = stableMode.stableMode
|
||||
userState.user.max_price = stableMode.maxPrice
|
||||
localStorage.setItem('user', JSON.stringify(userState.user));
|
||||
showSuccess('设置成功!');
|
||||
} else {
|
||||
showError(message);
|
||||
}
|
||||
}
|
||||
}>保存消费设置</Button>
|
||||
</Form>
|
||||
</Modal.Description>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
{
|
||||
status.github_oauth && (
|
||||
<Button onClick={openGitHubOAuth}>绑定 GitHub 账号</Button>
|
||||
)
|
||||
}
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowEmailBindModal(true);
|
||||
}}
|
||||
>
|
||||
绑定邮箱地址
|
||||
</Button>
|
||||
<Modal
|
||||
onClose={() => setShowEmailBindModal(false)}
|
||||
onOpen={() => setShowEmailBindModal(true)}
|
||||
open={showEmailBindModal}
|
||||
size={'tiny'}
|
||||
style={{ maxWidth: '450px' }}
|
||||
>
|
||||
<Modal.Header>绑定邮箱地址</Modal.Header>
|
||||
<Modal.Content>
|
||||
<Modal.Description>
|
||||
<Form size='large'>
|
||||
<Form.Input
|
||||
fluid
|
||||
placeholder='输入邮箱地址'
|
||||
onChange={handleInputChange}
|
||||
name='email'
|
||||
type='email'
|
||||
action={
|
||||
<Button onClick={sendVerificationCode} disabled={disableButton || loading}>
|
||||
{disableButton ? `重新发送(${countdown})` : '获取验证码'}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<Form.Input
|
||||
fluid
|
||||
placeholder='验证码'
|
||||
name='email_verification_code'
|
||||
value={inputs.email_verification_code}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
{turnstileEnabled ? (
|
||||
<Turnstile
|
||||
sitekey={turnstileSiteKey}
|
||||
onVerify={(token) => {
|
||||
setTurnstileToken(token);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<Button
|
||||
color=''
|
||||
fluid
|
||||
size='large'
|
||||
onClick={bindEmail}
|
||||
loading={loading}
|
||||
>
|
||||
绑定
|
||||
</Button>
|
||||
</Form>
|
||||
</Modal.Description>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
<Modal
|
||||
onClose={() => setShowAccountDeleteModal(false)}
|
||||
onOpen={() => setShowAccountDeleteModal(true)}
|
||||
open={showAccountDeleteModal}
|
||||
size={'tiny'}
|
||||
style={{ maxWidth: '450px' }}
|
||||
>
|
||||
<Modal.Header>确认删除自己的帐户</Modal.Header>
|
||||
<Modal.Content>
|
||||
<Modal.Description>
|
||||
<Form size='large'>
|
||||
<Form.Input
|
||||
fluid
|
||||
placeholder={`输入你的账户名 ${userState?.user?.username} 以确认删除`}
|
||||
name='self_account_deletion_confirmation'
|
||||
value={inputs.self_account_deletion_confirmation}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
{turnstileEnabled ? (
|
||||
<Turnstile
|
||||
sitekey={turnstileSiteKey}
|
||||
onVerify={(token) => {
|
||||
setTurnstileToken(token);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<Button
|
||||
color='red'
|
||||
fluid
|
||||
size='large'
|
||||
onClick={deleteAccount}
|
||||
loading={loading}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Form>
|
||||
</Modal.Description>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
{/*<Checkbox label="启用稳定模式(当低价渠道宕机时,自动选择已开启的渠道,以保证稳定性)" onChange={*/}
|
||||
{/* (e, data) => {*/}
|
||||
{/* // if (inputs.email_verification_code === '') return;*/}
|
||||
{/* console.log(data)*/}
|
||||
{/* }*/}
|
||||
{/*}></Checkbox>*/}
|
||||
{/*<Input label="最高接受价格(n元/刀)" type="integer"></Input>*/}
|
||||
<Divider/>
|
||||
<Header as='h3'>账号绑定</Header>
|
||||
{
|
||||
status.wechat_login && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowWeChatBindModal(true);
|
||||
}}
|
||||
>
|
||||
绑定微信账号
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
<Modal
|
||||
onClose={() => setShowWeChatBindModal(false)}
|
||||
onOpen={() => setShowWeChatBindModal(true)}
|
||||
open={showWeChatBindModal}
|
||||
size={'mini'}
|
||||
>
|
||||
<Modal.Content>
|
||||
<Modal.Description>
|
||||
<Image src={status.wechat_qrcode} fluid/>
|
||||
<div style={{textAlign: 'center'}}>
|
||||
<p>
|
||||
微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
|
||||
</p>
|
||||
</div>
|
||||
<Form size='large'>
|
||||
<Form.Input
|
||||
fluid
|
||||
placeholder='验证码'
|
||||
name='wechat_verification_code'
|
||||
value={inputs.wechat_verification_code}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
<Button color='' fluid size='large' onClick={bindWeChat}>
|
||||
绑定
|
||||
</Button>
|
||||
</Form>
|
||||
</Modal.Description>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
{
|
||||
status.github_oauth && (
|
||||
<Button onClick={openGitHubOAuth}>绑定 GitHub 账号</Button>
|
||||
)
|
||||
}
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowEmailBindModal(true);
|
||||
}}
|
||||
>
|
||||
绑定邮箱地址
|
||||
</Button>
|
||||
<Modal
|
||||
onClose={() => setShowEmailBindModal(false)}
|
||||
onOpen={() => setShowEmailBindModal(true)}
|
||||
open={showEmailBindModal}
|
||||
size={'tiny'}
|
||||
style={{maxWidth: '450px'}}
|
||||
>
|
||||
<Modal.Header>绑定邮箱地址</Modal.Header>
|
||||
<Modal.Content>
|
||||
<Modal.Description>
|
||||
<Form size='large'>
|
||||
<Form.Input
|
||||
fluid
|
||||
placeholder='输入邮箱地址'
|
||||
onChange={handleInputChange}
|
||||
name='email'
|
||||
type='email'
|
||||
action={
|
||||
<Button onClick={sendVerificationCode} disabled={disableButton || loading}>
|
||||
{disableButton ? `重新发送(${countdown})` : '获取验证码'}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<Form.Input
|
||||
fluid
|
||||
placeholder='验证码'
|
||||
name='email_verification_code'
|
||||
value={inputs.email_verification_code}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
{turnstileEnabled ? (
|
||||
<Turnstile
|
||||
sitekey={turnstileSiteKey}
|
||||
onVerify={(token) => {
|
||||
setTurnstileToken(token);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<Button
|
||||
color=''
|
||||
fluid
|
||||
size='large'
|
||||
onClick={bindEmail}
|
||||
loading={loading}
|
||||
>
|
||||
绑定
|
||||
</Button>
|
||||
</Form>
|
||||
</Modal.Description>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
<Modal
|
||||
onClose={() => setShowAccountDeleteModal(false)}
|
||||
onOpen={() => setShowAccountDeleteModal(true)}
|
||||
open={showAccountDeleteModal}
|
||||
size={'tiny'}
|
||||
style={{maxWidth: '450px'}}
|
||||
>
|
||||
<Modal.Header>确认删除自己的帐户</Modal.Header>
|
||||
<Modal.Content>
|
||||
<Modal.Description>
|
||||
<Form size='large'>
|
||||
<Form.Input
|
||||
fluid
|
||||
placeholder={`输入你的账户名 ${userState?.user?.username} 以确认删除`}
|
||||
name='self_account_deletion_confirmation'
|
||||
value={inputs.self_account_deletion_confirmation}
|
||||
onChange={handleInputChange}
|
||||
/>
|
||||
{turnstileEnabled ? (
|
||||
<Turnstile
|
||||
sitekey={turnstileSiteKey}
|
||||
onVerify={(token) => {
|
||||
setTurnstileToken(token);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
<Button
|
||||
color='red'
|
||||
fluid
|
||||
size='large'
|
||||
onClick={deleteAccount}
|
||||
loading={loading}
|
||||
>
|
||||
删除
|
||||
</Button>
|
||||
</Form>
|
||||
</Modal.Description>
|
||||
</Modal.Content>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default PersonalSetting;
|
||||
|
||||
@@ -235,21 +235,27 @@ const TokensTable = () => {
|
||||
<Table.Cell>{token.expired_time === -1 ? '永不过期' : renderTimestamp(token.expired_time)}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<div>
|
||||
<Button
|
||||
size={'small'}
|
||||
positive
|
||||
onClick={async () => {
|
||||
let key = "sk-" + token.key;
|
||||
if (await copy(key)) {
|
||||
showSuccess('已复制到剪贴板!');
|
||||
} else {
|
||||
showWarning('无法复制到剪贴板,请手动复制,已将令牌填入搜索框。');
|
||||
setSearchKeyword(key);
|
||||
}
|
||||
}}
|
||||
>
|
||||
复制
|
||||
</Button>
|
||||
<Popup
|
||||
trigger={
|
||||
<Button
|
||||
size={'small'}
|
||||
positive
|
||||
onClick={async () => {
|
||||
let key = "sk-" + token.key;
|
||||
if (await copy(key)) {
|
||||
showSuccess('已复制到剪贴板!');
|
||||
} else {
|
||||
showWarning('无法复制到剪贴板,请手动复制,已将令牌填入搜索框。');
|
||||
setSearchKeyword(key);
|
||||
}
|
||||
}}
|
||||
>
|
||||
复制
|
||||
</Button>
|
||||
}
|
||||
on={'hover'}
|
||||
content={"sk-" + token.key}
|
||||
/>
|
||||
<Popup
|
||||
trigger={
|
||||
<Button size='small' negative>
|
||||
|
||||
Reference in New Issue
Block a user