Browse Source

♻️Refactor: Token Page

Apple\Apple 7 months ago
parent
commit
67a65213d8

+ 286 - 176
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 (
-            <Tag color='green' size='large'>
+            <Tag color='green' size='large' shape='circle'>
               {t('已启用:限制模型')}
             </Tag>
           );
         } else {
           return (
-            <Tag color='green' size='large'>
+            <Tag color='green' size='large' shape='circle'>
               {t('已启用')}
             </Tag>
           );
         }
       case 2:
         return (
-          <Tag color='red' size='large'>
+          <Tag color='red' size='large' shape='circle'>
             {t('已禁用')}
           </Tag>
         );
       case 3:
         return (
-          <Tag color='yellow' size='large'>
+          <Tag color='yellow' size='large' shape='circle'>
             {t('已过期')}
           </Tag>
         );
       case 4:
         return (
-          <Tag color='grey' size='large'>
+          <Tag color='grey' size='large' shape='circle'>
             {t('已耗尽')}
           </Tag>
         );
       default:
         return (
-          <Tag color='black' size='large'>
+          <Tag color='black' size='large' shape='circle'>
             {t('未知状态')}
           </Tag>
         );
@@ -111,11 +128,11 @@ const TokensTable = () => {
         return (
           <div>
             {record.unlimited_quota ? (
-              <Tag size={'large'} color={'white'}>
+              <Tag size={'large'} color={'white'} shape='circle'>
                 {t('无限制')}
               </Tag>
             ) : (
-              <Tag size={'large'} color={'light-blue'}>
+              <Tag size={'large'} color={'light-blue'} shape='circle'>
                 {renderQuota(parseInt(text))}
               </Tag>
             )}
@@ -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: <IconEyeOpened />,
+            onClick: () => {
+              Modal.info({
+                title: t('令牌详情'),
+                content: 'sk-' + record.key,
+                size: 'large',
+              });
+            },
+          },
+          {
+            node: 'item',
+            name: t('删除'),
+            icon: <IconDelete />,
+            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: <IconStop />,
+            type: 'warning',
+            onClick: () => {
+              manageToken(record.id, 'disable', record);
+            },
+          });
+        } else {
+          moreMenuItems.push({
+            node: 'item',
+            name: t('启用'),
+            icon: <IconPlay />,
+            type: 'secondary',
+            onClick: () => {
+              manageToken(record.id, 'enable', record);
+            },
+          });
+        }
+        
         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>
+          <Space wrap>
             <SplitButtonGroup
-              style={{ marginRight: 1 }}
+              className="!rounded-full overflow-hidden"
               aria-label={t('项目操作按钮组')}
             >
               <Button
                 theme='light'
+                size="small"
                 style={{ color: 'rgba(var(--semi-teal-7), 1)' }}
                 onClick={() => {
                   if (chatsArray.length === 0) {
@@ -227,56 +278,35 @@ const TokensTable = () => {
               >
                 <Button
                   style={{
-                    padding: '8px 4px',
+                    padding: '4px 4px',
                     color: 'rgba(var(--semi-teal-7), 1)',
                   }}
                   type='primary'
                   icon={<IconTreeTriangleDown />}
+                  size="small"
                 ></Button>
               </Dropdown>
             </SplitButtonGroup>
-            <Popconfirm
-              title={t('确定是否要删除此令牌?')}
-              content={t('此修改将不可逆')}
-              okType={'danger'}
-              position={'left'}
-              onConfirm={() => {
-                manageToken(record.id, 'delete', record).then(() => {
-                  removeRecord(record.key);
-                });
+            
+            <Button
+              icon={<IconCopy />}
+              theme='light'
+              type='secondary'
+              size="small"
+              className="!rounded-full"
+              onClick={async (text) => {
+                await copyText('sk-' + 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>
-            )}
+              {t('复制')}
+            </Button>
+            
             <Button
+              icon={<IconEdit />}
               theme='light'
               type='tertiary'
-              style={{ marginRight: 1 }}
+              size="small"
+              className="!rounded-full"
               onClick={() => {
                 setEditingToken(record);
                 setShowEdit(true);
@@ -284,7 +314,21 @@ const TokensTable = () => {
             >
               {t('编辑')}
             </Button>
-          </div>
+            
+            <Dropdown
+              trigger='click'
+              position='bottomRight'
+              menu={moreMenuItems}
+            >
+              <Button
+                icon={<IconMore />}
+                theme='light'
+                type='tertiary'
+                size="small"
+                className="!rounded-full"
+              />
+            </Dropdown>
+          </Space>
         );
       },
     },
@@ -362,7 +406,6 @@ const TokensTable = () => {
   };
 
   const onOpenLink = async (type, url, record) => {
-    // console.log(type, url, key);
     let status = localStorage.getItem('status');
     let serverAddress = '';
     if (status) {
@@ -379,7 +422,26 @@ const TokensTable = () => {
     window.open(url, '_blank');
   };
 
+  // 获取用户数据
+  const getUserData = async () => {
+    try {
+      const res = await API.get(`/api/user/self`);
+      const { success, message, data } = res.data;
+      if (success) {
+        userDispatch({ type: 'login', payload: data });
+      } else {
+        showError(message);
+      }
+    } catch (error) {
+      console.error('获取用户数据失败:', error);
+      showError(t('获取用户数据失败'));
+    }
+  };
+
   useEffect(() => {
+    // 获取用户数据以确保显示正确的余额和使用量
+    getUserData();
+
     loadTokens(0)
       .then()
       .catch((reason) => {
@@ -421,11 +483,9 @@ const TokensTable = () => {
       showSuccess('操作成功完成!');
       let token = res.data.data;
       let newTokens = [...tokens];
-      // let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
       if (action === 'delete') {
       } else {
         record.status = token.status;
-        // newTokens[realIdx].status = token.status;
       }
       setTokensFormat(newTokens);
     } else {
@@ -436,7 +496,6 @@ const TokensTable = () => {
 
   const searchTokens = async () => {
     if (searchKeyword === '' && searchToken === '') {
-      // if keyword is blank, load files instead.
       await loadTokens(0);
       setActivePage(1);
       return;
@@ -480,14 +539,13 @@ const TokensTable = () => {
   const handlePageChange = (page) => {
     setActivePage(page);
     if (page === Math.ceil(tokens.length / pageSize) + 1) {
-      // In this case we have to load more data and then append them.
-      loadTokens(page - 1).then((r) => {});
+      loadTokens(page - 1).then((r) => { });
     }
   };
 
   const rowSelection = {
-    onSelect: (record, selected) => {},
-    onSelectAll: (selected, selectedRows) => {},
+    onSelect: (record, selected) => { },
+    onSelectAll: (selected, selectedRows) => { },
     onChange: (selectedRowKeys, selectedRows) => {
       setSelectedKeys(selectedRows);
     },
@@ -505,6 +563,117 @@ const TokensTable = () => {
     }
   };
 
+  const renderHeader = () => (
+    <div className="flex flex-col w-full">
+      <div className="mb-2">
+        <div className="flex flex-col md:flex-row justify-between items-center">
+          <div className="flex items-center text-orange-500">
+            <IconEyeOpened className="mr-2" />
+            <Text>{t('令牌无法精确控制使用额度,只允许自用,请勿直接将令牌分发给他人。')}</Text>
+          </div>
+
+          <div className="flex flex-wrap gap-4 mt-2 md:mt-0">
+            <div className="flex items-center">
+              <span className="text-xl mr-2">💰</span>
+              <div>
+                <Text type="tertiary" size="small">{t('当前余额')}</Text>
+                <div className="font-medium">{renderQuota(userState?.user?.quota)}</div>
+              </div>
+            </div>
+
+            <div className="flex items-center">
+              <span className="text-xl mr-2">📊</span>
+              <div>
+                <Text type="tertiary" size="small">{t('累计消费')}</Text>
+                <div className="font-medium">{renderQuota(userState?.user?.used_quota)}</div>
+              </div>
+            </div>
+
+            <div className="flex items-center">
+              <span className="text-xl mr-2">🔄</span>
+              <div>
+                <Text type="tertiary" size="small">{t('请求次数')}</Text>
+                <div className="font-medium">{userState?.user?.request_count || 0}</div>
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <Divider margin="12px" />
+
+      <div className="flex flex-col md:flex-row justify-between items-center gap-4 w-full">
+        <div className="flex gap-2 w-full md:w-auto order-2 md:order-1">
+          <Button
+            theme="light"
+            type="primary"
+            icon={<IconPlus />}
+            className="!rounded-full w-full md:w-auto"
+            onClick={() => {
+              setEditingToken({
+                id: undefined,
+              });
+              setShowEdit(true);
+            }}
+          >
+            {t('添加令牌')}
+          </Button>
+          <Button
+            theme="light"
+            type="warning"
+            icon={<IconCopy />}
+            className="!rounded-full w-full md:w-auto"
+            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>
+
+        <div className="flex flex-col md:flex-row items-center gap-4 w-full md:w-auto order-1 md:order-2">
+          <div className="relative w-full md:w-56">
+            <Input
+              prefix={<IconSearch />}
+              placeholder={t('搜索关键字')}
+              value={searchKeyword}
+              onChange={handleKeywordChange}
+              className="!rounded-full"
+              showClear
+            />
+          </div>
+          <div className="relative w-full md:w-56">
+            <Input
+              prefix={<IconSearch />}
+              placeholder={t('密钥')}
+              value={searchToken}
+              onChange={handleSearchTokenChange}
+              className="!rounded-full"
+              showClear
+            />
+          </div>
+          <Button
+            type="primary"
+            onClick={searchTokens}
+            loading={searching}
+            className="!rounded-full w-full md:w-auto"
+          >
+            {t('查询')}
+          </Button>
+        </div>
+      </div>
+    </div>
+  );
+
   return (
     <>
       <EditToken
@@ -513,99 +682,40 @@ const TokensTable = () => {
         visiable={showEdit}
         handleClose={closeEdit}
       ></EditToken>
-      <Form
-        layout='horizontal'
-        style={{ marginTop: 10 }}
-        labelPosition={'left'}
+
+      <Card
+        className="!rounded-2xl overflow-hidden"
+        title={renderHeader()}
+        shadows='hover'
       >
-        <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);
+        <Table
+          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,
           }}
-        >
-          {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>
+          loading={loading}
+          rowSelection={rowSelection}
+          onRow={handleRow}
+          className="rounded-xl overflow-hidden"
+          size="middle"
+        ></Table>
+      </Card>
     </>
   );
 };

+ 2 - 1
web/src/helpers/render.js

@@ -17,7 +17,7 @@ export function renderText(text, limit) {
 export function renderGroup(group) {
   if (group === '') {
     return (
-      <Tag size='large' key='default' color='orange'>
+      <Tag size='large' key='default' color='orange' shape='circle'>
         {i18next.t('用户分组')}
       </Tag>
     );
@@ -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)) {

+ 21 - 3
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"
 }

+ 320 - 229
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 (
-    <>
-      <SideSheet
-        placement={isEdit ? 'right' : 'left'}
-        title={
-          <Title level={3}>
+    <SideSheet
+      placement={isEdit ? 'right' : 'left'}
+      title={
+        <Space>
+          {isEdit ?
+            <Tag color="blue" shape="circle">{t('更新')}</Tag> :
+            <Tag color="green" shape="circle">{t('新建')}</Tag>
+          }
+          <Title heading={4} className="m-0">
             {isEdit ? t('更新令牌信息') : t('创建新的令牌')}
           </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}>
-                {t('提交')}
-              </Button>
-              <Button
-                theme='solid'
-                size={'large'}
-                type={'tertiary'}
-                onClick={handleCancel}
-              >
-                {t('取消')}
-              </Button>
-            </Space>
-          </div>
-        }
-        closeIcon={null}
-        onCancel={() => handleCancel()}
-        width={isMobile() ? '100%' : 600}
-      >
-        <Spin spinning={loading}>
-          <Input
-            style={{ marginTop: 20 }}
-            label={t('名称')}
-            name='name'
-            placeholder={t('请输入名称')}
-            onChange={(value) => handleInputChange('name', value)}
-            value={name}
-            autoComplete='new-password'
-            required={!isEdit}
-          />
-          <Divider />
-          <DatePicker
-            label={t('过期时间')}
-            name='expired_time'
-            placeholder={t('请选择过期时间')}
-            onChange={(value) => handleInputChange('expired_time', value)}
-            value={expired_time}
-            autoComplete='new-password'
-            type='dateTime'
-          />
-          <div style={{ marginTop: 20 }}>
-            <Space>
-              <Button
-                type={'tertiary'}
-                onClick={() => {
-                  setExpiredTime(0, 0, 0, 0);
-                }}
-              >
-                {t('永不过期')}
-              </Button>
-              <Button
-                type={'tertiary'}
-                onClick={() => {
-                  setExpiredTime(0, 0, 1, 0);
-                }}
-              >
-                {t('一小时')}
-              </Button>
-              <Button
-                type={'tertiary'}
-                onClick={() => {
-                  setExpiredTime(1, 0, 0, 0);
-                }}
-              >
-                {t('一个月')}
-              </Button>
-              <Button
-                type={'tertiary'}
-                onClick={() => {
-                  setExpiredTime(0, 1, 0, 0);
-                }}
-              >
-                {t('一天')}
-              </Button>
-            </Space>
-          </div>
-
-          <Divider />
-          <Banner
-            type={'warning'}
-            description={t(
-              '注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。',
-            )}
-          ></Banner>
-          <div style={{ marginTop: 20 }}>
-            <Typography.Text>{`${t('额度')}${renderQuotaWithPrompt(remain_quota)}`}</Typography.Text>
-          </div>
-          <AutoComplete
-            style={{ marginTop: 8 }}
-            name='remain_quota'
-            placeholder={t('请输入额度')}
-            onChange={(value) => 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 && (
-            <>
-              <div style={{ marginTop: 20 }}>
-                <Typography.Text>{t('新建数量')}</Typography.Text>
-              </div>
-              <AutoComplete
-                style={{ marginTop: 8 }}
-                label={t('数量')}
-                placeholder={t('请选择或输入创建令牌的数量')}
-                onChange={(value) => 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}
-              />
-            </>
-          )}
-
-          <div>
+        </Space>
+      }
+      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={
+        <div className="flex justify-end bg-white">
+          <Space>
             <Button
-              style={{ marginTop: 8 }}
-              type={'warning'}
-              onClick={() => {
-                setUnlimitedQuota();
-              }}
+              theme="solid"
+              size="large"
+              className="!rounded-full"
+              onClick={submit}
+              icon={<IconSave />}
+              loading={loading}
             >
-              {unlimited_quota ? t('取消无限额度') : t('设为无限额度')}
+              {t('提交')}
             </Button>
-          </div>
-          <Divider />
-          <div style={{ marginTop: 10 }}>
-            <Typography.Text>
-              {t('IP白名单(请勿过度信任此功能)')}
-            </Typography.Text>
-          </div>
-          <TextArea
-            label={t('IP白名单')}
-            name='allow_ips'
-            placeholder={t('允许的IP,一行一个,不填写则不限制')}
-            onChange={(value) => {
-              handleInputChange('allow_ips', value);
-            }}
-            value={inputs.allow_ips}
-            style={{ fontFamily: 'JetBrains Mono, Consolas' }}
-          />
-          <div style={{ marginTop: 10, display: 'flex' }}>
-            <Space>
-              <Checkbox
-                name='model_limits_enabled'
-                checked={model_limits_enabled}
-                onChange={(e) =>
-                  handleInputChange('model_limits_enabled', e.target.checked)
-                }
-              >
-                {t('启用模型限制(非必要,不建议启用)')}
-              </Checkbox>
-            </Space>
-          </div>
-
-          <Select
-            style={{ marginTop: 8 }}
-            placeholder={t('请选择该渠道所支持的模型')}
-            name='models'
-            required
-            multiple
-            selection
-            onChange={(value) => {
-              handleInputChange('model_limits', value);
-            }}
-            value={inputs.model_limits}
-            autoComplete='new-password'
-            optionList={models}
-            disabled={!model_limits_enabled}
-          />
-          <div style={{ marginTop: 10 }}>
-            <Typography.Text>{t('令牌分组,默认为用户的分组')}</Typography.Text>
-          </div>
-          {groups.length > 0 ? (
-            <Select
-              style={{ marginTop: 8 }}
-              placeholder={t('令牌分组,默认为用户的分组')}
-              name='gruop'
-              required
-              selection
-              onChange={(value) => {
-                handleInputChange('group', value);
-              }}
-              position={'topLeft'}
-              renderOptionItem={renderGroupOption}
-              value={inputs.group}
-              autoComplete='new-password'
-              optionList={groups}
-            />
-          ) : (
-            <Select
-              style={{ marginTop: 8 }}
-              placeholder={t('管理员未设置用户可选分组')}
-              name='gruop'
-              disabled={true}
+            <Button
+              theme="light"
+              size="large"
+              className="!rounded-full"
+              type="primary"
+              onClick={handleCancel}
+              icon={<IconClose />}
+            >
+              {t('取消')}
+            </Button>
+          </Space>
+        </div>
+      }
+      closeIcon={null}
+      onCancel={() => handleCancel()}
+    >
+      <Spin spinning={loading}>
+        <div className="p-6">
+          <Card className="!rounded-2xl shadow-sm border-0 mb-6">
+            <div className="flex items-center mb-4">
+              <div className="w-10 h-10 rounded-full bg-blue-50 flex items-center justify-center mr-4">
+                <IconPlusCircle size="large" className="text-blue-500" />
+              </div>
+              <div>
+                <Text className="text-lg font-medium">{t('基本信息')}</Text>
+                <div className="text-gray-500 text-sm">{t('设置令牌的基本信息')}</div>
+              </div>
+            </div>
+
+            <div className="space-y-4">
+              <div>
+                <Text strong className="block mb-2">{t('名称')}</Text>
+                <Input
+                  placeholder={t('请输入名称')}
+                  onChange={(value) => handleInputChange('name', value)}
+                  value={name}
+                  autoComplete="new-password"
+                  size="large"
+                  className="!rounded-lg"
+                  showClear
+                  required
+                />
+              </div>
+
+              <div>
+                <Text strong className="block mb-2">{t('过期时间')}</Text>
+                <div className="mb-2">
+                  <DatePicker
+                    placeholder={t('请选择过期时间')}
+                    onChange={(value) => handleInputChange('expired_time', value)}
+                    value={expired_time}
+                    autoComplete="new-password"
+                    type="dateTime"
+                    className="w-full !rounded-lg"
+                    size="large"
+                    prefix={<IconCalendar />}
+                  />
+                </div>
+
+                <div className="flex flex-wrap gap-2">
+                  <Button
+                    theme="light"
+                    type="primary"
+                    onClick={() => setExpiredTime(0, 0, 0, 0)}
+                    className="!rounded-full"
+                  >
+                    {t('永不过期')}
+                  </Button>
+                  <Button
+                    theme="light"
+                    type="tertiary"
+                    onClick={() => setExpiredTime(0, 0, 1, 0)}
+                    className="!rounded-full"
+                    icon={<IconClock />}
+                  >
+                    {t('一小时')}
+                  </Button>
+                  <Button
+                    theme="light"
+                    type="tertiary"
+                    onClick={() => setExpiredTime(0, 1, 0, 0)}
+                    className="!rounded-full"
+                    icon={<IconCalendar />}
+                  >
+                    {t('一天')}
+                  </Button>
+                  <Button
+                    theme="light"
+                    type="tertiary"
+                    onClick={() => setExpiredTime(1, 0, 0, 0)}
+                    className="!rounded-full"
+                    icon={<IconCalendar />}
+                  >
+                    {t('一个月')}
+                  </Button>
+                </div>
+              </div>
+            </div>
+          </Card>
+
+          <Card className="!rounded-2xl shadow-sm border-0 mb-6">
+            <div className="flex items-center mb-4">
+              <div className="w-10 h-10 rounded-full bg-green-50 flex items-center justify-center mr-4">
+                <IconCreditCard size="large" className="text-green-500" />
+              </div>
+              <div>
+                <Text className="text-lg font-medium">{t('额度设置')}</Text>
+                <div className="text-gray-500 text-sm">{t('设置令牌可用额度和数量')}</div>
+              </div>
+            </div>
+
+            <Banner
+              type="warning"
+              description={t('注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。')}
+              className="mb-4 !rounded-lg"
             />
-          )}
-        </Spin>
-      </SideSheet>
-    </>
+
+            <div className="space-y-4">
+              <div>
+                <div className="flex justify-between mb-2">
+                  <Text strong>{t('额度')}</Text>
+                  <Text type="tertiary">{renderQuotaWithPrompt(remain_quota)}</Text>
+                </div>
+                <AutoComplete
+                  placeholder={t('请输入额度')}
+                  onChange={(value) => handleInputChange('remain_quota', value)}
+                  value={remain_quota}
+                  autoComplete="new-password"
+                  type="number"
+                  size="large"
+                  className="w-full !rounded-lg"
+                  prefix={<IconCreditCard />}
+                  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}
+                />
+              </div>
+
+              {!isEdit && (
+                <div>
+                  <Text strong className="block mb-2">{t('新建数量')}</Text>
+                  <AutoComplete
+                    placeholder={t('请选择或输入创建令牌的数量')}
+                    onChange={(value) => handleTokenCountChange(value)}
+                    onSelect={(value) => handleTokenCountChange(value)}
+                    value={tokenCount.toString()}
+                    autoComplete="off"
+                    type="number"
+                    className="w-full !rounded-lg"
+                    size="large"
+                    prefix={<IconPlusCircle />}
+                    data={[
+                      { value: 10, label: t('10个') },
+                      { value: 20, label: t('20个') },
+                      { value: 30, label: t('30个') },
+                      { value: 100, label: t('100个') },
+                    ]}
+                    disabled={unlimited_quota}
+                  />
+                </div>
+              )}
+
+              <div className="flex justify-end">
+                <Button
+                  theme="light"
+                  type={unlimited_quota ? "danger" : "warning"}
+                  onClick={setUnlimitedQuota}
+                  className="!rounded-full"
+                >
+                  {unlimited_quota ? t('取消无限额度') : t('设为无限额度')}
+                </Button>
+              </div>
+            </div>
+          </Card>
+
+          <Card className="!rounded-2xl shadow-sm border-0 mb-6">
+            <div className="flex items-center mb-4">
+              <div className="w-10 h-10 rounded-full bg-purple-50 flex items-center justify-center mr-4">
+                <IconLink size="large" className="text-purple-500" />
+              </div>
+              <div>
+                <Text className="text-lg font-medium">{t('访问限制')}</Text>
+                <div className="text-gray-500 text-sm">{t('设置令牌的访问限制')}</div>
+              </div>
+            </div>
+
+            <div className="space-y-4">
+              <div>
+                <Text strong className="block mb-2">{t('IP白名单')}</Text>
+                <TextArea
+                  placeholder={t('允许的IP,一行一个,不填写则不限制')}
+                  onChange={(value) => handleInputChange('allow_ips', value)}
+                  value={inputs.allow_ips}
+                  style={{ fontFamily: 'JetBrains Mono, Consolas' }}
+                  className="!rounded-lg"
+                  rows={4}
+                />
+                <Text type="tertiary" className="mt-1 block text-xs">{t('请勿过度信任此功能,IP可能被伪造')}</Text>
+              </div>
+
+              <div>
+                <div className="flex items-center mb-2">
+                  <Checkbox
+                    checked={model_limits_enabled}
+                    onChange={(e) => handleInputChange('model_limits_enabled', e.target.checked)}
+                  >
+                    <Text strong>{t('模型限制')}</Text>
+                  </Checkbox>
+                </div>
+                <Select
+                  placeholder={model_limits_enabled ? t('请选择该渠道所支持的模型') : t('勾选启用模型限制后可选择')}
+                  onChange={(value) => handleInputChange('model_limits', value)}
+                  value={inputs.model_limits}
+                  multiple
+                  size="large"
+                  className="w-full !rounded-lg"
+                  prefix={<IconServer />}
+                  optionList={models}
+                  disabled={!model_limits_enabled}
+                  maxTagCount={3}
+                />
+                <Text type="tertiary" className="mt-1 block text-xs">{t('非必要,不建议启用模型限制')}</Text>
+              </div>
+            </div>
+          </Card>
+
+          <Card className="!rounded-2xl shadow-sm border-0">
+            <div className="flex items-center mb-4">
+              <div className="w-10 h-10 rounded-full bg-orange-50 flex items-center justify-center mr-4">
+                <IconUserGroup size="large" className="text-orange-500" />
+              </div>
+              <div>
+                <Text className="text-lg font-medium">{t('分组信息')}</Text>
+                <div className="text-gray-500 text-sm">{t('设置令牌的分组')}</div>
+              </div>
+            </div>
+
+            <div>
+              <Text strong className="block mb-2">{t('令牌分组')}</Text>
+              {groups.length > 0 ? (
+                <Select
+                  placeholder={t('令牌分组,默认为用户的分组')}
+                  onChange={(value) => handleInputChange('group', value)}
+                  renderOptionItem={renderGroupOption}
+                  value={inputs.group}
+                  size="large"
+                  className="w-full !rounded-lg"
+                  prefix={<IconUserGroup />}
+                  optionList={groups}
+                />
+              ) : (
+                <Select
+                  placeholder={t('管理员未设置用户可选分组')}
+                  disabled={true}
+                  size="large"
+                  className="w-full !rounded-lg"
+                  prefix={<IconUserGroup />}
+                />
+              )}
+            </div>
+          </Card>
+        </div>
+      </Spin>
+    </SideSheet>
   );
 };
 

+ 1 - 14
web/src/pages/Token/index.js

@@ -1,24 +1,11 @@
 import React from 'react';
 import TokensTable from '../../components/TokensTable';
-import { Banner, Layout } from '@douyinfe/semi-ui';
 import { useTranslation } from 'react-i18next';
 const Token = () => {
   const { t } = useTranslation();
   return (
     <>
-      <Layout>
-        <Layout.Header>
-          <Banner
-            type='warning'
-            description={t(
-              '令牌无法精确控制使用额度,只允许自用,请勿直接将令牌分发给他人。',
-            )}
-          />
-        </Layout.Header>
-        <Layout.Content>
-          <TokensTable />
-        </Layout.Content>
-      </Layout>
+      <TokensTable />
     </>
   );
 };