Compare commits
8 Commits
v0.2.5
...
v0.2.6-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d267211ee7 | ||
|
|
570b3bc71c | ||
|
|
225176aae9 | ||
|
|
443a22b75d | ||
|
|
b44f0519a0 | ||
|
|
4a0e81fe83 | ||
|
|
976c29ea9f | ||
|
|
926951ee03 |
61
.github/workflows/docker-image-arm64.yml
vendored
61
.github/workflows/docker-image-arm64.yml
vendored
@@ -1,61 +0,0 @@
|
|||||||
name: Publish Docker image (arm64)
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- '*'
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
name:
|
|
||||||
description: 'reason'
|
|
||||||
required: false
|
|
||||||
jobs:
|
|
||||||
push_to_registries:
|
|
||||||
name: Push Docker image to multiple registries
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
packages: write
|
|
||||||
contents: read
|
|
||||||
steps:
|
|
||||||
- name: Check out the repo
|
|
||||||
uses: actions/checkout@v3
|
|
||||||
|
|
||||||
- name: Save version info
|
|
||||||
run: |
|
|
||||||
git describe --tags > VERSION
|
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v2
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v2
|
|
||||||
|
|
||||||
- name: Log in to Docker Hub
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Log in to the Container registry
|
|
||||||
uses: docker/login-action@v2
|
|
||||||
with:
|
|
||||||
registry: ghcr.io
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v4
|
|
||||||
with:
|
|
||||||
images: |
|
|
||||||
justsong/one-api
|
|
||||||
ghcr.io/${{ github.repository }}
|
|
||||||
|
|
||||||
- name: Build and push Docker images
|
|
||||||
uses: docker/build-push-action@v3
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
26
README.md
26
README.md
@@ -43,28 +43,28 @@ _✨ All in one 的 OpenAI 接口,整合各种 API 访问方式,开箱即用
|
|||||||
## 功能
|
## 功能
|
||||||
1. 支持多种 API 访问渠道,欢迎 PR 或提 issue 添加更多渠道:
|
1. 支持多种 API 访问渠道,欢迎 PR 或提 issue 添加更多渠道:
|
||||||
+ [x] OpenAI 官方通道
|
+ [x] OpenAI 官方通道
|
||||||
+ [x] Azure OpenAI API
|
+ [x] **Azure OpenAI API**
|
||||||
+ [x] [API2D](https://api2d.com/r/197971)
|
+ [x] [API2D](https://api2d.com/r/197971)
|
||||||
+ [x] [CloseAI](https://console.openai-asia.com)
|
+ [x] [CloseAI](https://console.openai-asia.com)
|
||||||
+ [x] [OpenAI-SB](https://openai-sb.com)
|
+ [x] [OpenAI-SB](https://openai-sb.com)
|
||||||
+ [x] [OpenAI Max](https://openaimax.com)
|
+ [x] [OpenAI Max](https://openaimax.com)
|
||||||
+ [x] [OhMyGPT](https://www.ohmygpt.com)
|
+ [x] [OhMyGPT](https://www.ohmygpt.com)
|
||||||
+ [x] 自定义渠道:例如使用自行搭建的 OpenAI 代理
|
+ [x] 自定义渠道:例如使用自行搭建的 OpenAI 代理
|
||||||
2. 支持通过负载均衡的方式访问多个渠道。
|
2. 支持通过**负载均衡**的方式访问多个渠道。
|
||||||
3. 支持单个访问渠道设置多个 API Key,利用起来你的多个 API Key。
|
3. 支持 **stream 模式**,可以通过流式传输实现打字机效果。
|
||||||
4. 支持 HTTP SSE,可以通过流式传输实现打字机效果。
|
4. 支持**令牌管理**,设置令牌的过期时间和使用次数。
|
||||||
5. 支持设置令牌的过期时间和使用次数。
|
5. 支持**兑换码管理**,支持批量生成和导出兑换码,可使用兑换码为令牌进行充值。
|
||||||
6. 支持批量生成和导出兑换码,可使用兑换码为令牌进行充值。
|
6. 支持**通道管理**,批量创建通道。
|
||||||
7. 支持批量创建通道。
|
7. 支持发布公告,设置充值链接,设置新用户初始额度。
|
||||||
8. 支持发布公告,自定义关于页面,设置充值链接,自定义页脚。
|
8. 支持丰富的**自定义**设置,
|
||||||
9. 支持自定义首页,可以选择使用 HTML & Markdown 代码进行自定义,或者使用一个单独的网页通过 iframe 嵌入。
|
1. 支持自定义系统名称,logo 以及页脚。
|
||||||
10. 支持通过系统访问令牌访问管理 API。
|
2. 支持自定义首页和关于页面,可以选择使用 HTML & Markdown 代码进行自定义,或者使用一个单独的网页通过 iframe 嵌入。
|
||||||
11. 多种用户登录注册方式:
|
9. 支持通过系统访问令牌访问管理 API。
|
||||||
|
10. 支持用户管理,支持**多种用户登录注册方式**:
|
||||||
+ 邮箱登录注册以及通过邮箱进行密码重置。
|
+ 邮箱登录注册以及通过邮箱进行密码重置。
|
||||||
+ [GitHub 开放授权](https://github.com/settings/applications/new)。
|
+ [GitHub 开放授权](https://github.com/settings/applications/new)。
|
||||||
+ 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。
|
+ 微信公众号授权(需要额外部署 [WeChat Server](https://github.com/songquanpeng/wechat-server))。
|
||||||
12. 支持用户管理,支持为新用户设置初始配额。。
|
11. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式。
|
||||||
13. 未来其他大模型开放 API 后,将第一时间支持,并将其封装成同样的 API 访问方式。
|
|
||||||
|
|
||||||
## 部署
|
## 部署
|
||||||
### 基于 Docker 进行部署
|
### 基于 Docker 进行部署
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ var Version = "v0.0.0" // this hard coding will be replaced automatic
|
|||||||
var SystemName = "One API"
|
var SystemName = "One API"
|
||||||
var ServerAddress = "http://localhost:3000"
|
var ServerAddress = "http://localhost:3000"
|
||||||
var Footer = ""
|
var Footer = ""
|
||||||
|
var Logo = ""
|
||||||
var TopUpLink = ""
|
var TopUpLink = ""
|
||||||
|
|
||||||
var UsingSQLite = false
|
var UsingSQLite = false
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
package controller
|
package controller
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"net/http"
|
"net/http"
|
||||||
"one-api/common"
|
"one-api/common"
|
||||||
"one-api/model"
|
"one-api/model"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetAllChannels(c *gin.Context) {
|
func GetAllChannels(c *gin.Context) {
|
||||||
@@ -14,7 +20,7 @@ func GetAllChannels(c *gin.Context) {
|
|||||||
if p < 0 {
|
if p < 0 {
|
||||||
p = 0
|
p = 0
|
||||||
}
|
}
|
||||||
channels, err := model.GetAllChannels(p*common.ItemsPerPage, common.ItemsPerPage)
|
channels, err := model.GetAllChannels(p*common.ItemsPerPage, common.ItemsPerPage, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"success": false,
|
"success": false,
|
||||||
@@ -84,7 +90,6 @@ func AddChannel(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
channel.CreatedTime = common.GetTimestamp()
|
channel.CreatedTime = common.GetTimestamp()
|
||||||
channel.AccessedTime = common.GetTimestamp()
|
|
||||||
keys := strings.Split(channel.Key, "\n")
|
keys := strings.Split(channel.Key, "\n")
|
||||||
channels := make([]model.Channel, 0)
|
channels := make([]model.Channel, 0)
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
@@ -153,3 +158,171 @@ func UpdateChannel(c *gin.Context) {
|
|||||||
})
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testChannel(channel *model.Channel, request *ChatRequest) error {
|
||||||
|
if request.Model == "" {
|
||||||
|
request.Model = "gpt-3.5-turbo"
|
||||||
|
if channel.Type == common.ChannelTypeAzure {
|
||||||
|
request.Model = "gpt-35-turbo"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
requestURL := common.ChannelBaseURLs[channel.Type]
|
||||||
|
if channel.Type == common.ChannelTypeAzure {
|
||||||
|
requestURL = fmt.Sprintf("%s/openai/deployments/%s/chat/completions?api-version=2023-03-15-preview", channel.BaseURL, request.Model)
|
||||||
|
} else {
|
||||||
|
if channel.Type == common.ChannelTypeCustom {
|
||||||
|
requestURL = channel.BaseURL
|
||||||
|
}
|
||||||
|
requestURL += "/v1/chat/completions"
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonData, err := json.Marshal(request)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req, err := http.NewRequest("POST", requestURL, bytes.NewBuffer(jsonData))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if channel.Type == common.ChannelTypeAzure {
|
||||||
|
req.Header.Set("api-key", channel.Key)
|
||||||
|
} else {
|
||||||
|
req.Header.Set("Authorization", "Bearer "+channel.Key)
|
||||||
|
}
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
var response TextResponse
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&response)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if response.Error.Type != "" {
|
||||||
|
return errors.New(fmt.Sprintf("type %s, code %s, message %s", response.Error.Type, response.Error.Code, response.Error.Message))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildTestRequest(c *gin.Context) *ChatRequest {
|
||||||
|
model_ := c.Query("model")
|
||||||
|
testRequest := &ChatRequest{
|
||||||
|
Model: model_,
|
||||||
|
}
|
||||||
|
testMessage := Message{
|
||||||
|
Role: "user",
|
||||||
|
Content: "echo hi",
|
||||||
|
}
|
||||||
|
testRequest.Messages = append(testRequest.Messages, testMessage)
|
||||||
|
return testRequest
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChannel(c *gin.Context) {
|
||||||
|
id, err := strconv.Atoi(c.Param("id"))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
channel, err := model.GetChannelById(id, true)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
testRequest := buildTestRequest(c)
|
||||||
|
tik := time.Now()
|
||||||
|
err = testChannel(channel, testRequest)
|
||||||
|
tok := time.Now()
|
||||||
|
milliseconds := tok.Sub(tik).Milliseconds()
|
||||||
|
go channel.UpdateResponseTime(milliseconds)
|
||||||
|
consumedTime := float64(milliseconds) / 1000.0
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
"time": consumedTime,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
"time": consumedTime,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var testAllChannelsLock sync.Mutex
|
||||||
|
|
||||||
|
func testAllChannels(c *gin.Context) error {
|
||||||
|
ok := testAllChannelsLock.TryLock()
|
||||||
|
if !ok {
|
||||||
|
return errors.New("测试已在运行")
|
||||||
|
}
|
||||||
|
defer testAllChannelsLock.Unlock()
|
||||||
|
channels, err := model.GetAllChannels(0, 0, true)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
testRequest := buildTestRequest(c)
|
||||||
|
var disableThreshold int64 = 5000 // TODO: make it configurable
|
||||||
|
email := model.GetRootUserEmail()
|
||||||
|
go func() {
|
||||||
|
for _, channel := range channels {
|
||||||
|
if channel.Status != common.ChannelStatusEnabled {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tik := time.Now()
|
||||||
|
err := testChannel(channel, testRequest)
|
||||||
|
tok := time.Now()
|
||||||
|
milliseconds := tok.Sub(tik).Milliseconds()
|
||||||
|
if err != nil || milliseconds > disableThreshold {
|
||||||
|
if milliseconds > disableThreshold {
|
||||||
|
err = errors.New(fmt.Sprintf("响应时间 %.2fs 超过阈值 %.2fs", float64(milliseconds)/1000.0, float64(disableThreshold)/1000.0))
|
||||||
|
}
|
||||||
|
// disable & notify
|
||||||
|
channel.UpdateStatus(common.ChannelStatusDisabled)
|
||||||
|
subject := fmt.Sprintf("通道「%s」(#%d)已被禁用", channel.Name, channel.Id)
|
||||||
|
content := fmt.Sprintf("通道「%s」(#%d)已被禁用,原因:%s", channel.Name, channel.Id, err.Error())
|
||||||
|
err = common.SendEmail(subject, email, content)
|
||||||
|
if err != nil {
|
||||||
|
common.SysError(fmt.Sprintf("发送邮件失败:%s", err.Error()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
channel.UpdateResponseTime(milliseconds)
|
||||||
|
}
|
||||||
|
err := common.SendEmail("通道测试完成", email, "通道测试完成")
|
||||||
|
if err != nil {
|
||||||
|
common.SysError(fmt.Sprintf("发送邮件失败:%s", err.Error()))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAllChannels(c *gin.Context) {
|
||||||
|
err := testAllChannels(c)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ func GetStatus(c *gin.Context) {
|
|||||||
"github_oauth": common.GitHubOAuthEnabled,
|
"github_oauth": common.GitHubOAuthEnabled,
|
||||||
"github_client_id": common.GitHubClientId,
|
"github_client_id": common.GitHubClientId,
|
||||||
"system_name": common.SystemName,
|
"system_name": common.SystemName,
|
||||||
|
"logo": common.Logo,
|
||||||
"footer_html": common.Footer,
|
"footer_html": common.Footer,
|
||||||
"wechat_qrcode": common.WeChatAccountQRCodeImageURL,
|
"wechat_qrcode": common.WeChatAccountQRCodeImageURL,
|
||||||
"wechat_login": common.WeChatAuthEnabled,
|
"wechat_login": common.WeChatAuthEnabled,
|
||||||
|
|||||||
@@ -19,6 +19,11 @@ type Message struct {
|
|||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type ChatRequest struct {
|
||||||
|
Model string `json:"model"`
|
||||||
|
Messages []Message `json:"messages"`
|
||||||
|
}
|
||||||
|
|
||||||
type TextRequest struct {
|
type TextRequest struct {
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
Messages []Message `json:"messages"`
|
Messages []Message `json:"messages"`
|
||||||
@@ -32,8 +37,16 @@ type Usage struct {
|
|||||||
TotalTokens int `json:"total_tokens"`
|
TotalTokens int `json:"total_tokens"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OpenAIError struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Param string `json:"param"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
}
|
||||||
|
|
||||||
type TextResponse struct {
|
type TextResponse struct {
|
||||||
Usage `json:"usage"`
|
Usage `json:"usage"`
|
||||||
|
Error OpenAIError `json:"error"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type StreamResponse struct {
|
type StreamResponse struct {
|
||||||
|
|||||||
@@ -111,14 +111,9 @@ func TokenAuth() func(c *gin.Context) {
|
|||||||
c.Set("id", token.UserId)
|
c.Set("id", token.UserId)
|
||||||
c.Set("token_id", token.Id)
|
c.Set("token_id", token.Id)
|
||||||
requestURL := c.Request.URL.String()
|
requestURL := c.Request.URL.String()
|
||||||
consumeQuota := false
|
consumeQuota := !token.UnlimitedQuota
|
||||||
switch requestURL {
|
if strings.HasPrefix(requestURL, "/models") {
|
||||||
case "/v1/chat/completions":
|
consumeQuota = false
|
||||||
consumeQuota = !token.UnlimitedQuota
|
|
||||||
case "/v1/completions":
|
|
||||||
consumeQuota = !token.UnlimitedQuota
|
|
||||||
case "/v1/edits":
|
|
||||||
consumeQuota = !token.UnlimitedQuota
|
|
||||||
}
|
}
|
||||||
c.Set("consume_quota", consumeQuota)
|
c.Set("consume_quota", consumeQuota)
|
||||||
if len(parts) > 1 {
|
if len(parts) > 1 {
|
||||||
|
|||||||
@@ -13,15 +13,20 @@ type Channel struct {
|
|||||||
Name string `json:"name" gorm:"index"`
|
Name string `json:"name" gorm:"index"`
|
||||||
Weight int `json:"weight"`
|
Weight int `json:"weight"`
|
||||||
CreatedTime int64 `json:"created_time" gorm:"bigint"`
|
CreatedTime int64 `json:"created_time" gorm:"bigint"`
|
||||||
AccessedTime int64 `json:"accessed_time" gorm:"bigint"`
|
TestTime int64 `json:"test_time" gorm:"bigint"`
|
||||||
|
ResponseTime int `json:"response_time"` // in milliseconds
|
||||||
BaseURL string `json:"base_url" gorm:"column:base_url"`
|
BaseURL string `json:"base_url" gorm:"column:base_url"`
|
||||||
Other string `json:"other"`
|
Other string `json:"other"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAllChannels(startIdx int, num int) ([]*Channel, error) {
|
func GetAllChannels(startIdx int, num int, selectAll bool) ([]*Channel, error) {
|
||||||
var channels []*Channel
|
var channels []*Channel
|
||||||
var err error
|
var err error
|
||||||
err = DB.Order("id desc").Limit(num).Offset(startIdx).Omit("key").Find(&channels).Error
|
if selectAll {
|
||||||
|
err = DB.Order("id desc").Find(&channels).Error
|
||||||
|
} else {
|
||||||
|
err = DB.Order("id desc").Limit(num).Offset(startIdx).Omit("key").Find(&channels).Error
|
||||||
|
}
|
||||||
return channels, err
|
return channels, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,6 +76,23 @@ func (channel *Channel) Update() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (channel *Channel) UpdateResponseTime(responseTime int64) {
|
||||||
|
err := DB.Model(channel).Select("response_time", "test_time").Updates(Channel{
|
||||||
|
TestTime: common.GetTimestamp(),
|
||||||
|
ResponseTime: int(responseTime),
|
||||||
|
}).Error
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("failed to update response time: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (channel *Channel) UpdateStatus(status int) {
|
||||||
|
err := DB.Model(channel).Update("status", status).Error
|
||||||
|
if err != nil {
|
||||||
|
common.SysError("failed to update response time: " + err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (channel *Channel) Delete() error {
|
func (channel *Channel) Delete() error {
|
||||||
var err error
|
var err error
|
||||||
err = DB.Delete(channel).Error
|
err = DB.Delete(channel).Error
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ func InitOptionMap() {
|
|||||||
common.OptionMap["About"] = ""
|
common.OptionMap["About"] = ""
|
||||||
common.OptionMap["HomePageContent"] = ""
|
common.OptionMap["HomePageContent"] = ""
|
||||||
common.OptionMap["Footer"] = common.Footer
|
common.OptionMap["Footer"] = common.Footer
|
||||||
|
common.OptionMap["SystemName"] = common.SystemName
|
||||||
|
common.OptionMap["Logo"] = common.Logo
|
||||||
common.OptionMap["ServerAddress"] = ""
|
common.OptionMap["ServerAddress"] = ""
|
||||||
common.OptionMap["GitHubClientId"] = ""
|
common.OptionMap["GitHubClientId"] = ""
|
||||||
common.OptionMap["GitHubClientSecret"] = ""
|
common.OptionMap["GitHubClientSecret"] = ""
|
||||||
@@ -134,6 +136,10 @@ func updateOptionMap(key string, value string) (err error) {
|
|||||||
common.GitHubClientSecret = value
|
common.GitHubClientSecret = value
|
||||||
case "Footer":
|
case "Footer":
|
||||||
common.Footer = value
|
common.Footer = value
|
||||||
|
case "SystemName":
|
||||||
|
common.SystemName = value
|
||||||
|
case "Logo":
|
||||||
|
common.Logo = value
|
||||||
case "WeChatServerAddress":
|
case "WeChatServerAddress":
|
||||||
common.WeChatServerAddress = value
|
common.WeChatServerAddress = value
|
||||||
case "WeChatServerToken":
|
case "WeChatServerToken":
|
||||||
|
|||||||
@@ -234,3 +234,8 @@ func DecreaseUserQuota(id int, quota int) (err error) {
|
|||||||
err = DB.Model(&User{}).Where("id = ?", id).Update("quota", gorm.Expr("quota - ?", quota)).Error
|
err = DB.Model(&User{}).Where("id = ?", id).Update("quota", gorm.Expr("quota - ?", quota)).Error
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetRootUserEmail() (email string) {
|
||||||
|
DB.Model(&User{}).Where("role = ?", common.RoleRootUser).Select("email").Find(&email)
|
||||||
|
return email
|
||||||
|
}
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
channelRoute.GET("/", controller.GetAllChannels)
|
channelRoute.GET("/", controller.GetAllChannels)
|
||||||
channelRoute.GET("/search", controller.SearchChannels)
|
channelRoute.GET("/search", controller.SearchChannels)
|
||||||
channelRoute.GET("/:id", controller.GetChannel)
|
channelRoute.GET("/:id", controller.GetChannel)
|
||||||
|
channelRoute.GET("/test", controller.TestAllChannels)
|
||||||
|
channelRoute.GET("/test/:id", controller.TestChannel)
|
||||||
channelRoute.POST("/", controller.AddChannel)
|
channelRoute.POST("/", controller.AddChannel)
|
||||||
channelRoute.PUT("/", controller.UpdateChannel)
|
channelRoute.PUT("/", controller.UpdateChannel)
|
||||||
channelRoute.DELETE("/:id", controller.DeleteChannel)
|
channelRoute.DELETE("/:id", controller.DeleteChannel)
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ function App() {
|
|||||||
if (success) {
|
if (success) {
|
||||||
localStorage.setItem('status', JSON.stringify(data));
|
localStorage.setItem('status', JSON.stringify(data));
|
||||||
statusDispatch({ type: 'set', payload: data });
|
statusDispatch({ type: 'set', payload: data });
|
||||||
|
localStorage.setItem('system_name', data.system_name);
|
||||||
|
localStorage.setItem('logo', data.logo);
|
||||||
localStorage.setItem('footer_html', data.footer_html);
|
localStorage.setItem('footer_html', data.footer_html);
|
||||||
if (
|
if (
|
||||||
data.version !== process.env.REACT_APP_VERSION &&
|
data.version !== process.env.REACT_APP_VERSION &&
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Button, Form, Label, Pagination, Table } from 'semantic-ui-react';
|
import { Button, Form, Label, Pagination, Popup, Table } from 'semantic-ui-react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { API, copy, showError, showSuccess, timestamp2string } from '../helpers';
|
import { API, showError, showInfo, showSuccess, timestamp2string } from '../helpers';
|
||||||
|
|
||||||
import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../constants';
|
import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../constants';
|
||||||
|
|
||||||
@@ -120,6 +120,22 @@ const ChannelsTable = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderResponseTime = (responseTime) => {
|
||||||
|
let time = responseTime / 1000;
|
||||||
|
time = time.toFixed(2) + " 秒";
|
||||||
|
if (responseTime === 0) {
|
||||||
|
return <Label basic color='grey'>未测试</Label>;
|
||||||
|
} else if (responseTime <= 1000) {
|
||||||
|
return <Label basic color='green'>{time}</Label>;
|
||||||
|
} else if (responseTime <= 3000) {
|
||||||
|
return <Label basic color='olive'>{time}</Label>;
|
||||||
|
} else if (responseTime <= 5000) {
|
||||||
|
return <Label basic color='yellow'>{time}</Label>;
|
||||||
|
} else {
|
||||||
|
return <Label basic color='red'>{time}</Label>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const searchChannels = async () => {
|
const searchChannels = async () => {
|
||||||
if (searchKeyword === '') {
|
if (searchKeyword === '') {
|
||||||
// if keyword is blank, load files instead.
|
// if keyword is blank, load files instead.
|
||||||
@@ -139,6 +155,31 @@ const ChannelsTable = () => {
|
|||||||
setSearching(false);
|
setSearching(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const testChannel = async (id, name, idx) => {
|
||||||
|
const res = await API.get(`/api/channel/test/${id}/`);
|
||||||
|
const { success, message, time } = res.data;
|
||||||
|
if (success) {
|
||||||
|
let newChannels = [...channels];
|
||||||
|
let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
|
||||||
|
newChannels[realIdx].response_time = time * 1000;
|
||||||
|
newChannels[realIdx].test_time = Date.now() / 1000;
|
||||||
|
setChannels(newChannels);
|
||||||
|
showInfo(`通道 ${name} 测试成功,耗时 ${time} 秒。`);
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const testAllChannels = async () => {
|
||||||
|
const res = await API.get(`/api/channel/test`);
|
||||||
|
const { success, message } = res.data;
|
||||||
|
if (success) {
|
||||||
|
showSuccess("已成功开始测试所有已启用通道,请刷新页面查看结果。");
|
||||||
|
} else {
|
||||||
|
showError(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleKeywordChange = async (e, { value }) => {
|
const handleKeywordChange = async (e, { value }) => {
|
||||||
setSearchKeyword(value.trim());
|
setSearchKeyword(value.trim());
|
||||||
};
|
};
|
||||||
@@ -209,18 +250,18 @@ const ChannelsTable = () => {
|
|||||||
<Table.HeaderCell
|
<Table.HeaderCell
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
sortChannel('created_time');
|
sortChannel('response_time');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
创建时间
|
响应时间
|
||||||
</Table.HeaderCell>
|
</Table.HeaderCell>
|
||||||
<Table.HeaderCell
|
<Table.HeaderCell
|
||||||
style={{ cursor: 'pointer' }}
|
style={{ cursor: 'pointer' }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
sortChannel('accessed_time');
|
sortChannel('test_time');
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
访问时间
|
测试时间
|
||||||
</Table.HeaderCell>
|
</Table.HeaderCell>
|
||||||
<Table.HeaderCell>操作</Table.HeaderCell>
|
<Table.HeaderCell>操作</Table.HeaderCell>
|
||||||
</Table.Row>
|
</Table.Row>
|
||||||
@@ -240,19 +281,38 @@ const ChannelsTable = () => {
|
|||||||
<Table.Cell>{channel.name ? channel.name : '无'}</Table.Cell>
|
<Table.Cell>{channel.name ? channel.name : '无'}</Table.Cell>
|
||||||
<Table.Cell>{renderType(channel.type)}</Table.Cell>
|
<Table.Cell>{renderType(channel.type)}</Table.Cell>
|
||||||
<Table.Cell>{renderStatus(channel.status)}</Table.Cell>
|
<Table.Cell>{renderStatus(channel.status)}</Table.Cell>
|
||||||
<Table.Cell>{renderTimestamp(channel.created_time)}</Table.Cell>
|
<Table.Cell>{renderResponseTime(channel.response_time)}</Table.Cell>
|
||||||
<Table.Cell>{renderTimestamp(channel.accessed_time)}</Table.Cell>
|
<Table.Cell>{channel.test_time ? renderTimestamp(channel.test_time) : "未测试"}</Table.Cell>
|
||||||
<Table.Cell>
|
<Table.Cell>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
size={'small'}
|
size={'small'}
|
||||||
negative
|
positive
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
manageChannel(channel.id, 'delete', idx);
|
testChannel(channel.id, channel.name, idx);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
删除
|
测试
|
||||||
</Button>
|
</Button>
|
||||||
|
<Popup
|
||||||
|
trigger={
|
||||||
|
<Button size='small' negative>
|
||||||
|
删除
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
on='click'
|
||||||
|
flowing
|
||||||
|
hoverable
|
||||||
|
>
|
||||||
|
<Button
|
||||||
|
negative
|
||||||
|
onClick={() => {
|
||||||
|
manageChannel(channel.id, 'delete', idx);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
删除渠道 {channel.name}
|
||||||
|
</Button>
|
||||||
|
</Popup>
|
||||||
<Button
|
<Button
|
||||||
size={'small'}
|
size={'small'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -285,6 +345,9 @@ const ChannelsTable = () => {
|
|||||||
<Button size='small' as={Link} to='/channel/add' loading={loading}>
|
<Button size='small' as={Link} to='/channel/add' loading={loading}>
|
||||||
添加新的渠道
|
添加新的渠道
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button size='small' loading={loading} onClick={testAllChannels}>
|
||||||
|
测试所有已启用通道
|
||||||
|
</Button>
|
||||||
<Pagination
|
<Pagination
|
||||||
floated='right'
|
floated='right'
|
||||||
activePage={activePage}
|
activePage={activePage}
|
||||||
|
|||||||
@@ -1,40 +1,37 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { Container, Segment } from 'semantic-ui-react';
|
import { Container, Segment } from 'semantic-ui-react';
|
||||||
|
import { getFooterHTML, getSystemName } from '../helpers';
|
||||||
|
|
||||||
const Footer = () => {
|
const Footer = () => {
|
||||||
const [Footer, setFooter] = useState('');
|
const systemName = getSystemName();
|
||||||
useEffect(() => {
|
const footer = getFooterHTML();
|
||||||
let savedFooter = localStorage.getItem('footer_html');
|
|
||||||
if (!savedFooter) savedFooter = '';
|
|
||||||
setFooter(savedFooter);
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Segment vertical>
|
<Segment vertical>
|
||||||
<Container textAlign="center">
|
<Container textAlign='center'>
|
||||||
{Footer === '' ? (
|
{footer ? (
|
||||||
<div className="custom-footer">
|
<div
|
||||||
|
className='custom-footer'
|
||||||
|
dangerouslySetInnerHTML={{ __html: footer }}
|
||||||
|
></div>
|
||||||
|
) : (
|
||||||
|
<div className='custom-footer'>
|
||||||
<a
|
<a
|
||||||
href="https://github.com/songquanpeng/one-api"
|
href='https://github.com/songquanpeng/one-api'
|
||||||
target="_blank"
|
target='_blank'
|
||||||
>
|
>
|
||||||
One API {process.env.REACT_APP_VERSION}{' '}
|
{systemName} {process.env.REACT_APP_VERSION}{' '}
|
||||||
</a>
|
</a>
|
||||||
由{' '}
|
由{' '}
|
||||||
<a href="https://github.com/songquanpeng" target="_blank">
|
<a href='https://github.com/songquanpeng' target='_blank'>
|
||||||
JustSong
|
JustSong
|
||||||
</a>{' '}
|
</a>{' '}
|
||||||
构建,源代码遵循{' '}
|
构建,源代码遵循{' '}
|
||||||
<a href="https://opensource.org/licenses/mit-license.php">
|
<a href='https://opensource.org/licenses/mit-license.php'>
|
||||||
MIT 协议
|
MIT 协议
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className="custom-footer"
|
|
||||||
dangerouslySetInnerHTML={{ __html: Footer }}
|
|
||||||
></div>
|
|
||||||
)}
|
)}
|
||||||
</Container>
|
</Container>
|
||||||
</Segment>
|
</Segment>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { Link, useNavigate } from 'react-router-dom';
|
|||||||
import { UserContext } from '../context/User';
|
import { UserContext } from '../context/User';
|
||||||
|
|
||||||
import { Button, Container, Dropdown, Icon, Menu, Segment } from 'semantic-ui-react';
|
import { Button, Container, Dropdown, Icon, Menu, Segment } from 'semantic-ui-react';
|
||||||
import { API, isAdmin, isMobile, showSuccess } from '../helpers';
|
import { API, getLogo, getSystemName, isAdmin, isMobile, showSuccess } from '../helpers';
|
||||||
import '../index.css';
|
import '../index.css';
|
||||||
|
|
||||||
// Header Buttons
|
// Header Buttons
|
||||||
@@ -53,6 +53,8 @@ const Header = () => {
|
|||||||
let navigate = useNavigate();
|
let navigate = useNavigate();
|
||||||
|
|
||||||
const [showSidebar, setShowSidebar] = useState(false);
|
const [showSidebar, setShowSidebar] = useState(false);
|
||||||
|
const systemName = getSystemName();
|
||||||
|
const logo = getLogo();
|
||||||
|
|
||||||
async function logout() {
|
async function logout() {
|
||||||
setShowSidebar(false);
|
setShowSidebar(false);
|
||||||
@@ -111,12 +113,12 @@ const Header = () => {
|
|||||||
<Container>
|
<Container>
|
||||||
<Menu.Item as={Link} to='/'>
|
<Menu.Item as={Link} to='/'>
|
||||||
<img
|
<img
|
||||||
src='/logo.png'
|
src={logo}
|
||||||
alt='logo'
|
alt='logo'
|
||||||
style={{ marginRight: '0.75em' }}
|
style={{ marginRight: '0.75em' }}
|
||||||
/>
|
/>
|
||||||
<div style={{ fontSize: '20px' }}>
|
<div style={{ fontSize: '20px' }}>
|
||||||
<b>One API</b>
|
<b>{systemName}</b>
|
||||||
</div>
|
</div>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
<Menu.Menu position='right'>
|
<Menu.Menu position='right'>
|
||||||
@@ -168,9 +170,9 @@ const Header = () => {
|
|||||||
<Menu borderless style={{ borderTop: 'none' }}>
|
<Menu borderless style={{ borderTop: 'none' }}>
|
||||||
<Container>
|
<Container>
|
||||||
<Menu.Item as={Link} to='/' className={'hide-on-mobile'}>
|
<Menu.Item as={Link} to='/' className={'hide-on-mobile'}>
|
||||||
<img src='/logo.png' alt='logo' style={{ marginRight: '0.75em' }} />
|
<img src={logo} alt='logo' style={{ marginRight: '0.75em' }} />
|
||||||
<div style={{ fontSize: '20px' }}>
|
<div style={{ fontSize: '20px' }}>
|
||||||
<b>One API</b>
|
<b>{systemName}</b>
|
||||||
</div>
|
</div>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
{renderButtons(false)}
|
{renderButtons(false)}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
} from 'semantic-ui-react';
|
} from 'semantic-ui-react';
|
||||||
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
import { Link, useNavigate, useSearchParams } from 'react-router-dom';
|
||||||
import { UserContext } from '../context/User';
|
import { UserContext } from '../context/User';
|
||||||
import { API, showError, showSuccess } from '../helpers';
|
import { API, getLogo, showError, showSuccess } from '../helpers';
|
||||||
|
|
||||||
const LoginForm = () => {
|
const LoginForm = () => {
|
||||||
const [inputs, setInputs] = useState({
|
const [inputs, setInputs] = useState({
|
||||||
@@ -27,6 +27,7 @@ const LoginForm = () => {
|
|||||||
let navigate = useNavigate();
|
let navigate = useNavigate();
|
||||||
|
|
||||||
const [status, setStatus] = useState({});
|
const [status, setStatus] = useState({});
|
||||||
|
const logo = getLogo();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (searchParams.get("expired")) {
|
if (searchParams.get("expired")) {
|
||||||
@@ -95,7 +96,7 @@ const LoginForm = () => {
|
|||||||
<Grid textAlign="center" style={{ marginTop: '48px' }}>
|
<Grid textAlign="center" style={{ marginTop: '48px' }}>
|
||||||
<Grid.Column style={{ maxWidth: 450 }}>
|
<Grid.Column style={{ maxWidth: 450 }}>
|
||||||
<Header as="h2" color="" textAlign="center">
|
<Header as="h2" color="" textAlign="center">
|
||||||
<Image src="/logo.png" /> 用户登录
|
<Image src={logo} /> 用户登录
|
||||||
</Header>
|
</Header>
|
||||||
<Form size="large">
|
<Form size="large">
|
||||||
<Segment>
|
<Segment>
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ const OtherSetting = () => {
|
|||||||
Footer: '',
|
Footer: '',
|
||||||
Notice: '',
|
Notice: '',
|
||||||
About: '',
|
About: '',
|
||||||
|
SystemName: '',
|
||||||
|
Logo: '',
|
||||||
HomePageContent: '',
|
HomePageContent: '',
|
||||||
});
|
});
|
||||||
let originInputs = {};
|
let originInputs = {};
|
||||||
@@ -66,6 +68,14 @@ const OtherSetting = () => {
|
|||||||
await updateOption('Footer', inputs.Footer);
|
await updateOption('Footer', inputs.Footer);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const submitSystemName = async () => {
|
||||||
|
await updateOption('SystemName', inputs.SystemName);
|
||||||
|
};
|
||||||
|
|
||||||
|
const submitLogo = async () => {
|
||||||
|
await updateOption('Logo', inputs.Logo);
|
||||||
|
};
|
||||||
|
|
||||||
const submitAbout = async () => {
|
const submitAbout = async () => {
|
||||||
await updateOption('About', inputs.About);
|
await updateOption('About', inputs.About);
|
||||||
};
|
};
|
||||||
@@ -114,6 +124,27 @@ const OtherSetting = () => {
|
|||||||
<Form.Button onClick={submitNotice}>保存公告</Form.Button>
|
<Form.Button onClick={submitNotice}>保存公告</Form.Button>
|
||||||
<Divider />
|
<Divider />
|
||||||
<Header as='h3'>个性化设置</Header>
|
<Header as='h3'>个性化设置</Header>
|
||||||
|
<Form.Group widths='equal'>
|
||||||
|
<Form.Input
|
||||||
|
label='系统名称'
|
||||||
|
placeholder='在此输入系统名称'
|
||||||
|
value={inputs.SystemName}
|
||||||
|
name='SystemName'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Button onClick={submitSystemName}>设置系统名称</Form.Button>
|
||||||
|
<Form.Group widths='equal'>
|
||||||
|
<Form.Input
|
||||||
|
label='Logo 图片地址'
|
||||||
|
placeholder='在此输入 Logo 图片地址'
|
||||||
|
value={inputs.Logo}
|
||||||
|
name='Logo'
|
||||||
|
type='url'
|
||||||
|
onChange={handleInputChange}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Button onClick={submitLogo}>设置 Logo</Form.Button>
|
||||||
<Form.Group widths='equal'>
|
<Form.Group widths='equal'>
|
||||||
<Form.TextArea
|
<Form.TextArea
|
||||||
label='首页内容'
|
label='首页内容'
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
Segment,
|
Segment,
|
||||||
} from 'semantic-ui-react';
|
} from 'semantic-ui-react';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { API, showError, showInfo, showSuccess } from '../helpers';
|
import { API, getLogo, showError, showInfo, showSuccess } from '../helpers';
|
||||||
import Turnstile from 'react-turnstile';
|
import Turnstile from 'react-turnstile';
|
||||||
|
|
||||||
const RegisterForm = () => {
|
const RegisterForm = () => {
|
||||||
@@ -26,6 +26,7 @@ const RegisterForm = () => {
|
|||||||
const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
|
const [turnstileSiteKey, setTurnstileSiteKey] = useState('');
|
||||||
const [turnstileToken, setTurnstileToken] = useState('');
|
const [turnstileToken, setTurnstileToken] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const logo = getLogo();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let status = localStorage.getItem('status');
|
let status = localStorage.getItem('status');
|
||||||
@@ -100,7 +101,7 @@ const RegisterForm = () => {
|
|||||||
<Grid textAlign='center' style={{ marginTop: '48px' }}>
|
<Grid textAlign='center' style={{ marginTop: '48px' }}>
|
||||||
<Grid.Column style={{ maxWidth: 450 }}>
|
<Grid.Column style={{ maxWidth: 450 }}>
|
||||||
<Header as='h2' color='' textAlign='center'>
|
<Header as='h2' color='' textAlign='center'>
|
||||||
<Image src='/logo.png' /> 新用户注册
|
<Image src={logo} /> 新用户注册
|
||||||
</Header>
|
</Header>
|
||||||
<Form size='large'>
|
<Form size='large'>
|
||||||
<Segment>
|
<Segment>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { Button, Form, Label, Modal, Pagination, Table } from 'semantic-ui-react';
|
import { Button, Form, Label, Modal, Pagination, Popup, Table } from 'semantic-ui-react';
|
||||||
import { Link } from 'react-router-dom';
|
import { Link } from 'react-router-dom';
|
||||||
import { API, copy, showError, showSuccess, showWarning, timestamp2string } from '../helpers';
|
import { API, copy, showError, showSuccess, showWarning, timestamp2string } from '../helpers';
|
||||||
|
|
||||||
@@ -283,15 +283,25 @@ const TokensTable = () => {
|
|||||||
}}>
|
}}>
|
||||||
充值
|
充值
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Popup
|
||||||
size={'small'}
|
trigger={
|
||||||
negative
|
<Button size='small' negative>
|
||||||
onClick={() => {
|
删除
|
||||||
manageToken(token.id, 'delete', idx);
|
</Button>
|
||||||
}}
|
}
|
||||||
|
on='click'
|
||||||
|
flowing
|
||||||
|
hoverable
|
||||||
>
|
>
|
||||||
删除
|
<Button
|
||||||
</Button>
|
negative
|
||||||
|
onClick={() => {
|
||||||
|
manageToken(token.id, 'delete', idx);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
删除令牌 {token.name}
|
||||||
|
</Button>
|
||||||
|
</Popup>
|
||||||
<Button
|
<Button
|
||||||
size={'small'}
|
size={'small'}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -15,6 +15,22 @@ export function isRoot() {
|
|||||||
return user.role >= 100;
|
return user.role >= 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getSystemName() {
|
||||||
|
let system_name = localStorage.getItem('system_name');
|
||||||
|
if (!system_name) return 'One API';
|
||||||
|
return system_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLogo() {
|
||||||
|
let logo = localStorage.getItem('logo');
|
||||||
|
if (!logo) return '/logo.png';
|
||||||
|
return logo
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getFooterHTML() {
|
||||||
|
return localStorage.getItem('footer_html');
|
||||||
|
}
|
||||||
|
|
||||||
export async function copy(text) {
|
export async function copy(text) {
|
||||||
let okay = true;
|
let okay = true;
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user