From 67a65213d8f4806d1660b67440eceada5ea6005f Mon Sep 17 00:00:00 2001 From: "Apple\\Apple" Date: Fri, 23 May 2025 00:24:08 +0800 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8FRefactor:=20Token=20Page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- web/src/components/TokensTable.js | 464 +++++++++++++++---------- web/src/helpers/render.js | 3 +- web/src/i18n/locales/en.json | 24 +- web/src/pages/Token/EditToken.js | 547 +++++++++++++++++------------- web/src/pages/Token/index.js | 15 +- 5 files changed, 630 insertions(+), 423 deletions(-) diff --git a/web/src/components/TokensTable.js b/web/src/components/TokensTable.js index 599b3459..129364d2 100644 --- a/web/src/components/TokensTable.js +++ b/web/src/components/TokensTable.js @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useContext } from 'react'; import { API, copy, @@ -11,9 +11,8 @@ import { ITEMS_PER_PAGE } from '../constants'; import { renderGroup, renderQuota } from '../helpers/render'; import { Button, - Divider, + Card, Dropdown, - Form, Modal, Popconfirm, Popover, @@ -21,11 +20,28 @@ import { SplitButtonGroup, Table, Tag, + Typography, + Input, + Divider, } from '@douyinfe/semi-ui'; -import { IconTreeTriangleDown } from '@douyinfe/semi-icons'; +import { + IconPlus, + IconCopy, + IconSearch, + IconTreeTriangleDown, + IconEyeOpened, + IconEdit, + IconDelete, + IconStop, + IconPlay, + IconMore, +} from '@douyinfe/semi-icons'; import EditToken from '../pages/Token/EditToken'; import { useTranslation } from 'react-i18next'; +import { UserContext } from '../context/User'; + +const { Text } = Typography; function renderTimestamp(timestamp) { return <>{timestamp2string(timestamp)}; @@ -33,44 +49,45 @@ function renderTimestamp(timestamp) { const TokensTable = () => { const { t } = useTranslation(); + const [userState, userDispatch] = useContext(UserContext); const renderStatus = (status, model_limits_enabled = false) => { switch (status) { case 1: if (model_limits_enabled) { return ( - + {t('已启用:限制模型')} ); } else { return ( - + {t('已启用')} ); } case 2: return ( - + {t('已禁用')} ); case 3: return ( - + {t('已过期')} ); case 4: return ( - + {t('已耗尽')} ); default: return ( - + {t('未知状态')} ); @@ -111,11 +128,11 @@ const TokensTable = () => { return (
{record.unlimited_quota ? ( - + {t('无限制')} ) : ( - + {renderQuota(parseInt(text))} )} @@ -151,16 +168,11 @@ const TokensTable = () => { if (shouldUseCustom) { try { - // console.log(chats); chats = JSON.parse(chats); - // check chats is array if (Array.isArray(chats)) { for (let i = 0; i < chats.length; i++) { let chat = {}; chat.node = 'item'; - // c is a map - // chat.key = chats[i].name; - // console.log(chats[i]) for (let key in chats[i]) { if (chats[i].hasOwnProperty(key)) { chat.key = i; @@ -178,33 +190,72 @@ const TokensTable = () => { showError(t('聊天链接配置错误,请联系管理员')); } } + + // 创建更多操作的下拉菜单项 + const moreMenuItems = [ + { + node: 'item', + name: t('查看'), + icon: , + onClick: () => { + Modal.info({ + title: t('令牌详情'), + content: 'sk-' + record.key, + size: 'large', + }); + }, + }, + { + node: 'item', + name: t('删除'), + icon: , + type: 'danger', + onClick: () => { + Modal.confirm({ + title: t('确定是否要删除此令牌?'), + content: t('此修改将不可逆'), + onOk: () => { + manageToken(record.id, 'delete', record).then(() => { + removeRecord(record.key); + }); + }, + }); + }, + } + ]; + + // 动态添加启用/禁用按钮 + if (record.status === 1) { + moreMenuItems.push({ + node: 'item', + name: t('禁用'), + icon: , + type: 'warning', + onClick: () => { + manageToken(record.id, 'disable', record); + }, + }); + } else { + moreMenuItems.push({ + node: 'item', + name: t('启用'), + icon: , + type: 'secondary', + onClick: () => { + manageToken(record.id, 'enable', record); + }, + }); + } + return ( -
- - - - + - { - manageToken(record.id, 'delete', record).then(() => { - removeRecord(record.key); - }); + + - - {record.status === 1 ? ( - - ) : ( - - )} + {t('复制')} + + -
+ + + + +
+ +
+
+ } + placeholder={t('搜索关键字')} + value={searchKeyword} + onChange={handleKeywordChange} + className="!rounded-full" + showClear + /> +
+
+ } + placeholder={t('密钥')} + value={searchToken} + onChange={handleSearchTokenChange} + className="!rounded-full" + showClear + /> +
+ +
+ + + ); + return ( <> { visiable={showEdit} handleClose={closeEdit} > -
- - - - - -
- - -
- - 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} - >
+ + + 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} + className="rounded-xl overflow-hidden" + size="middle" + >
+
); }; diff --git a/web/src/helpers/render.js b/web/src/helpers/render.js index 5a59356b..fa1de023 100644 --- a/web/src/helpers/render.js +++ b/web/src/helpers/render.js @@ -17,7 +17,7 @@ export function renderText(text, limit) { export function renderGroup(group) { if (group === '') { return ( - + {i18next.t('用户分组')} ); @@ -39,6 +39,7 @@ export function renderGroup(group) { size='large' color={tagColors[group] || stringToColor(group)} key={group} + shape='circle' onClick={async (event) => { event.stopPropagation(); if (await copy(group)) { diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index d48df20e..f95ec340 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -448,7 +448,7 @@ "一分钟后过期": "Expires after one minute", "创建新的令牌": "Create New Token", "令牌分组,默认为用户的分组": "Token group, default is the your's group", - "IP白名单(请勿过度信任此功能)": "IP whitelist (do not overly trust this function)", + "IP白名单": "IP whitelist", "注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。": "Note that the quota of the token is only used to limit the maximum quota usage of the token itself, and the actual usage is limited by the remaining quota of the account.", "设为无限额度": "Set to unlimited quota", "更新令牌信息": "Update Token Information", @@ -895,7 +895,7 @@ "渠道分组": "Channel grouping", "安全设置(可选)": "Security settings (optional)", "IP 限制": "IP restrictions", - "启用模型限制(非必要,不建议启用)": "Enable model restrictions (not necessary, not recommended)", + "模型限制": "Model restrictions", "秒": "Second", "更新令牌后需等待几分钟生效": "It will take a few minutes to take effect after updating the token.", "一小时": "One hour", @@ -1410,5 +1410,23 @@ "早上好": "Good morning", "中午好": "Good afternoon", "下午好": "Good afternoon", - "晚上好": "Good evening" + "晚上好": "Good evening", + "更多提示信息": "More Prompts", + "新建": "Create", + "更新": "Update", + "基本信息": "Basic Information", + "设置令牌的基本信息": "Set token basic information", + "设置令牌可用额度和数量": "Set token available quota and quantity", + "访问限制": "Access Restrictions", + "设置令牌的访问限制": "Set token access restrictions", + "请勿过度信任此功能,IP可能被伪造": "Do not over-trust this feature, IP can be spoofed", + "勾选启用模型限制后可选择": "Select after checking to enable model restrictions", + "非必要,不建议启用模型限制": "Not necessary, model restrictions are not recommended", + "分组信息": "Group Information", + "设置令牌的分组": "Set token grouping", + "管理员未设置用户可选分组": "Administrator has not set user-selectable groups", + "10个": "10 items", + "20个": "20 items", + "30个": "30 items", + "100个": "100 items" } \ No newline at end of file diff --git a/web/src/pages/Token/EditToken.js b/web/src/pages/Token/EditToken.js index 497320f0..75f5e651 100644 --- a/web/src/pages/Token/EditToken.js +++ b/web/src/pages/Token/EditToken.js @@ -21,12 +21,26 @@ import { Spin, TextArea, Typography, + Card, + Tag, } from '@douyinfe/semi-ui'; -import Title from '@douyinfe/semi-ui/lib/es/typography/title'; -import { Divider } from 'semantic-ui-react'; +import { + IconClock, + IconCalendar, + IconCreditCard, + IconLink, + IconServer, + IconUserGroup, + IconSave, + IconClose, + IconPlusCircle, +} from '@douyinfe/semi-icons'; import { useTranslation } from 'react-i18next'; +const { Text, Title } = Typography; + const EditToken = (props) => { + const { t } = useTranslation(); const [isEdit, setIsEdit] = useState(false); const [loading, setLoading] = useState(isEdit); const originInputs = { @@ -50,17 +64,18 @@ const EditToken = (props) => { allow_ips, group, } = inputs; - // const [visible, setVisible] = useState(false); const [models, setModels] = useState([]); const [groups, setGroups] = useState([]); const navigate = useNavigate(); - const { t } = useTranslation(); + const handleInputChange = (name, value) => { setInputs((inputs) => ({ ...inputs, [name]: value })); }; + const handleCancel = () => { props.handleClose(); }; + const setExpiredTime = (month, day, hour, minute) => { let now = new Date(); let timestamp = now.getTime() / 1000; @@ -128,6 +143,7 @@ const EditToken = (props) => { } setLoading(false); }; + useEffect(() => { setIsEdit(props.editingToken.id !== undefined); }, [props.editingToken.id]); @@ -241,237 +257,312 @@ const EditToken = (props) => { }; return ( - <> - + + {isEdit ? + {t('更新')} : + {t('新建')} + } + {isEdit ? t('更新令牌信息') : t('创建新的令牌')} - } - headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }} - bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }} - visible={props.visiable} - footer={ -
- - - - -
- } - closeIcon={null} - onCancel={() => handleCancel()} - width={isMobile() ? '100%' : 600} - > - - handleInputChange('name', value)} - value={name} - autoComplete='new-password' - required={!isEdit} - /> - - handleInputChange('expired_time', value)} - value={expired_time} - autoComplete='new-password' - type='dateTime' - /> -
- - - - - - -
- - - -
- {`${t('额度')}${renderQuotaWithPrompt(remain_quota)}`} -
- handleInputChange('remain_quota', value)} - value={remain_quota} - autoComplete='new-password' - type='number' - // position={'top'} - data={[ - { value: 500000, label: '1$' }, - { value: 5000000, label: '10$' }, - { value: 25000000, label: '50$' }, - { value: 50000000, label: '100$' }, - { value: 250000000, label: '500$' }, - { value: 500000000, label: '1000$' }, - ]} - disabled={unlimited_quota} - /> - - {!isEdit && ( - <> -
- {t('新建数量')} -
- handleTokenCountChange(value)} - onSelect={(value) => handleTokenCountChange(value)} - value={tokenCount.toString()} - autoComplete='off' - type='number' - data={[ - { value: 10, label: t('10个') }, - { value: 20, label: t('20个') }, - { value: 30, label: t('30个') }, - { value: 100, label: t('100个') }, - ]} - disabled={unlimited_quota} - /> - - )} - -
+ + } + headerStyle={{ + borderBottom: '1px solid var(--semi-color-border)', + padding: '24px' + }} + bodyStyle={{ + backgroundColor: 'var(--semi-color-bg-0)', + padding: '0' + }} + visible={props.visiable} + width={isMobile() ? '100%' : 600} + footer={ +
+ -
- -
- - {t('IP白名单(请勿过度信任此功能)')} - -
-