| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| import React, { useEffect, useState } from 'react'; |
| import { |
| API, |
| copy, |
| showError, |
| showSuccess, |
| timestamp2string, |
| } from '../helpers'; |
|
|
| import { ITEMS_PER_PAGE } from '../constants'; |
| import { renderGroup, renderQuota } from '../helpers/render'; |
| import { |
| Button, |
| Divider, |
| Dropdown, |
| Form, |
| Modal, |
| Popconfirm, |
| Popover, |
| Space, |
| SplitButtonGroup, |
| Table, |
| Tag, |
| } from '@douyinfe/semi-ui'; |
|
|
| import { IconTreeTriangleDown } from '@douyinfe/semi-icons'; |
| import EditToken from '../pages/Token/EditToken'; |
| import { useTranslation } from 'react-i18next'; |
|
|
| function renderTimestamp(timestamp) { |
| return <>{timestamp2string(timestamp)}</>; |
| } |
|
|
| const TokensTable = () => { |
| const { t } = useTranslation(); |
|
|
| const renderStatus = (status, model_limits_enabled = false) => { |
| switch (status) { |
| case 1: |
| if (model_limits_enabled) { |
| return ( |
| <Tag color='green' size='large'> |
| {t('已启用:限制模型')} |
| </Tag> |
| ); |
| } else { |
| return ( |
| <Tag color='green' size='large'> |
| {t('已启用')} |
| </Tag> |
| ); |
| } |
| case 2: |
| return ( |
| <Tag color='red' size='large'> |
| {t('已禁用')} |
| </Tag> |
| ); |
| case 3: |
| return ( |
| <Tag color='yellow' size='large'> |
| {t('已过期')} |
| </Tag> |
| ); |
| case 4: |
| return ( |
| <Tag color='grey' size='large'> |
| {t('已耗尽')} |
| </Tag> |
| ); |
| default: |
| return ( |
| <Tag color='black' size='large'> |
| {t('未知状态')} |
| </Tag> |
| ); |
| } |
| }; |
|
|
| const columns = [ |
| { |
| title: t('名称'), |
| dataIndex: 'name', |
| }, |
| { |
| title: t('状态'), |
| dataIndex: 'status', |
| key: 'status', |
| render: (text, record, index) => { |
| return ( |
| <div> |
| <Space> |
| {renderStatus(text, record.model_limits_enabled)} |
| {renderGroup(record.group)} |
| </Space> |
| </div> |
| ); |
| }, |
| }, |
| { |
| title: t('已用额度'), |
| dataIndex: 'used_quota', |
| render: (text, record, index) => { |
| return <div>{renderQuota(parseInt(text))}</div>; |
| }, |
| }, |
| { |
| title: t('剩余额度'), |
| dataIndex: 'remain_quota', |
| render: (text, record, index) => { |
| return ( |
| <div> |
| {record.unlimited_quota ? ( |
| <Tag size={'large'} color={'white'}> |
| {t('无限制')} |
| </Tag> |
| ) : ( |
| <Tag size={'large'} color={'light-blue'}> |
| {renderQuota(parseInt(text))} |
| </Tag> |
| )} |
| </div> |
| ); |
| }, |
| }, |
| { |
| title: t('创建时间'), |
| dataIndex: 'created_time', |
| render: (text, record, index) => { |
| return <div>{renderTimestamp(text)}</div>; |
| }, |
| }, |
| { |
| title: t('过期时间'), |
| dataIndex: 'expired_time', |
| render: (text, record, index) => { |
| return ( |
| <div> |
| {record.expired_time === -1 ? t('永不过期') : renderTimestamp(text)} |
| </div> |
| ); |
| }, |
| }, |
| { |
| title: '', |
| dataIndex: 'operate', |
| render: (text, record, index) => { |
| let chats = localStorage.getItem('chats'); |
| let chatsArray = []; |
| let shouldUseCustom = true; |
|
|
| if (shouldUseCustom) { |
| try { |
| |
| chats = JSON.parse(chats); |
| |
| if (Array.isArray(chats)) { |
| for (let i = 0; i < chats.length; i++) { |
| let chat = {}; |
| chat.node = 'item'; |
| |
| |
| |
| for (let key in chats[i]) { |
| if (chats[i].hasOwnProperty(key)) { |
| chat.key = i; |
| chat.name = key; |
| chat.onClick = () => { |
| onOpenLink(key, chats[i][key], record); |
| }; |
| } |
| } |
| chatsArray.push(chat); |
| } |
| } |
| } catch (e) { |
| console.log(e); |
| showError(t('聊天链接配置错误,请联系管理员')); |
| } |
| } |
| return ( |
| <div> |
| <Popover |
| content={'sk-' + record.key} |
| style={{ padding: 20 }} |
| position='top' |
| > |
| <Button theme='light' type='tertiary' style={{ marginRight: 1 }}> |
| {t('查看')} |
| </Button> |
| </Popover> |
| <Button |
| theme='light' |
| type='secondary' |
| style={{ marginRight: 1 }} |
| onClick={async (text) => { |
| await copyText('sk-' + record.key); |
| }} |
| > |
| {t('复制')} |
| </Button> |
| {chatsArray.length > 0 && ( |
| <SplitButtonGroup |
| style={{ marginRight: 1 }} |
| aria-label={t('项目操作按钮组')} |
| > |
| <Button |
| theme='light' |
| style={{ color: 'rgba(var(--semi-teal-7), 1)' }} |
| onClick={() => { |
| onOpenLink( |
| 'default', |
| chats[0][Object.keys(chats[0])[0]], |
| record, |
| ); |
| }} |
| > |
| {t('聊天')} |
| </Button> |
| <Dropdown |
| trigger='click' |
| position='bottomRight' |
| menu={chatsArray} |
| > |
| <Button |
| style={{ |
| padding: '8px 4px', |
| color: 'rgba(var(--semi-teal-7), 1)', |
| }} |
| type='primary' |
| icon={<IconTreeTriangleDown />} |
| ></Button> |
| </Dropdown> |
| </SplitButtonGroup> |
| )} |
| <Popconfirm |
| title={t('确定是否要删除此令牌?')} |
| content={t('此修改将不可逆')} |
| okType={'danger'} |
| position={'left'} |
| onConfirm={() => { |
| manageToken(record.id, 'delete', record).then(() => { |
| removeRecord(record.key); |
| }); |
| }} |
| > |
| <Button theme='light' type='danger' style={{ marginRight: 1 }}> |
| {t('删除')} |
| </Button> |
| </Popconfirm> |
| {record.status === 1 ? ( |
| <Button |
| theme='light' |
| type='warning' |
| style={{ marginRight: 1 }} |
| onClick={async () => { |
| manageToken(record.id, 'disable', record); |
| }} |
| > |
| {t('禁用')} |
| </Button> |
| ) : ( |
| <Button |
| theme='light' |
| type='secondary' |
| style={{ marginRight: 1 }} |
| onClick={async () => { |
| manageToken(record.id, 'enable', record); |
| }} |
| > |
| {t('启用')} |
| </Button> |
| )} |
| <Button |
| theme='light' |
| type='tertiary' |
| style={{ marginRight: 1 }} |
| onClick={() => { |
| setEditingToken(record); |
| setShowEdit(true); |
| }} |
| > |
| {t('编辑')} |
| </Button> |
| </div> |
| ); |
| }, |
| }, |
| ]; |
|
|
| const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE); |
| const [showEdit, setShowEdit] = useState(false); |
| const [tokens, setTokens] = useState([]); |
| const [selectedKeys, setSelectedKeys] = useState([]); |
| const [tokenCount, setTokenCount] = useState(pageSize); |
| const [loading, setLoading] = useState(true); |
| const [activePage, setActivePage] = useState(1); |
| const [searchKeyword, setSearchKeyword] = useState(''); |
| const [searchToken, setSearchToken] = useState(''); |
| const [searching, setSearching] = useState(false); |
| const [chats, setChats] = useState([]); |
| const [editingToken, setEditingToken] = useState({ |
| id: undefined, |
| }); |
|
|
| const closeEdit = () => { |
| setShowEdit(false); |
| setTimeout(() => { |
| setEditingToken({ |
| id: undefined, |
| }); |
| }, 500); |
| }; |
|
|
| const setTokensFormat = (tokens) => { |
| setTokens(tokens); |
| if (tokens.length >= pageSize) { |
| setTokenCount(tokens.length + pageSize); |
| } else { |
| setTokenCount(tokens.length); |
| } |
| }; |
|
|
| let pageData = tokens.slice( |
| (activePage - 1) * pageSize, |
| activePage * pageSize, |
| ); |
| const loadTokens = async (startIdx) => { |
| setLoading(true); |
| const res = await API.get(`/api/token/?p=${startIdx}&size=${pageSize}`); |
| const { success, message, data } = res.data; |
| if (success) { |
| if (startIdx === 0) { |
| setTokensFormat(data); |
| } else { |
| let newTokens = [...tokens]; |
| newTokens.splice(startIdx * pageSize, data.length, ...data); |
| setTokensFormat(newTokens); |
| } |
| } else { |
| showError(message); |
| } |
| setLoading(false); |
| }; |
|
|
| const refresh = async () => { |
| await loadTokens(activePage - 1); |
| }; |
|
|
| const copyText = async (text) => { |
| if (await copy(text)) { |
| showSuccess(t('已复制到剪贴板!')); |
| } else { |
| Modal.error({ |
| title: t('无法复制到剪贴板,请手动复制'), |
| content: text, |
| size: 'large', |
| }); |
| } |
| }; |
|
|
| const onOpenLink = async (type, url, record) => { |
| |
| let status = localStorage.getItem('status'); |
| let serverAddress = ''; |
| if (status) { |
| status = JSON.parse(status); |
| serverAddress = status.server_address; |
| } |
| if (serverAddress === '') { |
| serverAddress = window.location.origin; |
| } |
| let encodedServerAddress = encodeURIComponent(serverAddress); |
| url = url.replaceAll('{address}', encodedServerAddress); |
| url = url.replaceAll('{key}', 'sk-' + record.key); |
|
|
| window.open(url, '_blank'); |
| }; |
|
|
| useEffect(() => { |
| loadTokens(0) |
| .then() |
| .catch((reason) => { |
| showError(reason); |
| }); |
| }, [pageSize]); |
|
|
| const removeRecord = (key) => { |
| let newDataSource = [...tokens]; |
| if (key != null) { |
| let idx = newDataSource.findIndex((data) => data.key === key); |
|
|
| if (idx > -1) { |
| newDataSource.splice(idx, 1); |
| setTokensFormat(newDataSource); |
| } |
| } |
| }; |
|
|
| const manageToken = async (id, action, record) => { |
| setLoading(true); |
| let data = { id }; |
| let res; |
| switch (action) { |
| case 'delete': |
| res = await API.delete(`/api/token/${id}/`); |
| break; |
| case 'enable': |
| data.status = 1; |
| res = await API.put('/api/token/?status_only=true', data); |
| break; |
| case 'disable': |
| data.status = 2; |
| res = await API.put('/api/token/?status_only=true', data); |
| break; |
| } |
| const { success, message } = res.data; |
| if (success) { |
| showSuccess('操作成功完成!'); |
| let token = res.data.data; |
| let newTokens = [...tokens]; |
| |
| if (action === 'delete') { |
| } else { |
| record.status = token.status; |
| |
| } |
| setTokensFormat(newTokens); |
| } else { |
| showError(message); |
| } |
| setLoading(false); |
| }; |
|
|
| const searchTokens = async () => { |
| if (searchKeyword === '' && searchToken === '') { |
| |
| await loadTokens(0); |
| setActivePage(1); |
| return; |
| } |
| setSearching(true); |
| const res = await API.get( |
| `/api/token/search?keyword=${searchKeyword}&token=${searchToken}`, |
| ); |
| const { success, message, data } = res.data; |
| if (success) { |
| setTokensFormat(data); |
| setActivePage(1); |
| } else { |
| showError(message); |
| } |
| setSearching(false); |
| }; |
|
|
| const handleKeywordChange = async (value) => { |
| setSearchKeyword(value.trim()); |
| }; |
|
|
| const handleSearchTokenChange = async (value) => { |
| setSearchToken(value.trim()); |
| }; |
|
|
| const sortToken = (key) => { |
| if (tokens.length === 0) return; |
| setLoading(true); |
| let sortedTokens = [...tokens]; |
| sortedTokens.sort((a, b) => { |
| return ('' + a[key]).localeCompare(b[key]); |
| }); |
| if (sortedTokens[0].id === tokens[0].id) { |
| sortedTokens.reverse(); |
| } |
| setTokens(sortedTokens); |
| setLoading(false); |
| }; |
|
|
| const handlePageChange = (page) => { |
| setActivePage(page); |
| if (page === Math.ceil(tokens.length / pageSize) + 1) { |
| |
| loadTokens(page - 1).then((r) => {}); |
| } |
| }; |
|
|
| const rowSelection = { |
| onSelect: (record, selected) => {}, |
| onSelectAll: (selected, selectedRows) => {}, |
| onChange: (selectedRowKeys, selectedRows) => { |
| setSelectedKeys(selectedRows); |
| }, |
| }; |
|
|
| const handleRow = (record, index) => { |
| if (record.status !== 1) { |
| return { |
| style: { |
| background: 'var(--semi-color-disabled-border)', |
| }, |
| }; |
| } else { |
| return {}; |
| } |
| }; |
|
|
| return ( |
| <> |
| <EditToken |
| refresh={refresh} |
| editingToken={editingToken} |
| visiable={showEdit} |
| handleClose={closeEdit} |
| ></EditToken> |
| <Form |
| layout='horizontal' |
| style={{ marginTop: 10 }} |
| labelPosition={'left'} |
| > |
| <Form.Input |
| field='keyword' |
| label={t('搜索关键字')} |
| placeholder={t('令牌名称')} |
| value={searchKeyword} |
| loading={searching} |
| onChange={handleKeywordChange} |
| /> |
| <Form.Input |
| field='token' |
| label={t('密钥')} |
| placeholder={t('密钥')} |
| value={searchToken} |
| loading={searching} |
| onChange={handleSearchTokenChange} |
| /> |
| <Button |
| label={t('查询')} |
| type='primary' |
| htmlType='submit' |
| className='btn-margin-right' |
| onClick={searchTokens} |
| style={{ marginRight: 8 }} |
| > |
| {t('查询')} |
| </Button> |
| </Form> |
| <Divider style={{ margin: '15px 0' }} /> |
| <div> |
| <Button |
| theme='light' |
| type='primary' |
| style={{ marginRight: 8 }} |
| onClick={() => { |
| setEditingToken({ |
| id: undefined, |
| }); |
| setShowEdit(true); |
| }} |
| > |
| {t('添加令牌')} |
| </Button> |
| <Button |
| label={t('复制所选令牌')} |
| type='warning' |
| onClick={async () => { |
| if (selectedKeys.length === 0) { |
| showError(t('请至少选择一个令牌!')); |
| return; |
| } |
| let keys = ''; |
| for (let i = 0; i < selectedKeys.length; i++) { |
| keys += |
| selectedKeys[i].name + ' sk-' + selectedKeys[i].key + '\n'; |
| } |
| await copyText(keys); |
| }} |
| > |
| {t('复制所选令牌到剪贴板')} |
| </Button> |
| </div> |
| |
| <Table |
| style={{ marginTop: 20 }} |
| columns={columns} |
| dataSource={pageData} |
| pagination={{ |
| currentPage: activePage, |
| pageSize: pageSize, |
| total: tokenCount, |
| showSizeChanger: true, |
| pageSizeOptions: [10, 20, 50, 100], |
| formatPageText: (page) => |
| t('第 {{start}} - {{end}} 条,共 {{total}} 条', { |
| start: page.currentStart, |
| end: page.currentEnd, |
| total: tokens.length, |
| }), |
| onPageSizeChange: (size) => { |
| setPageSize(size); |
| setActivePage(1); |
| }, |
| onPageChange: handlePageChange, |
| }} |
| loading={loading} |
| rowSelection={rowSelection} |
| onRow={handleRow} |
| ></Table> |
| </> |
| ); |
| }; |
|
|
| export default TokensTable; |
|
|