Commit d6276937 by CaIon

Merge remote-tracking branch 'origin/alpha' into alpha

parents 5511ba36 1d289681
...@@ -80,6 +80,8 @@ func GetStatus(c *gin.Context) { ...@@ -80,6 +80,8 @@ func GetStatus(c *gin.Context) {
"oidc_authorization_endpoint": system_setting.GetOIDCSettings().AuthorizationEndpoint, "oidc_authorization_endpoint": system_setting.GetOIDCSettings().AuthorizationEndpoint,
"setup": constant.Setup, "setup": constant.Setup,
"api_info": setting.GetApiInfo(), "api_info": setting.GetApiInfo(),
"announcements": setting.GetAnnouncements(),
"faq": setting.GetFAQ(),
}, },
}) })
return return
......
...@@ -128,6 +128,24 @@ func UpdateOption(c *gin.Context) { ...@@ -128,6 +128,24 @@ func UpdateOption(c *gin.Context) {
}) })
return return
} }
case "Announcements":
err = setting.ValidateConsoleSettings(option.Value, "Announcements")
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
case "FAQ":
err = setting.ValidateConsoleSettings(option.Value, "FAQ")
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": err.Error(),
})
return
}
} }
err = model.UpdateOption(option.Key, option.Value) err = model.UpdateOption(option.Key, option.Value)
if err != nil { if err != nil {
......
...@@ -106,7 +106,7 @@ func RequestEpay(c *gin.Context) { ...@@ -106,7 +106,7 @@ func RequestEpay(c *gin.Context) {
payType = "wxpay" payType = "wxpay"
} }
callBackAddress := service.GetCallbackAddress() callBackAddress := service.GetCallbackAddress()
returnUrl, _ := url.Parse(setting.ServerAddress + "/log") returnUrl, _ := url.Parse(setting.ServerAddress + "/console/log")
notifyUrl, _ := url.Parse(callBackAddress + "/api/user/epay/notify") notifyUrl, _ := url.Parse(callBackAddress + "/api/user/epay/notify")
tradeNo := fmt.Sprintf("%s%d", common.GetRandomString(6), time.Now().Unix()) tradeNo := fmt.Sprintf("%s%d", common.GetRandomString(6), time.Now().Unix())
tradeNo = fmt.Sprintf("USR%dNO%s", id, tradeNo) tradeNo = fmt.Sprintf("USR%dNO%s", id, tradeNo)
......
...@@ -6,15 +6,31 @@ import ( ...@@ -6,15 +6,31 @@ import (
"net/url" "net/url"
"one-api/common" "one-api/common"
"regexp" "regexp"
"sort"
"strings" "strings"
"time"
) )
// ValidateApiInfo 验证API信息格式 // ValidateConsoleSettings 验证控制台设置信息格式
func ValidateApiInfo(apiInfoStr string) error { func ValidateConsoleSettings(settingsStr string, settingType string) error {
if apiInfoStr == "" { if settingsStr == "" {
return nil // 空字符串是合法的 return nil // 空字符串是合法的
} }
switch settingType {
case "ApiInfo":
return validateApiInfo(settingsStr)
case "Announcements":
return validateAnnouncements(settingsStr)
case "FAQ":
return validateFAQ(settingsStr)
default:
return fmt.Errorf("未知的设置类型:%s", settingType)
}
}
// validateApiInfo 验证API信息格式
func validateApiInfo(apiInfoStr string) error {
var apiInfoList []map[string]interface{} var apiInfoList []map[string]interface{}
if err := json.Unmarshal([]byte(apiInfoStr), &apiInfoList); err != nil { if err := json.Unmarshal([]byte(apiInfoStr), &apiInfoList); err != nil {
return fmt.Errorf("API信息格式错误:%s", err.Error()) return fmt.Errorf("API信息格式错误:%s", err.Error())
...@@ -33,8 +49,10 @@ func ValidateApiInfo(apiInfoStr string) error { ...@@ -33,8 +49,10 @@ func ValidateApiInfo(apiInfoStr string) error {
"violet": true, "grey": true, "violet": true, "grey": true,
} }
// URL正则表达式 // URL正则表达式,支持域名和IP地址格式
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])?)*(/.*)?$`) // 域名格式:https://example.com 或 https://sub.example.com:8080
// IP地址格式:https://192.168.1.1 或 https://192.168.1.1:8080
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])?|(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?))(?::[0-9]{1,5})?(?:/.*)?$`)
for i, apiInfo := range apiInfoList { for i, apiInfo := range apiInfoList {
// 检查必填字段 // 检查必填字段
...@@ -101,6 +119,11 @@ func ValidateApiInfo(apiInfoStr string) error { ...@@ -101,6 +119,11 @@ func ValidateApiInfo(apiInfoStr string) error {
return nil return nil
} }
// ValidateApiInfo 保持向后兼容的函数
func ValidateApiInfo(apiInfoStr string) error {
return validateApiInfo(apiInfoStr)
}
// GetApiInfo 获取API信息列表 // GetApiInfo 获取API信息列表
func GetApiInfo() []map[string]interface{} { func GetApiInfo() []map[string]interface{} {
// 从OptionMap中获取API信息,如果不存在则返回空数组 // 从OptionMap中获取API信息,如果不存在则返回空数组
...@@ -121,4 +144,184 @@ func GetApiInfo() []map[string]interface{} { ...@@ -121,4 +144,184 @@ func GetApiInfo() []map[string]interface{} {
} }
return apiInfo return apiInfo
}
// validateAnnouncements 验证系统公告格式
func validateAnnouncements(announcementsStr string) error {
var announcementsList []map[string]interface{}
if err := json.Unmarshal([]byte(announcementsStr), &announcementsList); err != nil {
return fmt.Errorf("系统公告格式错误:%s", err.Error())
}
// 验证数组长度
if len(announcementsList) > 100 {
return fmt.Errorf("系统公告数量不能超过100个")
}
// 允许的类型值
validTypes := map[string]bool{
"default": true, "ongoing": true, "success": true, "warning": true, "error": true,
}
for i, announcement := range announcementsList {
// 检查必填字段
content, ok := announcement["content"].(string)
if !ok || content == "" {
return fmt.Errorf("第%d个公告缺少内容字段", i+1)
}
// 检查发布日期字段
publishDate, exists := announcement["publishDate"]
if !exists {
return fmt.Errorf("第%d个公告缺少发布日期字段", i+1)
}
publishDateStr, ok := publishDate.(string)
if !ok || publishDateStr == "" {
return fmt.Errorf("第%d个公告的发布日期不能为空", i+1)
}
// 验证ISO日期格式
if _, err := time.Parse(time.RFC3339, publishDateStr); err != nil {
return fmt.Errorf("第%d个公告的发布日期格式错误", i+1)
}
// 验证可选字段
if announcementType, exists := announcement["type"]; exists {
if typeStr, ok := announcementType.(string); ok {
if !validTypes[typeStr] {
return fmt.Errorf("第%d个公告的类型值不合法", i+1)
}
}
}
// 验证字段长度
if len(content) > 500 {
return fmt.Errorf("第%d个公告的内容长度不能超过500字符", i+1)
}
if extra, exists := announcement["extra"]; exists {
if extraStr, ok := extra.(string); ok && len(extraStr) > 200 {
return fmt.Errorf("第%d个公告的说明长度不能超过200字符", i+1)
}
}
// 检查并过滤危险字符(防止XSS)
dangerousChars := []string{"<script", "<iframe", "javascript:", "onload=", "onerror=", "onclick="}
for _, dangerous := range dangerousChars {
if strings.Contains(strings.ToLower(content), dangerous) {
return fmt.Errorf("第%d个公告的内容包含不允许的内容", i+1)
}
}
}
return nil
}
// validateFAQ 验证常见问答格式
func validateFAQ(faqStr string) error {
var faqList []map[string]interface{}
if err := json.Unmarshal([]byte(faqStr), &faqList); err != nil {
return fmt.Errorf("常见问答格式错误:%s", err.Error())
}
// 验证数组长度
if len(faqList) > 100 {
return fmt.Errorf("常见问答数量不能超过100个")
}
for i, faq := range faqList {
// 检查必填字段
title, ok := faq["title"].(string)
if !ok || title == "" {
return fmt.Errorf("第%d个问答缺少标题字段", i+1)
}
content, ok := faq["content"].(string)
if !ok || content == "" {
return fmt.Errorf("第%d个问答缺少内容字段", i+1)
}
// 验证字段长度
if len(title) > 200 {
return fmt.Errorf("第%d个问答的标题长度不能超过200字符", i+1)
}
if len(content) > 1000 {
return fmt.Errorf("第%d个问答的内容长度不能超过1000字符", i+1)
}
// 检查并过滤危险字符(防止XSS)
dangerousChars := []string{"<script", "<iframe", "javascript:", "onload=", "onerror=", "onclick="}
for _, dangerous := range dangerousChars {
if strings.Contains(strings.ToLower(title), dangerous) {
return fmt.Errorf("第%d个问答的标题包含不允许的内容", i+1)
}
if strings.Contains(strings.ToLower(content), dangerous) {
return fmt.Errorf("第%d个问答的内容包含不允许的内容", i+1)
}
}
}
return nil
}
// GetAnnouncements 获取系统公告列表(返回最新的前20条)
func GetAnnouncements() []map[string]interface{} {
common.OptionMapRWMutex.RLock()
announcementsStr, exists := common.OptionMap["Announcements"]
common.OptionMapRWMutex.RUnlock()
if !exists || announcementsStr == "" {
return []map[string]interface{}{}
}
var announcements []map[string]interface{}
if err := json.Unmarshal([]byte(announcementsStr), &announcements); err != nil {
return []map[string]interface{}{}
}
// 按发布日期降序排序(最新的在前)
sort.Slice(announcements, func(i, j int) bool {
dateI, okI := announcements[i]["publishDate"].(string)
dateJ, okJ := announcements[j]["publishDate"].(string)
if !okI || !okJ {
return false
}
timeI, errI := time.Parse(time.RFC3339, dateI)
timeJ, errJ := time.Parse(time.RFC3339, dateJ)
if errI != nil || errJ != nil {
return false
}
return timeI.After(timeJ)
})
// 限制返回前20条
if len(announcements) > 20 {
announcements = announcements[:20]
}
return announcements
}
// GetFAQ 获取常见问答列表
func GetFAQ() []map[string]interface{} {
common.OptionMapRWMutex.RLock()
faqStr, exists := common.OptionMap["FAQ"]
common.OptionMapRWMutex.RUnlock()
if !exists || faqStr == "" {
return []map[string]interface{}{}
}
var faq []map[string]interface{}
if err := json.Unmarshal([]byte(faqStr), &faq); err != nil {
return []map[string]interface{}{}
}
return faq
} }
\ No newline at end of file
...@@ -2,10 +2,14 @@ import React, { useEffect, useState } from 'react'; ...@@ -2,10 +2,14 @@ import React, { useEffect, useState } from 'react';
import { Card, Spin } from '@douyinfe/semi-ui'; import { Card, Spin } from '@douyinfe/semi-ui';
import { API, showError } from '../../helpers'; import { API, showError } from '../../helpers';
import SettingsAPIInfo from '../../pages/Setting/Dashboard/SettingsAPIInfo.js'; import SettingsAPIInfo from '../../pages/Setting/Dashboard/SettingsAPIInfo.js';
import SettingsAnnouncements from '../../pages/Setting/Dashboard/SettingsAnnouncements.js';
import SettingsFAQ from '../../pages/Setting/Dashboard/SettingsFAQ.js';
const DashboardSetting = () => { const DashboardSetting = () => {
let [inputs, setInputs] = useState({ let [inputs, setInputs] = useState({
ApiInfo: '', ApiInfo: '',
Announcements: '',
FAQ: '',
}); });
let [loading, setLoading] = useState(false); let [loading, setLoading] = useState(false);
...@@ -49,6 +53,16 @@ const DashboardSetting = () => { ...@@ -49,6 +53,16 @@ const DashboardSetting = () => {
<Card style={{ marginTop: '10px' }}> <Card style={{ marginTop: '10px' }}>
<SettingsAPIInfo options={inputs} refresh={onRefresh} /> <SettingsAPIInfo options={inputs} refresh={onRefresh} />
</Card> </Card>
{/* 系统公告管理 */}
<Card style={{ marginTop: '10px' }}>
<SettingsAnnouncements options={inputs} refresh={onRefresh} />
</Card>
{/* 常见问答管理 */}
<Card style={{ marginTop: '10px' }}>
<SettingsFAQ options={inputs} refresh={onRefresh} />
</Card>
</Spin> </Spin>
</> </>
); );
......
...@@ -446,3 +446,66 @@ export const getLastAssistantMessage = (messages) => { ...@@ -446,3 +446,66 @@ export const getLastAssistantMessage = (messages) => {
} }
return null; return null;
}; };
// 计算相对时间(几天前、几小时前等)
export const getRelativeTime = (publishDate) => {
if (!publishDate) return '';
const now = new Date();
const pubDate = new Date(publishDate);
// 如果日期无效,返回原始字符串
if (isNaN(pubDate.getTime())) return publishDate;
const diffMs = now.getTime() - pubDate.getTime();
const diffSeconds = Math.floor(diffMs / 1000);
const diffMinutes = Math.floor(diffSeconds / 60);
const diffHours = Math.floor(diffMinutes / 60);
const diffDays = Math.floor(diffHours / 24);
const diffWeeks = Math.floor(diffDays / 7);
const diffMonths = Math.floor(diffDays / 30);
const diffYears = Math.floor(diffDays / 365);
// 如果是未来时间,显示具体日期
if (diffMs < 0) {
return formatDateString(pubDate);
}
// 根据时间差返回相应的描述
if (diffSeconds < 60) {
return '刚刚';
} else if (diffMinutes < 60) {
return `${diffMinutes} 分钟前`;
} else if (diffHours < 24) {
return `${diffHours} 小时前`;
} else if (diffDays < 7) {
return `${diffDays} 天前`;
} else if (diffWeeks < 4) {
return `${diffWeeks} 周前`;
} else if (diffMonths < 12) {
return `${diffMonths} 个月前`;
} else if (diffYears < 2) {
return '1 年前';
} else {
// 超过2年显示具体日期
return formatDateString(pubDate);
}
};
// 格式化日期字符串
export const formatDateString = (date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
};
// 格式化日期时间字符串(包含时间)
export const formatDateTimeString = (date) => {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hours = String(date.getHours()).padStart(2, '0');
const minutes = String(date.getMinutes()).padStart(2, '0');
return `${year}-${month}-${day} ${hours}:${minutes}`;
};
...@@ -510,7 +510,7 @@ ...@@ -510,7 +510,7 @@
"此项可选,输入镜像站地址,格式为:": "This is optional, enter the mirror site address, the format is:", "此项可选,输入镜像站地址,格式为:": "This is optional, enter the mirror site address, the format is:",
"模型映射": "Model mapping", "模型映射": "Model mapping",
"请输入默认 API 版本,例如:2023-03-15-preview,该配置可以被实际的请求查询参数所覆盖": "Please enter the default API version, for example: 2023-03-15-preview, this configuration can be overridden by the actual request query parameters", "请输入默认 API 版本,例如:2023-03-15-preview,该配置可以被实际的请求查询参数所覆盖": "Please enter the default API version, for example: 2023-03-15-preview, this configuration can be overridden by the actual request query parameters",
"默认": "default", "默认": "Default",
"图片演示": "Image demo", "图片演示": "Image demo",
"注意,系统请求的时模型名称中的点会被剔除,例如:gpt-4.1会请求为gpt-41,所以在Azure部署的时候,部署模型名称需要手动改为gpt-41": "Note that the dot in the model name requested by the system will be removed, for example: gpt-4.1 will be requested as gpt-41, so when deploying on Azure, the deployment model name needs to be manually changed to gpt-41", "注意,系统请求的时模型名称中的点会被剔除,例如:gpt-4.1会请求为gpt-41,所以在Azure部署的时候,部署模型名称需要手动改为gpt-41": "Note that the dot in the model name requested by the system will be removed, for example: gpt-4.1 will be requested as gpt-41, so when deploying on Azure, the deployment model name needs to be manually changed to gpt-41",
"2025年5月10日后添加的渠道,不需要再在部署的时候移除模型名称中的\".\"": "After May 10, 2025, channels added do not need to remove the dot in the model name during deployment", "2025年5月10日后添加的渠道,不需要再在部署的时候移除模型名称中的\".\"": "After May 10, 2025, channels added do not need to remove the dot in the model name during deployment",
...@@ -882,7 +882,7 @@ ...@@ -882,7 +882,7 @@
"线路监控": "line monitoring", "线路监控": "line monitoring",
"查看全部": "View all", "查看全部": "View all",
"高延迟": "high latency", "高延迟": "high latency",
"异常": "abnormal", "异常": "Abnormal",
"的未命名令牌": "unnamed token", "的未命名令牌": "unnamed token",
"令牌更新成功!": "Token updated successfully!", "令牌更新成功!": "Token updated successfully!",
"(origin) Discord原链接": "(origin) Discord original link", "(origin) Discord原链接": "(origin) Discord original link",
...@@ -1584,14 +1584,13 @@ ...@@ -1584,14 +1584,13 @@
"模型数据分析": "Model Data Analysis", "模型数据分析": "Model Data Analysis",
"搜索无结果": "No results found", "搜索无结果": "No results found",
"仪表盘配置": "Dashboard Configuration", "仪表盘配置": "Dashboard Configuration",
"API信息管理,可以配置多个API地址用于状态展示和负载均衡": "API information management, you can configure multiple API addresses for status display and load balancing", "API信息管理,可以配置多个API地址用于状态展示和负载均衡(最多50个)": "API information management, you can configure multiple API addresses for status display and load balancing (maximum 50)",
"线路描述": "Route description", "线路描述": "Route description",
"颜色": "Color", "颜色": "Color",
"标识颜色": "Identifier color", "标识颜色": "Identifier color",
"添加API": "Add API", "添加API": "Add API",
"保存配置": "Save Configuration", "保存配置": "Save Configuration",
"API信息": "API Information", "API信息": "API Information",
"暂无API信息配置": "No API information configured",
"暂无API信息": "No API information", "暂无API信息": "No API information",
"请输入API地址": "Please enter the API address", "请输入API地址": "Please enter the API address",
"请输入线路描述": "Please enter the route description", "请输入线路描述": "Please enter the route description",
...@@ -1599,6 +1598,36 @@ ...@@ -1599,6 +1598,36 @@
"请输入说明": "Please enter the description", "请输入说明": "Please enter the description",
"如:香港线路": "e.g. Hong Kong line", "如:香港线路": "e.g. Hong Kong line",
"请联系管理员在系统设置中配置API信息": "Please contact the administrator to configure API information in the system settings.", "请联系管理员在系统设置中配置API信息": "Please contact the administrator to configure API information in the system settings.",
"请联系管理员在系统设置中配置公告信息": "Please contact the administrator to configure notice information in the system settings.",
"请联系管理员在系统设置中配置常见问答": "Please contact the administrator to configure FAQ information in the system settings.",
"确定要删除此API信息吗?": "Are you sure you want to delete this API information?", "确定要删除此API信息吗?": "Are you sure you want to delete this API information?",
"测速": "Speed Test" "测速": "Speed Test",
"批量删除": "Batch Delete",
"常见问答": "FAQ",
"进行中": "Ongoing",
"警告": "Warning",
"添加公告": "Add Notice",
"编辑公告": "Edit Notice",
"公告内容": "Notice Content",
"请输入公告内容": "Please enter the notice content",
"发布日期": "Publish Date",
"请选择发布日期": "Please select the publish date",
"发布时间": "Publish Time",
"公告类型": "Notice Type",
"说明信息": "Description",
"可选,公告的补充说明": "Optional, additional information for the notice",
"确定要删除此公告吗?": "Are you sure you want to delete this notice?",
"系统公告管理,可以发布系统通知和重要消息": "System notice management, you can publish system notices and important messages",
"暂无系统公告": "No system notice",
"添加问答": "Add FAQ",
"编辑问答": "Edit FAQ",
"问题标题": "Question Title",
"请输入问题标题": "Please enter the question title",
"回答内容": "Answer Content",
"请输入回答内容": "Please enter the answer content",
"确定要删除此问答吗?": "Are you sure you want to delete this FAQ?",
"系统公告管理,可以发布系统通知和重要消息(最多100个,前端显示最新20条)": "System notice management, you can publish system notices and important messages (maximum 100, display latest 20 on the front end)",
"常见问答管理,为用户提供常见问题的答案(最多50个,前端显示最新20条)": "FAQ management, providing answers to common questions for users (maximum 50, display latest 20 on the front end)",
"暂无常见问答": "No FAQ",
"显示最新20条": "Display latest 20"
} }
\ No newline at end of file
...@@ -301,12 +301,12 @@ code { ...@@ -301,12 +301,12 @@ code {
font-size: 1.1em; font-size: 1.1em;
} }
/* API信息卡片样式 */ /* 卡片内容容器通用样式 */
.api-info-container { .card-content-container {
position: relative; position: relative;
} }
.api-info-fade-indicator { .card-content-fade-indicator {
position: absolute; position: absolute;
bottom: 0; bottom: 0;
left: 0; left: 0;
...@@ -374,8 +374,8 @@ code { ...@@ -374,8 +374,8 @@ code {
background: transparent; background: transparent;
} }
/* 隐藏模型设置区域的滚动条 */ /* 隐藏卡片内容区域的滚动条 */
.api-info-scroll::-webkit-scrollbar, .card-content-scroll::-webkit-scrollbar,
.model-settings-scroll::-webkit-scrollbar, .model-settings-scroll::-webkit-scrollbar,
.thinking-content-scroll::-webkit-scrollbar, .thinking-content-scroll::-webkit-scrollbar,
.custom-request-textarea .semi-input::-webkit-scrollbar, .custom-request-textarea .semi-input::-webkit-scrollbar,
...@@ -383,7 +383,7 @@ code { ...@@ -383,7 +383,7 @@ code {
display: none; display: none;
} }
.api-info-scroll, .card-content-scroll,
.model-settings-scroll, .model-settings-scroll,
.thinking-content-scroll, .thinking-content-scroll,
.custom-request-textarea .semi-input, .custom-request-textarea .semi-input,
......
...@@ -44,6 +44,9 @@ const SettingsAPIInfo = ({ options, refresh }) => { ...@@ -44,6 +44,9 @@ const SettingsAPIInfo = ({ options, refresh }) => {
route: '', route: '',
color: 'blue' color: 'blue'
}); });
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(10);
const [selectedRowKeys, setSelectedRowKeys] = useState([]);
const colorOptions = [ const colorOptions = [
{ value: 'blue', label: 'blue' }, { value: 'blue', label: 'blue' },
...@@ -237,6 +240,7 @@ const SettingsAPIInfo = ({ options, refresh }) => { ...@@ -237,6 +240,7 @@ const SettingsAPIInfo = ({ options, refresh }) => {
{ {
title: t('操作'), title: t('操作'),
fixed: 'right', fixed: 'right',
width: 150,
render: (_, record) => ( render: (_, record) => (
<Space> <Space>
<Button <Button
...@@ -264,12 +268,25 @@ const SettingsAPIInfo = ({ options, refresh }) => { ...@@ -264,12 +268,25 @@ const SettingsAPIInfo = ({ options, refresh }) => {
}, },
]; ];
const handleBatchDelete = () => {
if (selectedRowKeys.length === 0) {
showError('请先选择要删除的API信息');
return;
}
const newList = apiInfoList.filter(api => !selectedRowKeys.includes(api.id));
setApiInfoList(newList);
setSelectedRowKeys([]);
setHasChanges(true);
showSuccess(`已删除 ${selectedRowKeys.length} 个API信息,请及时点击“保存配置”进行保存`);
};
const renderHeader = () => ( const renderHeader = () => (
<div className="flex flex-col w-full"> <div className="flex flex-col w-full">
<div className="mb-2"> <div className="mb-2">
<div className="flex items-center text-blue-500"> <div className="flex items-center text-blue-500">
<Settings size={16} className="mr-2" /> <Settings size={16} className="mr-2" />
<Text>{t('API信息管理,可以配置多个API地址用于状态展示和负载均衡')}</Text> <Text>{t('API信息管理,可以配置多个API地址用于状态展示和负载均衡(最多50个)')}</Text>
</div> </div>
</div> </div>
...@@ -287,6 +304,16 @@ const SettingsAPIInfo = ({ options, refresh }) => { ...@@ -287,6 +304,16 @@ const SettingsAPIInfo = ({ options, refresh }) => {
{t('添加API')} {t('添加API')}
</Button> </Button>
<Button <Button
icon={<Trash2 size={14} />}
type='danger'
theme='light'
onClick={handleBatchDelete}
disabled={selectedRowKeys.length === 0}
className="!rounded-full w-full md:w-auto"
>
{t('批量删除')} {selectedRowKeys.length > 0 && `(${selectedRowKeys.length})`}
</Button>
<Button
icon={<Save size={14} />} icon={<Save size={14} />}
onClick={submitApiInfo} onClick={submitApiInfo}
loading={loading} loading={loading}
...@@ -301,14 +328,56 @@ const SettingsAPIInfo = ({ options, refresh }) => { ...@@ -301,14 +328,56 @@ const SettingsAPIInfo = ({ options, refresh }) => {
</div> </div>
); );
// 计算当前页显示的数据
const getCurrentPageData = () => {
const startIndex = (currentPage - 1) * pageSize;
const endIndex = startIndex + pageSize;
return apiInfoList.slice(startIndex, endIndex);
};
const rowSelection = {
selectedRowKeys,
onChange: (selectedRowKeys, selectedRows) => {
setSelectedRowKeys(selectedRowKeys);
},
onSelect: (record, selected, selectedRows) => {
console.log(`选择行: ${selected}`, record);
},
onSelectAll: (selected, selectedRows) => {
console.log(`全选: ${selected}`, selectedRows);
},
getCheckboxProps: (record) => ({
disabled: false,
name: record.id,
}),
};
return ( return (
<> <>
<Form.Section text={renderHeader()}> <Form.Section text={renderHeader()}>
<Table <Table
columns={columns} columns={columns}
dataSource={apiInfoList} dataSource={getCurrentPageData()}
rowSelection={rowSelection}
rowKey="id"
scroll={{ x: 'max-content' }} scroll={{ x: 'max-content' }}
pagination={false} pagination={{
currentPage: currentPage,
pageSize: pageSize,
total: apiInfoList.length,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total, range) => t(`共 ${total} 条记录,显示第 ${range[0]}-${range[1]} 条`),
pageSizeOptions: ['5', '10', '20', '50'],
onChange: (page, size) => {
setCurrentPage(page);
setPageSize(size);
},
onShowSizeChange: (current, size) => {
setCurrentPage(1);
setPageSize(size);
}
}}
size='middle' size='middle'
loading={loading} loading={loading}
empty={ empty={
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment