Преглед изворни кода

✨ feat: major refactor and enhancement of Detail dashboard component & add api url display

- **Code Organization & Architecture:**
  - Restructured component with clear sections (Hooks, Constants, Helper Functions, etc.)
  - Added comprehensive code organization comments for better maintainability
  - Extracted reusable helper functions and constants for better separation of concerns

- **Performance Optimizations:**
  - Implemented extensive use of useCallback and useMemo hooks for expensive operations
  - Optimized data processing pipeline with dedicated processing functions
  - Memoized chart configurations, performance metrics, and grouped stats data
  - Cached helper functions like getTrendSpec, handleCopyUrl, and modal handlers

- **UI/UX Enhancements:**
  - Added Empty state component with construction illustrations for better UX
  - Implemented responsive grid layout with conditional API info section visibility
  - Enhanced button styling with consistent rounded design and hover effects
  - Added mini trend charts to statistics cards for visual data representation
  - Improved form field consistency with reusable createFormField helper

- **Feature Improvements:**
  - Added self-use mode detection to conditionally hide/show API information section
  - Enhanced chart configurations with centralized CHART_CONFIG constant
  - Improved time handling with dedicated helper functions (getTimeInterval, getInitialTimestamp)
  - Added comprehensive performance metrics calculation (RPM/TPM trends)
  - Implemented advanced data aggregation and processing workflows

- **Code Quality & Maintainability:**
  - Extracted complex data processing logic into dedicated functions
  - Added proper prop destructuring and state organization
  - Implemented consistent naming conventions and helper utilities
  - Enhanced error handling and loading states management
  - Added comprehensive JSDoc-style comments for better code documentation

- **Technical Debt Reduction:**
  - Replaced repetitive form field definitions with reusable components
  - Consolidated chart update logic into centralized updateChartSpec function
  - Improved data flow with better state management patterns
  - Reduced code duplication through strategic use of helper functions

This refactor significantly improves component performance, maintainability, and user experience while maintaining backward compatibility and existing functionality.
Apple\Apple пре 9 месеци
родитељ
комит
768ab854d6

+ 22 - 0
controller/misc.go

@@ -74,11 +74,33 @@ func GetStatus(c *gin.Context) {
 			"oidc_client_id":              system_setting.GetOIDCSettings().ClientId,
 			"oidc_authorization_endpoint": system_setting.GetOIDCSettings().AuthorizationEndpoint,
 			"setup":                       constant.Setup,
+			"api_info":                    getApiInfo(),
 		},
 	})
 	return
 }
 
+func getApiInfo() []map[string]interface{} {
+	// 从OptionMap中获取API信息,如果不存在则返回空数组
+	common.OptionMapRWMutex.RLock()
+	apiInfoStr, exists := common.OptionMap["ApiInfo"]
+	common.OptionMapRWMutex.RUnlock()
+	
+	if !exists || apiInfoStr == "" {
+		// 如果没有配置,返回空数组
+		return []map[string]interface{}{}
+	}
+	
+	// 解析存储的API信息
+	var apiInfo []map[string]interface{}
+	if err := json.Unmarshal([]byte(apiInfoStr), &apiInfo); err != nil {
+		// 如果解析失败,返回空数组
+		return []map[string]interface{}{}
+	}
+	
+	return apiInfo
+}
+
 func GetNotice(c *gin.Context) {
 	common.OptionMapRWMutex.RLock()
 	defer common.OptionMapRWMutex.RUnlock()

+ 103 - 1
controller/option.go

@@ -2,16 +2,110 @@ package controller
 
 import (
 	"encoding/json"
+	"fmt"
 	"net/http"
+	"net/url"
 	"one-api/common"
 	"one-api/model"
 	"one-api/setting"
 	"one-api/setting/system_setting"
+	"regexp"
 	"strings"
 
 	"github.com/gin-gonic/gin"
 )
 
+func validateApiInfo(apiInfoStr string) error {
+	if apiInfoStr == "" {
+		return nil // 空字符串是合法的
+	}
+	
+	var apiInfoList []map[string]interface{}
+	if err := json.Unmarshal([]byte(apiInfoStr), &apiInfoList); err != nil {
+		return fmt.Errorf("API信息格式错误:%s", err.Error())
+	}
+	
+	// 验证数组长度
+	if len(apiInfoList) > 50 {
+		return fmt.Errorf("API信息数量不能超过50个")
+	}
+	
+	// 允许的颜色值
+	validColors := map[string]bool{
+		"blue": true, "green": true, "cyan": true, "purple": true, "pink": true,
+		"red": true, "orange": true, "amber": true, "yellow": true, "lime": true,
+		"light-green": true, "teal": true, "light-blue": true, "indigo": true,
+		"violet": true, "grey": true,
+	}
+	
+	// URL正则表达式
+	urlRegex := regexp.MustCompile(`^https?://[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*(/.*)?$`)
+	
+	for i, apiInfo := range apiInfoList {
+		// 检查必填字段
+		urlStr, ok := apiInfo["url"].(string)
+		if !ok || urlStr == "" {
+			return fmt.Errorf("第%d个API信息缺少URL字段", i+1)
+		}
+		
+		route, ok := apiInfo["route"].(string)
+		if !ok || route == "" {
+			return fmt.Errorf("第%d个API信息缺少线路描述字段", i+1)
+		}
+		
+		description, ok := apiInfo["description"].(string)
+		if !ok || description == "" {
+			return fmt.Errorf("第%d个API信息缺少说明字段", i+1)
+		}
+		
+		color, ok := apiInfo["color"].(string)
+		if !ok || color == "" {
+			return fmt.Errorf("第%d个API信息缺少颜色字段", i+1)
+		}
+		
+		// 验证URL格式
+		if !urlRegex.MatchString(urlStr) {
+			return fmt.Errorf("第%d个API信息的URL格式不正确", i+1)
+		}
+		
+		// 验证URL可解析性
+		if _, err := url.Parse(urlStr); err != nil {
+			return fmt.Errorf("第%d个API信息的URL无法解析:%s", i+1, err.Error())
+		}
+		
+		// 验证字段长度
+		if len(urlStr) > 500 {
+			return fmt.Errorf("第%d个API信息的URL长度不能超过500字符", i+1)
+		}
+		
+		if len(route) > 100 {
+			return fmt.Errorf("第%d个API信息的线路描述长度不能超过100字符", i+1)
+		}
+		
+		if len(description) > 200 {
+			return fmt.Errorf("第%d个API信息的说明长度不能超过200字符", i+1)
+		}
+		
+		// 验证颜色值
+		if !validColors[color] {
+			return fmt.Errorf("第%d个API信息的颜色值不合法", i+1)
+		}
+		
+		// 检查并过滤危险字符(防止XSS)
+		dangerousChars := []string{"<script", "<iframe", "javascript:", "onload=", "onerror=", "onclick="}
+		for _, dangerous := range dangerousChars {
+			if strings.Contains(strings.ToLower(description), dangerous) {
+				return fmt.Errorf("第%d个API信息的说明包含不允许的内容", i+1)
+			}
+			if strings.Contains(strings.ToLower(route), dangerous) {
+				return fmt.Errorf("第%d个API信息的线路描述包含不允许的内容", i+1)
+			}
+		}
+	}
+	
+	return nil
+}
+
 func GetOptions(c *gin.Context) {
 	var options []*model.Option
 	common.OptionMapRWMutex.Lock()
@@ -119,7 +213,15 @@ func UpdateOption(c *gin.Context) {
 			})
 			return
 		}
-
+	case "ApiInfo":
+		err = validateApiInfo(option.Value)
+		if err != nil {
+			c.JSON(http.StatusOK, gin.H{
+				"success": false,
+				"message": err.Error(),
+			})
+			return
+		}
 	}
 	err = model.UpdateOption(option.Key, option.Value)
 	if err != nil {

+ 1 - 0
model/option.go

@@ -122,6 +122,7 @@ func InitOptionMap() {
 	common.OptionMap["SensitiveWords"] = setting.SensitiveWordsToString()
 	common.OptionMap["StreamCacheQueueLength"] = strconv.Itoa(setting.StreamCacheQueueLength)
 	common.OptionMap["AutomaticDisableKeywords"] = operation_setting.AutomaticDisableKeywordsToString()
+	common.OptionMap["ApiInfo"] = ""
 
 	// 自动添加所有注册的模型配置
 	modelConfigs := config.GlobalConfig.ExportAllConfigs()

+ 57 - 0
web/src/components/settings/DashboardSetting.js

@@ -0,0 +1,57 @@
+import React, { useEffect, useState } from 'react';
+import { Card, Spin } from '@douyinfe/semi-ui';
+import { API, showError } from '../../helpers';
+import SettingsAPIInfo from '../../pages/Setting/Dashboard/SettingsAPIInfo.js';
+
+const DashboardSetting = () => {
+  let [inputs, setInputs] = useState({
+    ApiInfo: '',
+  });
+
+  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 in inputs) {
+          newInputs[item.key] = item.value;
+        }
+      });
+      setInputs(newInputs);
+    } else {
+      showError(message);
+    }
+  };
+
+  async function onRefresh() {
+    try {
+      setLoading(true);
+      await getOptions();
+    } catch (error) {
+      showError('刷新失败');
+      console.error(error);
+    } finally {
+      setLoading(false);
+    }
+  }
+
+  useEffect(() => {
+    onRefresh();
+  }, []);
+
+  return (
+    <>
+      <Spin spinning={loading} size='large'>
+        {/* API信息管理 */}
+        <Card style={{ marginTop: '10px' }}>
+          <SettingsAPIInfo options={inputs} refresh={onRefresh} />
+        </Card>
+      </Spin>
+    </>
+  );
+};
+
+export default DashboardSetting; 

+ 18 - 1
web/src/i18n/locales/en.json

@@ -1568,5 +1568,22 @@
   "资源消耗": "Resource Consumption",
   "性能指标": "Performance Indicators",
   "模型数据分析": "Model Data Analysis",
-  "搜索无结果": "No results found"
+  "搜索无结果": "No results found",
+  "仪表盘配置": "Dashboard Configuration",
+  "API信息管理,可以配置多个API地址用于状态展示和负载均衡": "API information management, you can configure multiple API addresses for status display and load balancing",
+  "线路描述": "Route description",
+  "颜色": "Color",
+  "标识颜色": "Identifier color",
+  "添加API": "Add API",
+  "保存配置": "Save Configuration",
+  "API信息": "API Information",
+  "暂无API信息配置": "No API information configured",
+  "暂无API信息": "No API information",
+  "请输入API地址": "Please enter the API address",
+  "请输入线路描述": "Please enter the route description",
+  "如:大带宽批量分析图片推荐": "e.g. Large bandwidth batch analysis of image recommendations",
+  "请输入说明": "Please enter the description",
+  "如:香港线路": "e.g. Hong Kong line",
+  "请联系管理员在系统设置中配置API信息": "Please contact the administrator to configure API information in the system settings.",
+  "确定要删除此API信息吗?": "Are you sure you want to delete this API information?"
 }

+ 23 - 0
web/src/index.css

@@ -74,6 +74,9 @@ code {
 .semi-navigation-item,
 .semi-tag-closable,
 .semi-input-wrapper,
+.semi-tabs-tab-button,
+.semi-select,
+.semi-button,
 .semi-datepicker-range-input {
   border-radius: 9999px !important;
 }
@@ -323,6 +326,24 @@ code {
   font-size: 1.1em;
 }
 
+/* API信息卡片样式 */
+.api-info-container {
+  position: relative;
+}
+
+.api-info-fade-indicator {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  height: 30px;
+  background: linear-gradient(transparent, var(--semi-color-bg-1));
+  pointer-events: none;
+  z-index: 1;
+  opacity: 0;
+  transition: opacity 0.3s ease;
+}
+
 /* ==================== 调试面板特定样式 ==================== */
 .debug-panel .semi-tabs {
   height: 100% !important;
@@ -379,6 +400,7 @@ code {
 }
 
 /* 隐藏模型设置区域的滚动条 */
+.api-info-scroll::-webkit-scrollbar,
 .model-settings-scroll::-webkit-scrollbar,
 .thinking-content-scroll::-webkit-scrollbar,
 .custom-request-textarea .semi-input::-webkit-scrollbar,
@@ -386,6 +408,7 @@ code {
   display: none;
 }
 
+.api-info-scroll,
 .model-settings-scroll,
 .thinking-content-scroll,
 .custom-request-textarea .semi-input,

Разлика између датотеке није приказан због своје велике величине
+ 461 - 363
web/src/pages/Detail/index.js


+ 399 - 0
web/src/pages/Setting/Dashboard/SettingsAPIInfo.js

@@ -0,0 +1,399 @@
+import React, { useEffect, useState } from 'react';
+import {
+  Button,
+  Space,
+  Table,
+  Form,
+  Typography,
+  Empty,
+  Divider,
+  Avatar,
+  Modal,
+  Tag
+} from '@douyinfe/semi-ui';
+import {
+  IllustrationNoResult,
+  IllustrationNoResultDark
+} from '@douyinfe/semi-illustrations';
+import {
+  Plus,
+  Edit,
+  Trash2,
+  Save,
+  Settings
+} from 'lucide-react';
+import { API, showError, showSuccess } from '../../../helpers';
+import { useTranslation } from 'react-i18next';
+
+const { Text } = Typography;
+
+const SettingsAPIInfo = ({ options, refresh }) => {
+  const { t } = useTranslation();
+
+  const [apiInfoList, setApiInfoList] = useState([]);
+  const [showApiModal, setShowApiModal] = useState(false);
+  const [showDeleteModal, setShowDeleteModal] = useState(false);
+  const [deletingApi, setDeletingApi] = useState(null);
+  const [editingApi, setEditingApi] = useState(null);
+  const [modalLoading, setModalLoading] = useState(false);
+  const [loading, setLoading] = useState(false);
+  const [hasChanges, setHasChanges] = useState(false);
+  const [apiForm, setApiForm] = useState({
+    url: '',
+    description: '',
+    route: '',
+    color: 'blue'
+  });
+
+  const colorOptions = [
+    { value: 'blue', label: 'blue' },
+    { value: 'green', label: 'green' },
+    { value: 'cyan', label: 'cyan' },
+    { value: 'purple', label: 'purple' },
+    { value: 'pink', label: 'pink' },
+    { value: 'red', label: 'red' },
+    { value: 'orange', label: 'orange' },
+    { value: 'amber', label: 'amber' },
+    { value: 'yellow', label: 'yellow' },
+    { value: 'lime', label: 'lime' },
+    { value: 'light-green', label: 'light-green' },
+    { value: 'teal', label: 'teal' },
+    { value: 'light-blue', label: 'light-blue' },
+    { value: 'indigo', label: 'indigo' },
+    { value: 'violet', label: 'violet' },
+    { value: 'grey', label: 'grey' }
+  ];
+
+  const updateOption = async (key, value) => {
+    const res = await API.put('/api/option/', {
+      key,
+      value,
+    });
+    const { success, message } = res.data;
+    if (success) {
+      showSuccess('API信息已更新');
+      if (refresh) refresh();
+    } else {
+      showError(message);
+    }
+  };
+
+  const submitApiInfo = async () => {
+    try {
+      setLoading(true);
+      const apiInfoJson = JSON.stringify(apiInfoList);
+      await updateOption('ApiInfo', apiInfoJson);
+      setHasChanges(false);
+    } catch (error) {
+      console.error('API信息更新失败', error);
+      showError('API信息更新失败');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  const handleAddApi = () => {
+    setEditingApi(null);
+    setApiForm({
+      url: '',
+      description: '',
+      route: '',
+      color: 'blue'
+    });
+    setShowApiModal(true);
+  };
+
+  const handleEditApi = (api) => {
+    setEditingApi(api);
+    setApiForm({
+      url: api.url,
+      description: api.description,
+      route: api.route,
+      color: api.color
+    });
+    setShowApiModal(true);
+  };
+
+  const handleDeleteApi = (api) => {
+    setDeletingApi(api);
+    setShowDeleteModal(true);
+  };
+
+  const confirmDeleteApi = () => {
+    if (deletingApi) {
+      const newList = apiInfoList.filter(api => api.id !== deletingApi.id);
+      setApiInfoList(newList);
+      setHasChanges(true);
+      showSuccess('API信息已删除,请及时点击“保存配置”进行保存');
+    }
+    setShowDeleteModal(false);
+    setDeletingApi(null);
+  };
+
+  const handleSaveApi = async () => {
+    if (!apiForm.url || !apiForm.route || !apiForm.description) {
+      showError('请填写完整的API信息');
+      return;
+    }
+
+    try {
+      setModalLoading(true);
+
+      let newList;
+      if (editingApi) {
+        newList = apiInfoList.map(api =>
+          api.id === editingApi.id
+            ? { ...api, ...apiForm }
+            : api
+        );
+      } else {
+        const newId = Math.max(...apiInfoList.map(api => api.id), 0) + 1;
+        const newApi = {
+          id: newId,
+          ...apiForm
+        };
+        newList = [...apiInfoList, newApi];
+      }
+
+      setApiInfoList(newList);
+      setHasChanges(true);
+      setShowApiModal(false);
+      showSuccess(editingApi ? 'API信息已更新,请及时点击“保存配置”进行保存' : 'API信息已添加,请及时点击“保存配置”进行保存');
+    } catch (error) {
+      showError('操作失败: ' + error.message);
+    } finally {
+      setModalLoading(false);
+    }
+  };
+
+  const parseApiInfo = (apiInfoStr) => {
+    if (!apiInfoStr) {
+      setApiInfoList([]);
+      return;
+    }
+
+    try {
+      const parsed = JSON.parse(apiInfoStr);
+      setApiInfoList(Array.isArray(parsed) ? parsed : []);
+    } catch (error) {
+      console.error('解析API信息失败:', error);
+      setApiInfoList([]);
+    }
+  };
+
+  useEffect(() => {
+    if (options.ApiInfo !== undefined) {
+      parseApiInfo(options.ApiInfo);
+    }
+  }, [options.ApiInfo]);
+
+  const columns = [
+    {
+      title: 'ID',
+      dataIndex: 'id',
+    },
+    {
+      title: t('API地址'),
+      dataIndex: 'url',
+      render: (text, record) => (
+        <Tag
+          color={record.color}
+          className="!rounded-full"
+          style={{ maxWidth: '280px' }}
+        >
+          {text}
+        </Tag>
+      ),
+    },
+    {
+      title: t('线路描述'),
+      dataIndex: 'route',
+      render: (text, record) => (
+        <Tag shape='circle'>
+          {text}
+        </Tag>
+      ),
+    },
+    {
+      title: t('说明'),
+      dataIndex: 'description',
+      ellipsis: true,
+      render: (text, record) => (
+        <Tag shape='circle'>
+          {text || '-'}
+        </Tag>
+      ),
+    },
+    {
+      title: t('颜色'),
+      dataIndex: 'color',
+      render: (color) => (
+        <Avatar
+          size="extra-extra-small"
+          color={color}
+        />
+      ),
+    },
+    {
+      title: t('操作'),
+      fixed: 'right',
+      render: (_, record) => (
+        <Space>
+          <Button
+            icon={<Edit size={14} />}
+            theme='light'
+            type='tertiary'
+            size='small'
+            className="!rounded-full"
+            onClick={() => handleEditApi(record)}
+          >
+            {t('编辑')}
+          </Button>
+          <Button
+            icon={<Trash2 size={14} />}
+            type='danger'
+            theme='light'
+            size='small'
+            className="!rounded-full"
+            onClick={() => handleDeleteApi(record)}
+          >
+            {t('删除')}
+          </Button>
+        </Space>
+      ),
+    },
+  ];
+
+  const renderHeader = () => (
+    <div className="flex flex-col w-full">
+      <div className="mb-2">
+        <div className="flex items-center text-blue-500">
+          <Settings size={16} className="mr-2" />
+          <Text>{t('API信息管理,可以配置多个API地址用于状态展示和负载均衡')}</Text>
+        </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={<Plus size={14} />}
+            className="!rounded-full w-full md:w-auto"
+            onClick={handleAddApi}
+          >
+            {t('添加API')}
+          </Button>
+          <Button
+            icon={<Save size={14} />}
+            onClick={submitApiInfo}
+            loading={loading}
+            disabled={!hasChanges}
+            type='secondary'
+            className="!rounded-full w-full md:w-auto"
+          >
+            {t('保存配置')}
+          </Button>
+        </div>
+      </div>
+    </div>
+  );
+
+  return (
+    <>
+      <Form.Section text={renderHeader()}>
+        <Table
+          columns={columns}
+          dataSource={apiInfoList}
+          scroll={{ x: 'max-content' }}
+          pagination={false}
+          size='middle'
+          loading={loading}
+          empty={
+            <Empty
+              image={<IllustrationNoResult style={{ width: 150, height: 150 }} />}
+              darkModeImage={<IllustrationNoResultDark style={{ width: 150, height: 150 }} />}
+              description={t('暂无API信息')}
+              style={{ padding: 30 }}
+            />
+          }
+          className="rounded-xl overflow-hidden"
+        />
+      </Form.Section>
+
+      <Modal
+        title={editingApi ? t('编辑API') : t('添加API')}
+        visible={showApiModal}
+        onOk={handleSaveApi}
+        onCancel={() => setShowApiModal(false)}
+        okText={t('保存')}
+        cancelText={t('取消')}
+        className="rounded-xl"
+        confirmLoading={modalLoading}
+      >
+        <Form layout='vertical' initValues={apiForm} key={editingApi ? editingApi.id : 'new'}>
+          <Form.Input
+            field='url'
+            label={t('API地址')}
+            placeholder='https://api.example.com'
+            rules={[{ required: true, message: t('请输入API地址') }]}
+            onChange={(value) => setApiForm({ ...apiForm, url: value })}
+          />
+          <Form.Input
+            field='route'
+            label={t('线路描述')}
+            placeholder={t('如:香港线路')}
+            rules={[{ required: true, message: t('请输入线路描述') }]}
+            onChange={(value) => setApiForm({ ...apiForm, route: value })}
+          />
+          <Form.Input
+            field='description'
+            label={t('说明')}
+            placeholder={t('如:大带宽批量分析图片推荐')}
+            rules={[{ required: true, message: t('请输入说明') }]}
+            onChange={(value) => setApiForm({ ...apiForm, description: value })}
+          />
+          <Form.Select
+            field='color'
+            label={t('标识颜色')}
+            optionList={colorOptions}
+            onChange={(value) => setApiForm({ ...apiForm, color: value })}
+            render={(option) => (
+              <div style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
+                <Avatar
+                  size="extra-extra-small"
+                  color={option.value}
+                />
+                {option.label}
+              </div>
+            )}
+          />
+        </Form>
+      </Modal>
+
+      <Modal
+        title={t('确认删除')}
+        visible={showDeleteModal}
+        onOk={confirmDeleteApi}
+        onCancel={() => {
+          setShowDeleteModal(false);
+          setDeletingApi(null);
+        }}
+        okText={t('确认删除')}
+        cancelText={t('取消')}
+        type="warning"
+        className="rounded-xl"
+        okButtonProps={{
+          type: 'danger',
+          theme: 'solid'
+        }}
+      >
+        <Text>{t('确定要删除此API信息吗?')}</Text>
+      </Modal>
+    </>
+  );
+};
+
+export default SettingsAPIInfo; 

+ 6 - 1
web/src/pages/Setting/index.js

@@ -6,10 +6,10 @@ import { useTranslation } from 'react-i18next';
 import SystemSetting from '../../components/settings/SystemSetting.js';
 import { isRoot } from '../../helpers';
 import OtherSetting from '../../components/settings/OtherSetting';
-import PersonalSetting from '../../components/settings/PersonalSetting.js';
 import OperationSetting from '../../components/settings/OperationSetting.js';
 import RateLimitSetting from '../../components/settings/RateLimitSetting.js';
 import ModelSetting from '../../components/settings/ModelSetting.js';
+import DashboardSetting from '../../components/settings/DashboardSetting.js';
 
 const Setting = () => {
   const { t } = useTranslation();
@@ -44,6 +44,11 @@ const Setting = () => {
       content: <OtherSetting />,
       itemKey: 'other',
     });
+    panes.push({
+      tab: t('仪表盘配置'),
+      content: <DashboardSetting />,
+      itemKey: 'dashboard',
+    });
   }
   const onChangeTab = (key) => {
     setTabActiveKey(key);

Неке датотеке нису приказане због велике количине промена