diff --git a/Dockerfile b/Dockerfile index aa43de1c9603..11983399f07e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,38 +1,14 @@ -FROM oven/bun:latest AS builder +FROM golang:latest -WORKDIR /build -COPY web/package.json . -COPY web/bun.lock . -RUN bun install -COPY ./web . -COPY ./VERSION . -RUN DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat VERSION) bun run build +WORKDIR /app -FROM golang:alpine AS builder2 -ENV GO111MODULE=on CGO_ENABLED=0 - -ARG TARGETOS -ARG TARGETARCH -ENV GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64} -ENV GOEXPERIMENT=greenteagc - -WORKDIR /build - -ADD go.mod go.sum ./ -RUN go mod download +ENV GOOS=windows +ENV GOARCH=amd64 +ENV CGO_ENABLED=0 +# 复制源码(包含我们伪造的 web/dist) COPY . . -COPY --from=builder /build/dist ./web/dist -RUN go build -ldflags "-s -w -X 'github.com/QuantumNous/new-api/common.Version=$(cat VERSION)'" -o new-api -FROM debian:bookworm-slim - -RUN apt-get update \ - && apt-get install -y --no-install-recommends ca-certificates tzdata libasan8 wget \ - && rm -rf /var/lib/apt/lists/* \ - && update-ca-certificates - -COPY --from=builder2 /build/new-api / -EXPOSE 3000 -WORKDIR /data -ENTRYPOINT ["/new-api"] +# 下载依赖并编译 +RUN go mod download +RUN go build -ldflags "-s -w" -o new-api-galaxy.exe diff --git a/controller/token.go b/controller/token.go index c5dc5ec42d4c..cef42043101d 100644 --- a/controller/token.go +++ b/controller/token.go @@ -191,6 +191,10 @@ func AddToken(c *gin.Context) { AllowIps: token.AllowIps, Group: token.Group, CrossGroupRetry: token.CrossGroupRetry, + Duration: token.Duration, + } + if token.Duration > 0 { + cleanToken.ExpiredTime = -1 } err = cleanToken.Insert() if err != nil { @@ -286,6 +290,7 @@ func UpdateToken(c *gin.Context) { cleanToken.AllowIps = token.AllowIps cleanToken.Group = token.Group cleanToken.CrossGroupRetry = token.CrossGroupRetry + cleanToken.Duration = token.Duration } err = cleanToken.Update() if err != nil { diff --git a/docs/deployment-guide.md b/docs/deployment-guide.md new file mode 100644 index 000000000000..e82e81f974e3 --- /dev/null +++ b/docs/deployment-guide.md @@ -0,0 +1,219 @@ +# New API 部署指南(避坑记录) + +> 记录日期:2026-01-30 +> 部署环境:Windows 11 本地开发 → Debian 12 服务器 + +## 部署方式 + +本项目采用**本地编译 + 上传可执行文件**的方式部署,而非 Docker 镜像。 + +### 为什么选择这种方式? + +| 方式 | 优点 | 缺点 | +|------|------|------| +| Docker 官方镜像 | 一键部署,简单 | 无法使用自定义修改的代码 | +| **本地编译上传** | 可部署自定义版本 | 需要手动处理依赖 | + +## 遇到的问题及解决方案 + +### 1. SSH 密钥加载失败 + +**错误信息:** +``` +Load key "xxx/id_ed25519": error in libcrypto +``` + +**原因:** Windows 系统的 SSH 密钥文件包含 CRLF 行尾符(`\r\n`),Linux 只认 LF(`\n`)。 + +**解决方案:** +```bash +# 去除 Windows 行尾符 +cat /path/to/id_ed25519 | tr -d '\r' > /tmp/fixed_key +chmod 600 /tmp/fixed_key +ssh -i /tmp/fixed_key user@server +``` + +--- + +### 2. 编译成了 Windows 可执行文件 + +**错误信息:** +``` +/www1/new-api/new-api: cannot execute binary file: Exec format error +``` + +**原因:** 在 Windows 上直接运行 `go build`,默认编译成 Windows PE 格式。 + +**解决方案:** 必须设置交叉编译环境变量: +```bash +# 正确的 Linux 编译命令 +GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "-s -w" -o new-api-linux . +``` + +**验证方法:** +```bash +file new-api-linux +# 应显示:ELF 64-bit LSB executable, x86-64 +# 而不是:PE32+ executable (console) x86-64, for MS Windows +``` + +--- + +### 3. 前端组件缺少导入导致黑屏 + +**错误信息:** +``` +ReferenceError: Space is not defined +``` + +**原因:** 某些组件使用了 `` 但忘记从 `@douyinfe/semi-ui` 导入。 + +**受影响文件:** +- `web/src/pages/Setting/Payment/SettingsPaymentGatewayCreem.jsx` +- `web/src/pages/Setting/Ratio/UpstreamRatioSync.jsx` + +**解决方案:** 在导入语句中添加 `Space`: +```jsx +import { + Button, + // ... 其他组件 + Space, // 添加这行 +} from '@douyinfe/semi-ui'; +``` + +**预防措施:** +- 使用 ESLint 的 `no-undef` 规则 +- 提交前运行 `bun run build` 检查是否有错误 + +--- + +### 4. 上传文件时服务正在运行 + +**错误信息:** +``` +scp: dest open "/www1/new-api/new-api": Failure +``` + +**原因:** Linux 下正在运行的可执行文件无法被覆盖。 + +**解决方案:** 先停止服务再上传: +```bash +ssh user@server "systemctl stop new-api" +scp new-api-linux user@server:/www1/new-api/new-api +ssh user@server "chmod +x /www1/new-api/new-api && systemctl start new-api" +``` + +--- + +## 完整部署流程 + +### 前置条件 + +- 本地安装 Go 1.20+ +- 本地安装 Bun(用于前端构建) +- 服务器安装 Docker(用于 PostgreSQL 和 Redis) + +### 步骤 + +```bash +# 1. 构建前端 +cd web +bun install +bun run build + +# 2. 编译后端(Linux 版本) +cd .. +GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "-s -w" -o new-api-linux . + +# 3. 上传到服务器 +ssh user@server "systemctl stop new-api 2>/dev/null; mkdir -p /www1/new-api/logs" +scp new-api-linux user@server:/www1/new-api/new-api +ssh user@server "chmod +x /www1/new-api/new-api" + +# 4. 首次部署:启动数据库 +ssh user@server << 'EOF' +docker run -d \ + --name new-api-postgres \ + --restart always \ + -e POSTGRES_USER=newapi \ + -e POSTGRES_PASSWORD=YOUR_SECURE_PASSWORD \ + -e POSTGRES_DB=newapi \ + -p 127.0.0.1:5433:5432 \ + -v /www1/new-api/pg_data:/var/lib/postgresql/data \ + postgres:15 + +docker run -d \ + --name new-api-redis \ + --restart always \ + -p 127.0.0.1:6380:6379 \ + redis:latest +EOF + +# 5. 创建启动脚本 +ssh user@server << 'EOF' +cat > /www1/new-api/start.sh << 'SCRIPT' +#!/bin/bash +cd /www1/new-api +export SQL_DSN="postgresql://newapi:YOUR_SECURE_PASSWORD@127.0.0.1:5433/newapi" +export REDIS_CONN_STRING="redis://127.0.0.1:6380" +export TZ=Asia/Shanghai +exec ./new-api --port 9527 --log-dir /www1/new-api/logs +SCRIPT +chmod +x /www1/new-api/start.sh +EOF + +# 6. 创建 systemd 服务 +ssh user@server << 'EOF' +cat > /etc/systemd/system/new-api.service << 'SERVICE' +[Unit] +Description=New API Service +After=network.target docker.service +Requires=docker.service + +[Service] +Type=simple +User=root +WorkingDirectory=/www1/new-api +ExecStart=/www1/new-api/start.sh +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target +SERVICE +systemctl daemon-reload +systemctl enable new-api +systemctl start new-api +EOF +``` + +--- + +## 常用运维命令 + +```bash +# 查看服务状态 +systemctl status new-api + +# 查看日志 +tail -f /www1/new-api/logs/*.log +journalctl -u new-api -f + +# 重启服务 +systemctl restart new-api + +# 数据库备份 +docker exec new-api-postgres pg_dump -U newapi newapi > backup_$(date +%Y%m%d).sql +``` + +--- + +## 目录结构 + +``` +/www1/new-api/ +├── new-api # 可执行文件 +├── start.sh # 启动脚本 +├── logs/ # 日志目录 +└── pg_data/ # PostgreSQL 数据目录 +``` diff --git a/model/token.go b/model/token.go index b68fc0cfba43..5728b6444f75 100644 --- a/model/token.go +++ b/model/token.go @@ -27,6 +27,8 @@ type Token struct { UsedQuota int `json:"used_quota" gorm:"default:0"` // used quota Group string `json:"group" gorm:"default:''"` CrossGroupRetry bool `json:"cross_group_retry"` // 跨分组重试,仅auto分组有效 + ActivatedTime int64 `json:"activated_time" gorm:"bigint;default:0"` + Duration int64 `json:"duration" gorm:"bigint;default:0"` DeletedAt gorm.DeletedAt `gorm:"index"` } @@ -87,6 +89,20 @@ func ValidateUserToken(key string) (token *Token, err error) { if token.Status != common.TokenStatusEnabled { return token, errors.New("该令牌状态不可用") } + if token.ExpiredTime == -1 && token.Duration > 0 && token.ActivatedTime == 0 { + token.ActivatedTime = common.GetTimestamp() + token.ExpiredTime = token.ActivatedTime + token.Duration + // Update DB + DB.Model(token).Select("activated_time", "expired_time").Updates(token) + // Update Redis if enabled + if common.RedisEnabled { + gopool.Go(func() { + if err := cacheSetToken(*token); err != nil { + common.SysLog("failed to update token cache: " + err.Error()) + } + }) + } + } if token.ExpiredTime != -1 && token.ExpiredTime < common.GetTimestamp() { if !common.RedisEnabled { token.Status = common.TokenStatusExpired @@ -190,7 +206,7 @@ func (token *Token) Update() (err error) { } }() err = DB.Model(token).Select("name", "status", "expired_time", "remain_quota", "unlimited_quota", - "model_limits_enabled", "model_limits", "allow_ips", "group", "cross_group_retry").Updates(token).Error + "model_limits_enabled", "model_limits", "allow_ips", "group", "cross_group_retry", "duration", "activated_time").Updates(token).Error return err } diff --git a/web/package.json b/web/package.json index 9ac8e266eb39..b641db91e78e 100644 --- a/web/package.json +++ b/web/package.json @@ -10,6 +10,7 @@ "@visactor/react-vchart": "~1.8.8", "@visactor/vchart": "~1.8.8", "@visactor/vchart-semi-theme": "~1.8.8", + "antd": "^6.2.2", "axios": "1.12.0", "clsx": "^2.1.1", "country-flag-icons": "^1.5.19", diff --git a/web/src/components/common/modals/SecureVerificationModal.jsx b/web/src/components/common/modals/SecureVerificationModal.jsx index 6c61c291d1d6..2aea332e9bc8 100644 --- a/web/src/components/common/modals/SecureVerificationModal.jsx +++ b/web/src/components/common/modals/SecureVerificationModal.jsx @@ -87,7 +87,11 @@ const SecureVerificationModal = ({ title={title || t('安全验证')} visible={visible} onCancel={onCancel} - footer={} + footer={ +
+ +
+ } width={500} style={{ maxWidth: '90vw' }} > diff --git a/web/src/components/common/modals/TwoFactorAuthModal.jsx b/web/src/components/common/modals/TwoFactorAuthModal.jsx index 082e63d797c4..2f6c8b15c23d 100644 --- a/web/src/components/common/modals/TwoFactorAuthModal.jsx +++ b/web/src/components/common/modals/TwoFactorAuthModal.jsx @@ -19,7 +19,7 @@ For commercial licensing, please contact support@quantumnous.com import React from 'react'; import { useTranslation } from 'react-i18next'; -import { Modal, Button, Input, Typography } from '@douyinfe/semi-ui'; +import { Modal, Button, Input, Typography, Space } from '@douyinfe/semi-ui'; /** * 可复用的两步验证模态框组件 @@ -76,7 +76,7 @@ const TwoFactorAuthModal = ({ visible={visible} onCancel={onCancel} footer={ - <> + - + } width={500} style={{ maxWidth: '90vw' }} diff --git a/web/src/components/layout/NoticeModal.jsx b/web/src/components/layout/NoticeModal.jsx index c8197a58ba7b..273452587dcd 100644 --- a/web/src/components/layout/NoticeModal.jsx +++ b/web/src/components/layout/NoticeModal.jsx @@ -236,7 +236,7 @@ const NoticeModal = ({ visible={visible} onCancel={onClose} footer={ -
+
diff --git a/web/src/components/settings/ChannelSelectorModal.jsx b/web/src/components/settings/ChannelSelectorModal.jsx index 757b0e2f328b..6280d3045552 100644 --- a/web/src/components/settings/ChannelSelectorModal.jsx +++ b/web/src/components/settings/ChannelSelectorModal.jsx @@ -32,6 +32,7 @@ import { Highlight, Select, Tag, + Button, } from '@douyinfe/semi-ui'; import { IconSearch } from '@douyinfe/semi-icons'; @@ -247,13 +248,22 @@ const ChannelSelectorModal = forwardRef( {t('选择同步渠道')} } size={isMobile ? 'full-width' : 'large'} keepDOM lazyRender={false} + footer={ +
+ + + + +
+ } > { + + + + +
+ } onCancel={() => setShowMigrateModal(false)} - confirmLoading={loading} - okText='确认迁移' - cancelText='取消' >

检测到旧版本的配置数据,是否要迁移到新的配置格式?

diff --git a/web/src/components/settings/OtherSetting.jsx b/web/src/components/settings/OtherSetting.jsx index f8e0b53756a1..0be97b102e1f 100644 --- a/web/src/components/settings/OtherSetting.jsx +++ b/web/src/components/settings/OtherSetting.jsx @@ -502,18 +502,21 @@ const OtherSetting = () => { title={t('新版本') + ':' + updateData.tag_name} visible={showUpdateModal} onCancel={() => setShowUpdateModal(false)} - footer={[ - , - ]} + footer={ +

+ +
+ } >
diff --git a/web/src/components/settings/SystemSetting.jsx b/web/src/components/settings/SystemSetting.jsx index c0529ea19e1c..2ba1627d0175 100644 --- a/web/src/components/settings/SystemSetting.jsx +++ b/web/src/components/settings/SystemSetting.jsx @@ -31,6 +31,7 @@ import { Card, Radio, Select, + Space, } from '@douyinfe/semi-ui'; const { Text } = Typography; import { @@ -1624,13 +1625,34 @@ const SystemSetting = () => { + + + + +
+ } onCancel={() => { setShowPasswordLoginConfirmModal(false); formApiRef.current.setValue('PasswordLoginEnabled', true); }} - okText={t('确认')} - cancelText={t('取消')} >

{t( diff --git a/web/src/components/settings/personal/components/TwoFASetting.jsx b/web/src/components/settings/personal/components/TwoFASetting.jsx index 10ee2373f288..b21123507c15 100644 --- a/web/src/components/settings/personal/components/TwoFASetting.jsx +++ b/web/src/components/settings/personal/components/TwoFASetting.jsx @@ -252,7 +252,7 @@ const TwoFASetting = ({ t }) => { // 渲染设置模态框footer const renderSetupModalFooter = () => { return ( - <> + {currentStep > 0 && ( )} - + ); }; // 渲染禁用模态框footer const renderDisableModalFooter = () => { return ( - <> + - + ); }; @@ -339,7 +339,7 @@ const TwoFASetting = ({ t }) => { } return ( - <> + - + ); }; diff --git a/web/src/components/settings/personal/modals/AccountDeleteModal.jsx b/web/src/components/settings/personal/modals/AccountDeleteModal.jsx index b2bc40f6935d..a6cd7225b6d1 100644 --- a/web/src/components/settings/personal/modals/AccountDeleteModal.jsx +++ b/web/src/components/settings/personal/modals/AccountDeleteModal.jsx @@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com */ import React from 'react'; -import { Banner, Input, Modal, Typography } from '@douyinfe/semi-ui'; +import { Banner, Input, Modal, Typography, Button, Space } from '@douyinfe/semi-ui'; import { IconDelete, IconUser } from '@douyinfe/semi-icons'; import Turnstile from 'react-turnstile'; @@ -44,10 +44,21 @@ const AccountDeleteModal = ({ } visible={showAccountDeleteModal} onCancel={() => setShowAccountDeleteModal(false)} - onOk={deleteAccount} size={'small'} centered={true} className='modern-modal' + footer={ +

+ + + + +
+ } >
setShowChangePasswordModal(false)} - onOk={changePassword} size={'small'} centered={true} className='modern-modal' + footer={ +
+ + + + +
+ } >
diff --git a/web/src/components/settings/personal/modals/EmailBindModal.jsx b/web/src/components/settings/personal/modals/EmailBindModal.jsx index 1cd1c735d1ad..8fd8fb34328f 100644 --- a/web/src/components/settings/personal/modals/EmailBindModal.jsx +++ b/web/src/components/settings/personal/modals/EmailBindModal.jsx @@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com */ import React from 'react'; -import { Button, Input, Modal } from '@douyinfe/semi-ui'; +import { Button, Input, Modal, Space } from '@douyinfe/semi-ui'; import { IconMail, IconKey } from '@douyinfe/semi-icons'; import Turnstile from 'react-turnstile'; @@ -47,11 +47,22 @@ const EmailBindModal = ({ } visible={showEmailBindModal} onCancel={() => setShowEmailBindModal(false)} - onOk={bindEmail} size={'small'} centered={true} maskClosable={false} className='modern-modal' + footer={ +
+ + + + +
+ } >
diff --git a/web/src/components/table/channels/modals/BatchTagModal.jsx b/web/src/components/table/channels/modals/BatchTagModal.jsx index 16bb64b085ff..b09f8c871b0e 100644 --- a/web/src/components/table/channels/modals/BatchTagModal.jsx +++ b/web/src/components/table/channels/modals/BatchTagModal.jsx @@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com */ import React from 'react'; -import { Modal, Input, Typography } from '@douyinfe/semi-ui'; +import { Modal, Input, Typography, Space, Button } from '@douyinfe/semi-ui'; const BatchTagModal = ({ showBatchSetTag, @@ -33,12 +33,21 @@ const BatchTagModal = ({ setShowBatchSetTag(false)} maskClosable={false} centered={true} size='small' className='!rounded-lg' + footer={ +
+ + + + +
+ } >
{t('请输入要设置的标签名称')} diff --git a/web/src/components/table/channels/modals/CodexOAuthModal.jsx b/web/src/components/table/channels/modals/CodexOAuthModal.jsx index 7f3f349b20a4..da79294bf6fd 100644 --- a/web/src/components/table/channels/modals/CodexOAuthModal.jsx +++ b/web/src/components/table/channels/modals/CodexOAuthModal.jsx @@ -116,19 +116,21 @@ const CodexOAuthModal = ({ visible, onCancel, onSuccess }) => { closeOnEsc width={720} footer={ - - - - +
+ + + + +
} > diff --git a/web/src/components/table/channels/modals/EditChannelModal.jsx b/web/src/components/table/channels/modals/EditChannelModal.jsx index 141cd5626485..3daf2c0b0940 100644 --- a/web/src/components/table/channels/modals/EditChannelModal.jsx +++ b/web/src/components/table/channels/modals/EditChannelModal.jsx @@ -3419,9 +3419,11 @@ const EditChannelModal = (props) => { visible={keyDisplayState.showModal} onCancel={resetKeyDisplayState} footer={ - +
+ +
} width={700} style={{ maxWidth: '90vw' }} diff --git a/web/src/components/table/channels/modals/ModelSelectModal.jsx b/web/src/components/table/channels/modals/ModelSelectModal.jsx index b38580b66f5f..ed0fe9e4983b 100644 --- a/web/src/components/table/channels/modals/ModelSelectModal.jsx +++ b/web/src/components/table/channels/modals/ModelSelectModal.jsx @@ -29,6 +29,8 @@ import { Tabs, Collapse, Tooltip, + Button, + Space, } from '@douyinfe/semi-ui'; import { IllustrationNoResult, @@ -326,14 +328,21 @@ const ModelSelectModal = ({
} visible={visible} - onOk={handleOk} onCancel={onCancel} - okText={t('确定')} - cancelText={t('取消')} size={isMobile ? 'full-width' : 'large'} closeOnEsc maskClosable centered + footer={ +
+ + + + +
+ } > } diff --git a/web/src/components/table/channels/modals/ModelTestModal.jsx b/web/src/components/table/channels/modals/ModelTestModal.jsx index 47aa66cbed83..b01cee630828 100644 --- a/web/src/components/table/channels/modals/ModelTestModal.jsx +++ b/web/src/components/table/channels/modals/ModelTestModal.jsx @@ -26,6 +26,7 @@ import { Tag, Typography, Select, + Space, } from '@douyinfe/semi-ui'; import { IconSearch } from '@douyinfe/semi-icons'; import { copy, showError, showInfo, showSuccess } from '../../../../helpers'; @@ -228,27 +229,29 @@ const ModelTestModal = ({ footer={ hasChannel ? (
- {isBatchTesting ? ( - - ) : ( - + ) : ( + + )} + - )} - +
) : null } diff --git a/web/src/components/table/channels/modals/OllamaModelModal.jsx b/web/src/components/table/channels/modals/OllamaModelModal.jsx index 684d2eb46538..89341e50e447 100644 --- a/web/src/components/table/channels/modals/OllamaModelModal.jsx +++ b/web/src/components/table/channels/modals/OllamaModelModal.jsx @@ -535,9 +535,11 @@ const OllamaModelModal = ({ width={720} style={{ maxWidth: '95vw' }} footer={ - +
+ +
} > diff --git a/web/src/components/table/channels/modals/SingleModelSelectModal.jsx b/web/src/components/table/channels/modals/SingleModelSelectModal.jsx index fdeb29152cae..3ea23d49c423 100644 --- a/web/src/components/table/channels/modals/SingleModelSelectModal.jsx +++ b/web/src/components/table/channels/modals/SingleModelSelectModal.jsx @@ -21,11 +21,13 @@ import React, { useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useIsMobile } from '../../../../hooks/common/useIsMobile'; import { + Button, Collapse, Empty, Input, Modal, Radio, + Space, Typography, } from '@douyinfe/semi-ui'; import { @@ -119,11 +121,22 @@ const SingleModelSelectModal = ({
} visible={visible} - onOk={() => onConfirm?.(selectedModel)} onCancel={onCancel} - okText={t('确定')} - cancelText={t('取消')} - okButtonProps={{ disabled: !selectedModel }} + footer={ +
+ + + + +
+ } size={isMobile ? 'full-width' : 'large'} closeOnEsc maskClosable diff --git a/web/src/components/table/mj-logs/modals/ContentModal.jsx b/web/src/components/table/mj-logs/modals/ContentModal.jsx index 8197ba88dd89..6757b08be38b 100644 --- a/web/src/components/table/mj-logs/modals/ContentModal.jsx +++ b/web/src/components/table/mj-logs/modals/ContentModal.jsx @@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com */ import React from 'react'; -import { Modal, ImagePreview } from '@douyinfe/semi-ui'; +import { Modal, ImagePreview, Button, Space } from '@douyinfe/semi-ui'; const ContentModal = ({ isModalOpen, @@ -33,11 +33,19 @@ const ContentModal = ({ {/* Text Content Modal */} setIsModalOpen(false)} onCancel={() => setIsModalOpen(false)} closable={null} bodyStyle={{ height: '400px', overflow: 'auto' }} width={800} + footer={ +
+ + + +
+ } >

{modalContent}

diff --git a/web/src/components/table/model-deployments/modals/ConfirmationDialog.jsx b/web/src/components/table/model-deployments/modals/ConfirmationDialog.jsx index 5e90b153bbcb..8a8673664c85 100644 --- a/web/src/components/table/model-deployments/modals/ConfirmationDialog.jsx +++ b/web/src/components/table/model-deployments/modals/ConfirmationDialog.jsx @@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com */ import React, { useState, useEffect } from 'react'; -import { Modal, Typography, Input } from '@douyinfe/semi-ui'; +import { Modal, Typography, Input, Button, Space } from '@douyinfe/semi-ui'; const { Text } = Typography; @@ -60,15 +60,23 @@ const ConfirmationDialog = ({ title={title} visible={visible} onCancel={handleCancel} - onOk={handleConfirm} - okText={t('确认')} - cancelText={t('取消')} - okButtonProps={{ - disabled: !isConfirmed, - type: type === 'danger' ? 'danger' : 'primary', - loading, - }} width={480} + footer={ +
+ + + + +
+ } >
diff --git a/web/src/components/table/model-deployments/modals/CreateDeploymentModal.jsx b/web/src/components/table/model-deployments/modals/CreateDeploymentModal.jsx index 35887b15b83e..6dc3e423440f 100644 --- a/web/src/components/table/model-deployments/modals/CreateDeploymentModal.jsx +++ b/web/src/components/table/model-deployments/modals/CreateDeploymentModal.jsx @@ -838,12 +838,23 @@ const CreateDeploymentModal = ({ visible, onCancel, onSuccess, t }) => { title={t('新建容器部署')} visible={visible} onCancel={onCancel} - onOk={() => formApi?.submitForm()} - okText={t('创建')} - cancelText={t('取消')} width={800} - confirmLoading={submitting} style={{ top: 20 }} + footer={ +
+ + + + +
+ } >
+ + + + +
+ } >
diff --git a/web/src/components/table/model-deployments/modals/UpdateConfigModal.jsx b/web/src/components/table/model-deployments/modals/UpdateConfigModal.jsx index 8d30415dbebe..5262fe4e069f 100644 --- a/web/src/components/table/model-deployments/modals/UpdateConfigModal.jsx +++ b/web/src/components/table/model-deployments/modals/UpdateConfigModal.jsx @@ -202,12 +202,23 @@ const UpdateConfigModal = ({ visible, onCancel, deployment, onSuccess, t }) => { } visible={visible} onCancel={handleCancel} - onOk={handleUpdate} - okText={t('更新配置')} - cancelText={t('取消')} - confirmLoading={loading} width={700} className='update-config-modal' + footer={ +
+ + + + +
+ } >
{/* Container Info */} diff --git a/web/src/components/table/model-pricing/modal/components/FilterModalFooter.jsx b/web/src/components/table/model-pricing/modal/components/FilterModalFooter.jsx index d99d62fec448..49f77f32f7ed 100644 --- a/web/src/components/table/model-pricing/modal/components/FilterModalFooter.jsx +++ b/web/src/components/table/model-pricing/modal/components/FilterModalFooter.jsx @@ -22,7 +22,7 @@ import { Button } from '@douyinfe/semi-ui'; const FilterModalFooter = ({ onReset, onConfirm, t }) => { return ( -
+
diff --git a/web/src/components/table/models/modals/EditVendorModal.jsx b/web/src/components/table/models/modals/EditVendorModal.jsx index 977f7d9fc2e7..840681d85dcd 100644 --- a/web/src/components/table/models/modals/EditVendorModal.jsx +++ b/web/src/components/table/models/modals/EditVendorModal.jsx @@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com */ import React, { useState, useRef, useEffect } from 'react'; -import { Modal, Form, Col, Row } from '@douyinfe/semi-ui'; +import { Modal, Form, Col, Row, Button, Space } from '@douyinfe/semi-ui'; import { API, showError, showSuccess } from '../../../../helpers'; import { Typography } from '@douyinfe/semi-ui'; import { IconLink } from '@douyinfe/semi-icons'; @@ -120,10 +120,24 @@ const EditVendorModal = ({ visible, handleClose, refresh, editingVendor }) => { formApiRef.current?.submitForm()} onCancel={handleCancel} confirmLoading={loading} size={isMobile ? 'full-width' : 'small'} + footer={ +
+ + + + +
+ } > { } visible={visible} onCancel={onClose} - footer={null} size={isMobile ? 'full-width' : 'medium'} className='!rounded-lg' + footer={ +
+ + + +
+ } > {missingModels.length === 0 && !loading ? ( diff --git a/web/src/components/table/models/modals/UpstreamConflictModal.jsx b/web/src/components/table/models/modals/UpstreamConflictModal.jsx index 3993f6dc7a85..48524fda715d 100644 --- a/web/src/components/table/models/modals/UpstreamConflictModal.jsx +++ b/web/src/components/table/models/modals/UpstreamConflictModal.jsx @@ -27,6 +27,8 @@ import { Tag, Popover, Input, + Button, + Space, } from '@douyinfe/semi-ui'; import { MousePointerClick } from 'lucide-react'; import { useIsMobile } from '../../../../hooks/common/useIsMobile'; @@ -267,11 +269,22 @@ const UpstreamConflictModal = ({ title={t('选择要覆盖的冲突项')} visible={visible} onCancel={onClose} - onOk={handleOk} - confirmLoading={loading} - okText={t('应用覆盖')} - cancelText={t('取消')} width={isMobile ? '100%' : 1000} + footer={ +
+ + + + +
+ } > {dataSource.length === 0 ? ( diff --git a/web/src/components/table/redemptions/modals/DeleteRedemptionModal.jsx b/web/src/components/table/redemptions/modals/DeleteRedemptionModal.jsx index 9c08413f7e11..a73dcb7e0ff2 100644 --- a/web/src/components/table/redemptions/modals/DeleteRedemptionModal.jsx +++ b/web/src/components/table/redemptions/modals/DeleteRedemptionModal.jsx @@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com */ import React from 'react'; -import { Modal } from '@douyinfe/semi-ui'; +import { Modal, Button, Space } from '@douyinfe/semi-ui'; import { REDEMPTION_ACTIONS } from '../../../../constants/redemption.constants'; const DeleteRedemptionModal = ({ @@ -47,8 +47,17 @@ const DeleteRedemptionModal = ({ title={t('确定是否要删除此兑换码?')} visible={visible} onCancel={onCancel} - onOk={handleConfirm} type='warning' + footer={ +
+ + + + +
+ } > {t('此修改将不可逆')}
diff --git a/web/src/components/table/task-logs/modals/ContentModal.jsx b/web/src/components/table/task-logs/modals/ContentModal.jsx index 88df4d8ceab2..50c64dd006f3 100644 --- a/web/src/components/table/task-logs/modals/ContentModal.jsx +++ b/web/src/components/table/task-logs/modals/ContentModal.jsx @@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com */ import React, { useState, useEffect } from 'react'; -import { Modal, Button, Typography, Spin } from '@douyinfe/semi-ui'; +import { Modal, Button, Typography, Spin, Space } from '@douyinfe/semi-ui'; import { IconExternalOpen, IconCopy } from '@douyinfe/semi-icons'; import { useTranslation } from 'react-i18next'; @@ -157,7 +157,6 @@ const ContentModal = ({ return ( setIsModalOpen(false)} onCancel={() => setIsModalOpen(false)} closable={null} bodyStyle={{ @@ -168,6 +167,19 @@ const ContentModal = ({ }} width={isVideo ? '90vw' : 800} style={isVideo ? { maxWidth: 960 } : undefined} + footer={ +
+ + + +
+ } > {isVideo ? ( renderVideoContent() diff --git a/web/src/components/table/tokens/TokensColumnDefs.jsx b/web/src/components/table/tokens/TokensColumnDefs.jsx index ce8eab807edf..e87243fae5ba 100644 --- a/web/src/components/table/tokens/TokensColumnDefs.jsx +++ b/web/src/components/table/tokens/TokensColumnDefs.jsx @@ -486,7 +486,17 @@ export const getTokensColumns = ({ render: (text, record, index) => { return (
- {record.expired_time === -1 ? t('永不过期') : renderTimestamp(text)} + {record.expired_time === -1 ? ( + record.duration > 0 ? ( + + {t('睡眠中')} + + ) : ( + t('永不过期') + ) + ) : ( + renderTimestamp(text) + )}
); }, diff --git a/web/src/components/table/tokens/modals/DeleteTokensModal.jsx b/web/src/components/table/tokens/modals/DeleteTokensModal.jsx index 21b04223f470..bf0f7b3f1407 100644 --- a/web/src/components/table/tokens/modals/DeleteTokensModal.jsx +++ b/web/src/components/table/tokens/modals/DeleteTokensModal.jsx @@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com */ import React from 'react'; -import { Modal } from '@douyinfe/semi-ui'; +import { Modal, Button, Space } from '@douyinfe/semi-ui'; const DeleteTokensModal = ({ visible, @@ -32,8 +32,17 @@ const DeleteTokensModal = ({ title={t('批量删除令牌')} visible={visible} onCancel={onCancel} - onOk={onConfirm} type='warning' + footer={ +
+ + + + +
+ } >
{t('确定要删除所选的 {{count}} 个令牌吗?', { diff --git a/web/src/components/table/tokens/modals/EditTokenModal.jsx b/web/src/components/table/tokens/modals/EditTokenModal.jsx index fce4820146c7..cc133864773d 100644 --- a/web/src/components/table/tokens/modals/EditTokenModal.jsx +++ b/web/src/components/table/tokens/modals/EditTokenModal.jsx @@ -41,6 +41,8 @@ import { Form, Col, Row, + Radio, + InputGroup, } from '@douyinfe/semi-ui'; import { IconCreditCard, @@ -75,6 +77,7 @@ const EditTokenModal = (props) => { group: '', cross_group_retry: false, tokenCount: 1, + is_sleep_mode: false, // 新增睡眠模式字段 }); const handleCancel = () => { @@ -154,16 +157,31 @@ const EditTokenModal = (props) => { let res = await API.get(`/api/token/${props.editingToken.id}`); const { success, message, data } = res.data; if (success) { - if (data.expired_time !== -1) { - data.expired_time = timestamp2string(data.expired_time); - } if (data.model_limits !== '') { data.model_limits = data.model_limits.split(','); } else { data.model_limits = []; } + + // 反推 UI 状态 + let uiValues = { ...getInitValues(), ...data }; + + if (data.expired_time === -1 && data.duration > 0) { + // 睡眠模式:expired_time 为 -1,但有 duration + uiValues.is_sleep_mode = true; + // 将 duration 转换为未来的时间点显示,以便用户直观看到时长 + let now = Math.floor(Date.now() / 1000); + uiValues.expired_time = timestamp2string(now + data.duration); + } else { + // 普通模式 + uiValues.is_sleep_mode = false; + if (data.expired_time !== -1) { + uiValues.expired_time = timestamp2string(data.expired_time); + } + } + if (formApiRef.current) { - formApiRef.current.setValues({ ...getInitValues(), ...data }); + formApiRef.current.setValues(uiValues); } } else { showError(message); @@ -205,76 +223,104 @@ const EditTokenModal = (props) => { return result; }; - const submit = async (values) => { - setLoading(true); - if (isEdit) { - let { tokenCount: _tc, ...localInputs } = values; - localInputs.remain_quota = parseInt(localInputs.remain_quota); - if (localInputs.expired_time !== -1) { - let time = Date.parse(localInputs.expired_time); - if (isNaN(time)) { - showError(t('过期时间格式错误!')); - setLoading(false); - return; - } - localInputs.expired_time = Math.ceil(time / 1000); + const processSubmitData = (values) => { + let { + tokenCount: _tc, + is_sleep_mode, + ...localInputs + } = values; + + localInputs.remain_quota = parseInt(localInputs.remain_quota); + + // 处理过期时间逻辑 + if (localInputs.expired_time !== -1) { + let time = Date.parse(localInputs.expired_time); + if (isNaN(time)) { + throw new Error(t('过期时间格式错误!')); } - localInputs.model_limits = localInputs.model_limits.join(','); - localInputs.model_limits_enabled = localInputs.model_limits.length > 0; - let res = await API.put(`/api/token/`, { - ...localInputs, - id: parseInt(props.editingToken.id), - }); - const { success, message } = res.data; - if (success) { - showSuccess(t('令牌更新成功!')); - props.refresh(); - props.handleClose(); + let timestamp = Math.ceil(time / 1000); + + if (is_sleep_mode) { + // 睡眠模式:计算时长,设置 expired_time 为 -1 + let now = Math.floor(Date.now() / 1000); + let duration = timestamp - now; + if (duration <= 0) { + throw new Error(t('睡眠模式下,请选择未来的时间以设置有效时长!')); + } + localInputs.expired_time = -1; + localInputs.duration = duration; } else { - showError(t(message)); + // 普通模式:直接设置 expired_time + localInputs.expired_time = timestamp; + localInputs.duration = 0; } } else { - const count = parseInt(values.tokenCount, 10) || 1; - let successCount = 0; - for (let i = 0; i < count; i++) { - let { tokenCount: _tc, ...localInputs } = values; - const baseName = - values.name.trim() === '' ? 'default' : values.name.trim(); - if (i !== 0 || values.name.trim() === '') { - localInputs.name = `${baseName}-${generateRandomSuffix()}`; + // 选择了永不过期 + localInputs.expired_time = -1; + localInputs.duration = 0; + } + + localInputs.model_limits = localInputs.model_limits.join(','); + localInputs.model_limits_enabled = localInputs.model_limits.length > 0; + + return localInputs; + }; + + const submit = async (values) => { + setLoading(true); + + try { + if (isEdit) { + let localInputs = processSubmitData(values); + let res = await API.put(`/api/token/`, { + ...localInputs, + id: parseInt(props.editingToken.id), + }); + const { success, message } = res.data; + if (success) { + showSuccess(t('令牌更新成功!')); + props.refresh(); + props.handleClose(); } else { - localInputs.name = baseName; + showError(t(message)); } - localInputs.remain_quota = parseInt(localInputs.remain_quota); + } else { + const count = parseInt(values.tokenCount, 10) || 1; + let successCount = 0; + let errorMsg = ''; + + for (let i = 0; i < count; i++) { + let localInputs = processSubmitData(values); + const baseName = values.name.trim() === '' ? 'default' : values.name.trim(); + if (i !== 0 || values.name.trim() === '') { + localInputs.name = `${baseName}-${generateRandomSuffix()}`; + } else { + localInputs.name = baseName; + } - if (localInputs.expired_time !== -1) { - let time = Date.parse(localInputs.expired_time); - if (isNaN(time)) { - showError(t('过期时间格式错误!')); - setLoading(false); + let res = await API.post(`/api/token/`, localInputs); + const { success, message } = res.data; + if (success) { + successCount++; + } else { + errorMsg = message; break; } - localInputs.expired_time = Math.ceil(time / 1000); } - localInputs.model_limits = localInputs.model_limits.join(','); - localInputs.model_limits_enabled = localInputs.model_limits.length > 0; - let res = await API.post(`/api/token/`, localInputs); - const { success, message } = res.data; - if (success) { - successCount++; + + if (successCount > 0) { + showSuccess(t('令牌创建成功,请在列表页面点击复制获取令牌!')); + props.refresh(); + props.handleClose(); } else { - showError(t(message)); - break; + showError(t(errorMsg)); } } - if (successCount > 0) { - showSuccess(t('令牌创建成功,请在列表页面点击复制获取令牌!')); - props.refresh(); - props.handleClose(); - } + } catch (error) { + showError(error.message); } + setLoading(false); - formApiRef.current?.setValues(getInitValues()); }; return ( @@ -403,9 +449,7 @@ const EditTokenModal = (props) => { { required: true, message: t('请选择过期时间') }, { validator: (rule, value) => { - // 允许 -1 表示永不过期,也允许空值在必填校验时被拦截 - if (value === -1 || !value) - return Promise.resolve(); + if (value === -1 || !value) return Promise.resolve(); const time = Date.parse(value); if (isNaN(time)) { return Promise.reject(t('过期时间格式错误!')); @@ -428,10 +472,24 @@ const EditTokenModal = (props) => { + + + + + + {!isEdit && ( + + + + +
+ } > {t('相当于删除用户,此修改将不可逆')}
diff --git a/web/src/components/table/users/modals/DemoteUserModal.jsx b/web/src/components/table/users/modals/DemoteUserModal.jsx index 4168ca7c8b20..68eb83229617 100644 --- a/web/src/components/table/users/modals/DemoteUserModal.jsx +++ b/web/src/components/table/users/modals/DemoteUserModal.jsx @@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com */ import React from 'react'; -import { Modal } from '@douyinfe/semi-ui'; +import { Modal, Button, Space } from '@douyinfe/semi-ui'; const DemoteUserModal = ({ visible, onCancel, onConfirm, user, t }) => { return ( @@ -26,8 +26,17 @@ const DemoteUserModal = ({ visible, onCancel, onConfirm, user, t }) => { title={t('确定要降级此用户吗?')} visible={visible} onCancel={onCancel} - onOk={onConfirm} type='warning' + footer={ +
+ + + + +
+ } > {t('此操作将降低用户的权限级别')} diff --git a/web/src/components/table/users/modals/EnableDisableUserModal.jsx b/web/src/components/table/users/modals/EnableDisableUserModal.jsx index 0e8cb184b995..13aea2dd2ec1 100644 --- a/web/src/components/table/users/modals/EnableDisableUserModal.jsx +++ b/web/src/components/table/users/modals/EnableDisableUserModal.jsx @@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com */ import React from 'react'; -import { Modal } from '@douyinfe/semi-ui'; +import { Modal, Button, Space } from '@douyinfe/semi-ui'; const EnableDisableUserModal = ({ visible, @@ -35,8 +35,17 @@ const EnableDisableUserModal = ({ title={isDisable ? t('确定要禁用此用户吗?') : t('确定要启用此用户吗?')} visible={visible} onCancel={onCancel} - onOk={onConfirm} type='warning' + footer={ +
+ + + + +
+ } > {isDisable ? t('此操作将禁用用户账户') : t('此操作将启用用户账户')} diff --git a/web/src/components/table/users/modals/PromoteUserModal.jsx b/web/src/components/table/users/modals/PromoteUserModal.jsx index 1490982b6536..6049d2e0defe 100644 --- a/web/src/components/table/users/modals/PromoteUserModal.jsx +++ b/web/src/components/table/users/modals/PromoteUserModal.jsx @@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com */ import React from 'react'; -import { Modal } from '@douyinfe/semi-ui'; +import { Modal, Button, Space } from '@douyinfe/semi-ui'; const PromoteUserModal = ({ visible, onCancel, onConfirm, user, t }) => { return ( @@ -26,8 +26,17 @@ const PromoteUserModal = ({ visible, onCancel, onConfirm, user, t }) => { title={t('确定要提升此用户吗?')} visible={visible} onCancel={onCancel} - onOk={onConfirm} type='warning' + footer={ +
+ + + + +
+ } > {t('此操作将提升用户的权限级别')} diff --git a/web/src/components/table/users/modals/ResetPasskeyModal.jsx b/web/src/components/table/users/modals/ResetPasskeyModal.jsx index 99f81c749388..ac5a1aed39a3 100644 --- a/web/src/components/table/users/modals/ResetPasskeyModal.jsx +++ b/web/src/components/table/users/modals/ResetPasskeyModal.jsx @@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com */ import React from 'react'; -import { Modal } from '@douyinfe/semi-ui'; +import { Modal, Button, Space } from '@douyinfe/semi-ui'; const ResetPasskeyModal = ({ visible, onCancel, onConfirm, user, t }) => { return ( @@ -26,8 +26,17 @@ const ResetPasskeyModal = ({ visible, onCancel, onConfirm, user, t }) => { title={t('确认重置 Passkey')} visible={visible} onCancel={onCancel} - onOk={onConfirm} type='warning' + footer={ +
+ + + + +
+ } > {t('此操作将解绑用户当前的 Passkey,下次登录需要重新注册。')}{' '} {user?.username diff --git a/web/src/components/table/users/modals/ResetTwoFAModal.jsx b/web/src/components/table/users/modals/ResetTwoFAModal.jsx index 64b42926b1d2..8526463d97c4 100644 --- a/web/src/components/table/users/modals/ResetTwoFAModal.jsx +++ b/web/src/components/table/users/modals/ResetTwoFAModal.jsx @@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com */ import React from 'react'; -import { Modal } from '@douyinfe/semi-ui'; +import { Modal, Button, Space } from '@douyinfe/semi-ui'; const ResetTwoFAModal = ({ visible, onCancel, onConfirm, user, t }) => { return ( @@ -26,8 +26,17 @@ const ResetTwoFAModal = ({ visible, onCancel, onConfirm, user, t }) => { title={t('确认重置两步验证')} visible={visible} onCancel={onCancel} - onOk={onConfirm} type='warning' + footer={ +
+ + + + +
+ } > {t( '此操作将禁用该用户当前的两步验证配置,下次登录将不再强制输入验证码,直到用户重新启用。', diff --git a/web/src/components/topup/modals/PaymentConfirmModal.jsx b/web/src/components/topup/modals/PaymentConfirmModal.jsx index 8bd5455c7f84..f4ffccd1a1c3 100644 --- a/web/src/components/topup/modals/PaymentConfirmModal.jsx +++ b/web/src/components/topup/modals/PaymentConfirmModal.jsx @@ -18,7 +18,7 @@ For commercial licensing, please contact support@quantumnous.com */ import React from 'react'; -import { Modal, Typography, Card, Skeleton } from '@douyinfe/semi-ui'; +import { Modal, Typography, Card, Skeleton, Button, Space } from '@douyinfe/semi-ui'; import { SiAlipay, SiWechat, SiStripe } from 'react-icons/si'; import { CreditCard } from 'lucide-react'; @@ -53,12 +53,25 @@ const PaymentConfirmModal = ({
} visible={open} - onOk={onlineTopUp} onCancel={handleCancel} maskClosable={false} size='small' centered - confirmLoading={confirmLoading} + footer={ +
+ + + + +
+ } >
diff --git a/web/src/index.jsx b/web/src/index.jsx index 5162b0cbdd2a..bf17c0433f82 100644 --- a/web/src/index.jsx +++ b/web/src/index.jsx @@ -20,7 +20,6 @@ For commercial licensing, please contact support@quantumnous.com import React from 'react'; import ReactDOM from 'react-dom/client'; import { BrowserRouter } from 'react-router-dom'; -import '@douyinfe/semi-ui/dist/css/semi.css'; import { UserProvider } from './context/User'; import 'react-toastify/dist/ReactToastify.css'; import { StatusProvider } from './context/Status'; diff --git a/web/src/pages/Setting/Chat/SettingsChats.jsx b/web/src/pages/Setting/Chat/SettingsChats.jsx index f7f309ac9868..04fe2d23b704 100644 --- a/web/src/pages/Setting/Chat/SettingsChats.jsx +++ b/web/src/pages/Setting/Chat/SettingsChats.jsx @@ -473,9 +473,18 @@ export default function SettingsChats(props) { + + + + +
+ } > (modalFormRef.current = api)}> { setShowApiModal(false)} - okText={t('保存')} - cancelText={t('取消')} - confirmLoading={modalLoading} + footer={ +
+ + + + +
+ } > { { setShowDeleteModal(false); setDeletingApi(null); }} - okText={t('确认删除')} - cancelText={t('取消')} + footer={ +
+ + + + +
+ } type='warning' - okButtonProps={{ - type: 'danger', - theme: 'solid', - }} > {t('确定要删除此API信息吗?')}
diff --git a/web/src/pages/Setting/Dashboard/SettingsAnnouncements.jsx b/web/src/pages/Setting/Dashboard/SettingsAnnouncements.jsx index c61102f15039..2b98bd8675d1 100644 --- a/web/src/pages/Setting/Dashboard/SettingsAnnouncements.jsx +++ b/web/src/pages/Setting/Dashboard/SettingsAnnouncements.jsx @@ -520,11 +520,24 @@ const SettingsAnnouncements = ({ options, refresh }) => { setShowAnnouncementModal(false)} - okText={t('保存')} - cancelText={t('取消')} - confirmLoading={modalLoading} + footer={ +
+ + + + +
+ } > { { setShowDeleteModal(false); setDeletingAnnouncement(null); }} - okText={t('确认删除')} - cancelText={t('取消')} + footer={ +
+ + + + +
+ } type='warning' - okButtonProps={{ - type: 'danger', - theme: 'solid', - }} > {t('确定要删除此公告吗?')}
@@ -604,16 +631,32 @@ const SettingsAnnouncements = ({ options, refresh }) => { { - // 将内容同步到表单 - if (formApiRef.current) { - formApiRef.current.setValue('content', announcementForm.content); - } - setShowContentModal(false); - }} onCancel={() => setShowContentModal(false)} - okText={t('确定')} - cancelText={t('取消')} + footer={ +
+ + + + +
+ } width={800} >