mirror of
				https://github.com/linux-do/new-api.git
				synced 2025-11-04 13:23:42 +08:00 
			
		
		
		
	merge upstream
Signed-off-by: wozulong <>
This commit is contained in:
		
							
								
								
									
										25
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										25
									
								
								Dockerfile
									
									
									
									
									
								
							@@ -1,29 +1,32 @@
 | 
			
		||||
FROM node:16-slim as builder
 | 
			
		||||
FROM node:16 as builder
 | 
			
		||||
 | 
			
		||||
WORKDIR /build
 | 
			
		||||
COPY web/package.json .
 | 
			
		||||
COPY web/yarn.lock .
 | 
			
		||||
RUN yarn install --network-timeout 1000000
 | 
			
		||||
RUN npm install
 | 
			
		||||
COPY ./web .
 | 
			
		||||
COPY ./VERSION .
 | 
			
		||||
RUN DISABLE_ESLINT_PLUGIN='true' REACT_APP_VERSION=$(cat VERSION) yarn build
 | 
			
		||||
RUN DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat VERSION) npm run build
 | 
			
		||||
 | 
			
		||||
FROM golang AS builder2
 | 
			
		||||
 | 
			
		||||
FROM golang:1.19-alpine AS builder2
 | 
			
		||||
RUN apk add --no-cache build-base
 | 
			
		||||
ENV GO111MODULE=on \
 | 
			
		||||
    CGO_ENABLED=1 \
 | 
			
		||||
    GOOS=linux
 | 
			
		||||
 | 
			
		||||
WORKDIR /build
 | 
			
		||||
#ADD go.mod go.sum ./
 | 
			
		||||
ADD go.mod go.sum ./
 | 
			
		||||
RUN go mod download
 | 
			
		||||
COPY . .
 | 
			
		||||
COPY --from=builder /build/build ./web/build
 | 
			
		||||
 | 
			
		||||
RUN go mod tidy \
 | 
			
		||||
    && go build -ldflags "-s -w -X 'one-api/common.Version=$(cat VERSION)' -extldflags '-static'" -o one-api
 | 
			
		||||
COPY --from=builder /build/dist ./web/dist
 | 
			
		||||
RUN go build -ldflags "-s -w -X 'one-api/common.Version=$(cat VERSION)' -extldflags '-static'" -o one-api
 | 
			
		||||
 | 
			
		||||
FROM alpine
 | 
			
		||||
 | 
			
		||||
RUN apk update \
 | 
			
		||||
    && apk upgrade \
 | 
			
		||||
    && apk add --no-cache ca-certificates tzdata \
 | 
			
		||||
    && update-ca-certificates 2>/dev/null || true
 | 
			
		||||
 | 
			
		||||
COPY --from=builder2 /build/one-api /
 | 
			
		||||
EXPOSE 3000
 | 
			
		||||
WORKDIR /data
 | 
			
		||||
 
 | 
			
		||||
@@ -55,6 +55,7 @@
 | 
			
		||||
3. Anthropic Claude 3 (claude-3-opus-20240229, claude-3-sonnet-20240229)
 | 
			
		||||
4. [Ollama](https://github.com/ollama/ollama?tab=readme-ov-file),添加渠道时,密钥可以随便填写,默认的请求地址是[http://localhost:11434](http://localhost:11434),如果需要修改请在渠道中修改
 | 
			
		||||
5. [Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy)接口,[对接文档](Midjourney.md)
 | 
			
		||||
6. [零一万物](https://platform.lingyiwanwu.com/)
 | 
			
		||||
 | 
			
		||||
您可以在渠道中添加自定义模型gpt-4-gizmo-*,此模型并非OpenAI官方模型,而是第三方模型,使用官方key无法调用。
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -223,6 +223,7 @@ const (
 | 
			
		||||
	ChannelTypeMoonshot       = 25
 | 
			
		||||
	ChannelTypeZhipu_v4       = 26
 | 
			
		||||
	ChannelTypePerplexity     = 27
 | 
			
		||||
	ChannelTypeLingYiWanWu    = 31
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var ChannelBaseURLs = []string{
 | 
			
		||||
@@ -254,4 +255,8 @@ var ChannelBaseURLs = []string{
 | 
			
		||||
	"https://api.moonshot.cn",                   //25
 | 
			
		||||
	"https://open.bigmodel.cn",                  //26
 | 
			
		||||
	"https://api.perplexity.ai",                 //27
 | 
			
		||||
	"",                                          //28
 | 
			
		||||
	"",                                          //29
 | 
			
		||||
	"",                                          //30
 | 
			
		||||
	"https://api.lingyiwanwu.com",               //31
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ import (
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// ModelRatio
 | 
			
		||||
// modelRatio
 | 
			
		||||
// https://platform.openai.com/docs/models/model-endpoint-compatibility
 | 
			
		||||
// https://cloud.baidu.com/doc/WENXINWORKSHOP/s/Blfmc9dlf
 | 
			
		||||
// https://openai.com/pricing
 | 
			
		||||
@@ -93,6 +93,11 @@ var DefaultModelRatio = map[string]float64{
 | 
			
		||||
	"embedding_s1_v1":           0.0715, // ¥0.001 / 1k tokens
 | 
			
		||||
	"semantic_similarity_s1_v1": 0.0715, // ¥0.001 / 1k tokens
 | 
			
		||||
	"hunyuan":                   7.143,  // ¥0.1 / 1k tokens  // https://cloud.tencent.com/document/product/1729/97731#e0e6be58-60c8-469f-bdeb-6c264ce3b4d0
 | 
			
		||||
	// https://platform.lingyiwanwu.com/docs#-计费单元
 | 
			
		||||
	// 已经按照 7.2 来换算美元价格
 | 
			
		||||
	"yi-34b-chat-0205": 0.018,
 | 
			
		||||
	"yi-34b-chat-200k": 0.0864,
 | 
			
		||||
	"yi-vl-plus":       0.0432,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var DefaultModelPrice = map[string]float64{
 | 
			
		||||
@@ -114,14 +119,14 @@ var DefaultModelPrice = map[string]float64{
 | 
			
		||||
	"swap_face":         0.05,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var ModelPrice = map[string]float64{}
 | 
			
		||||
var ModelRatio = map[string]float64{}
 | 
			
		||||
var modelPrice map[string]float64 = nil
 | 
			
		||||
var modelRatio map[string]float64 = nil
 | 
			
		||||
 | 
			
		||||
func ModelPrice2JSONString() string {
 | 
			
		||||
	if len(ModelPrice) == 0 {
 | 
			
		||||
		ModelPrice = DefaultModelPrice
 | 
			
		||||
	if modelPrice == nil {
 | 
			
		||||
		modelPrice = DefaultModelPrice
 | 
			
		||||
	}
 | 
			
		||||
	jsonBytes, err := json.Marshal(ModelPrice)
 | 
			
		||||
	jsonBytes, err := json.Marshal(modelPrice)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		SysError("error marshalling model price: " + err.Error())
 | 
			
		||||
	}
 | 
			
		||||
@@ -129,18 +134,18 @@ func ModelPrice2JSONString() string {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func UpdateModelPriceByJSONString(jsonStr string) error {
 | 
			
		||||
	ModelPrice = make(map[string]float64)
 | 
			
		||||
	return json.Unmarshal([]byte(jsonStr), &ModelPrice)
 | 
			
		||||
	modelPrice = make(map[string]float64)
 | 
			
		||||
	return json.Unmarshal([]byte(jsonStr), &modelPrice)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetModelPrice(name string, printErr bool) float64 {
 | 
			
		||||
	if len(ModelPrice) == 0 {
 | 
			
		||||
		ModelPrice = DefaultModelPrice
 | 
			
		||||
	if modelPrice == nil {
 | 
			
		||||
		modelPrice = DefaultModelPrice
 | 
			
		||||
	}
 | 
			
		||||
	if strings.HasPrefix(name, "gpt-4-gizmo") {
 | 
			
		||||
		name = "gpt-4-gizmo-*"
 | 
			
		||||
	}
 | 
			
		||||
	price, ok := ModelPrice[name]
 | 
			
		||||
	price, ok := modelPrice[name]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		if printErr {
 | 
			
		||||
			SysError("model price not found: " + name)
 | 
			
		||||
@@ -151,10 +156,10 @@ func GetModelPrice(name string, printErr bool) float64 {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ModelRatio2JSONString() string {
 | 
			
		||||
	if len(ModelRatio) == 0 {
 | 
			
		||||
		ModelRatio = DefaultModelRatio
 | 
			
		||||
	if modelRatio == nil {
 | 
			
		||||
		modelRatio = DefaultModelRatio
 | 
			
		||||
	}
 | 
			
		||||
	jsonBytes, err := json.Marshal(ModelRatio)
 | 
			
		||||
	jsonBytes, err := json.Marshal(modelRatio)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		SysError("error marshalling model ratio: " + err.Error())
 | 
			
		||||
	}
 | 
			
		||||
@@ -162,18 +167,18 @@ func ModelRatio2JSONString() string {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func UpdateModelRatioByJSONString(jsonStr string) error {
 | 
			
		||||
	ModelRatio = make(map[string]float64)
 | 
			
		||||
	return json.Unmarshal([]byte(jsonStr), &ModelRatio)
 | 
			
		||||
	modelRatio = make(map[string]float64)
 | 
			
		||||
	return json.Unmarshal([]byte(jsonStr), &modelRatio)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetModelRatio(name string) float64 {
 | 
			
		||||
	if len(ModelRatio) == 0 {
 | 
			
		||||
		ModelRatio = DefaultModelRatio
 | 
			
		||||
	if modelRatio == nil {
 | 
			
		||||
		modelRatio = DefaultModelRatio
 | 
			
		||||
	}
 | 
			
		||||
	if strings.HasPrefix(name, "gpt-4-gizmo") {
 | 
			
		||||
		name = "gpt-4-gizmo-*"
 | 
			
		||||
	}
 | 
			
		||||
	ratio, ok := ModelRatio[name]
 | 
			
		||||
	ratio, ok := modelRatio[name]
 | 
			
		||||
	if !ok {
 | 
			
		||||
		SysError("model ratio not found: " + name)
 | 
			
		||||
		return 30
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ import (
 | 
			
		||||
	"one-api/relay"
 | 
			
		||||
	"one-api/relay/channel/ai360"
 | 
			
		||||
	"one-api/relay/channel/moonshot"
 | 
			
		||||
	"one-api/relay/channel/lingyiwanwu"
 | 
			
		||||
	relayconstant "one-api/relay/constant"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -101,6 +102,17 @@ func init() {
 | 
			
		||||
			Parent:     nil,
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
	for _, modelName := range lingyiwanwu.ModelList {
 | 
			
		||||
		openAIModels = append(openAIModels, OpenAIModels{
 | 
			
		||||
			Id:         modelName,
 | 
			
		||||
			Object:     "model",
 | 
			
		||||
			Created:    1626777600,
 | 
			
		||||
			OwnedBy:    "lingyiwanwu",
 | 
			
		||||
			Permission: permission,
 | 
			
		||||
			Root:       modelName,
 | 
			
		||||
			Parent:     nil,
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
	for modelName, _ := range constant.MidjourneyModel2Action {
 | 
			
		||||
		openAIModels = append(openAIModels, OpenAIModels{
 | 
			
		||||
			Id:         modelName,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								main.go
									
									
									
									
									
								
							
							
						
						
									
										4
									
								
								main.go
									
									
									
									
									
								
							@@ -20,10 +20,10 @@ import (
 | 
			
		||||
	_ "net/http/pprof"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
//go:embed web/build
 | 
			
		||||
//go:embed web/dist
 | 
			
		||||
var buildFS embed.FS
 | 
			
		||||
 | 
			
		||||
//go:embed web/build/index.html
 | 
			
		||||
//go:embed web/dist/index.html
 | 
			
		||||
var indexPage []byte
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
 
 | 
			
		||||
@@ -82,8 +82,8 @@ func InitOptionMap() {
 | 
			
		||||
	common.OptionMap["QuotaForInvitee"] = strconv.Itoa(common.QuotaForInvitee)
 | 
			
		||||
	common.OptionMap["QuotaRemindThreshold"] = strconv.Itoa(common.QuotaRemindThreshold)
 | 
			
		||||
	common.OptionMap["PreConsumedQuota"] = strconv.Itoa(common.PreConsumedQuota)
 | 
			
		||||
	common.OptionMap["ModelRatio"] = common.ModelRatio2JSONString()
 | 
			
		||||
	common.OptionMap["ModelPrice"] = common.ModelPrice2JSONString()
 | 
			
		||||
	common.OptionMap["modelRatio"] = common.ModelRatio2JSONString()
 | 
			
		||||
	common.OptionMap["modelPrice"] = common.ModelPrice2JSONString()
 | 
			
		||||
	common.OptionMap["GroupRatio"] = common.GroupRatio2JSONString()
 | 
			
		||||
	common.OptionMap["TopUpLink"] = common.TopUpLink
 | 
			
		||||
	common.OptionMap["ChatLink"] = common.ChatLink
 | 
			
		||||
@@ -283,11 +283,11 @@ func updateOptionMap(key string, value string) (err error) {
 | 
			
		||||
		common.DataExportInterval, _ = strconv.Atoi(value)
 | 
			
		||||
	case "DataExportDefaultTime":
 | 
			
		||||
		common.DataExportDefaultTime = value
 | 
			
		||||
	case "ModelRatio":
 | 
			
		||||
	case "modelRatio":
 | 
			
		||||
		err = common.UpdateModelRatioByJSONString(value)
 | 
			
		||||
	case "GroupRatio":
 | 
			
		||||
		err = common.UpdateGroupRatioByJSONString(value)
 | 
			
		||||
	case "ModelPrice":
 | 
			
		||||
	case "modelPrice":
 | 
			
		||||
		err = common.UpdateModelPriceByJSONString(value)
 | 
			
		||||
	case "TopUpLink":
 | 
			
		||||
		common.TopUpLink = value
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										9
									
								
								relay/channel/lingyiwanwu/constrants.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								relay/channel/lingyiwanwu/constrants.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
package lingyiwanwu
 | 
			
		||||
 | 
			
		||||
// https://platform.lingyiwanwu.com/docs
 | 
			
		||||
 | 
			
		||||
var ModelList = []string{
 | 
			
		||||
	"yi-34b-chat-0205",
 | 
			
		||||
	"yi-34b-chat-200k",
 | 
			
		||||
	"yi-vl-plus",
 | 
			
		||||
}
 | 
			
		||||
@@ -11,6 +11,7 @@ import (
 | 
			
		||||
	"one-api/relay/channel"
 | 
			
		||||
	"one-api/relay/channel/ai360"
 | 
			
		||||
	"one-api/relay/channel/moonshot"
 | 
			
		||||
	"one-api/relay/channel/lingyiwanwu"
 | 
			
		||||
	relaycommon "one-api/relay/common"
 | 
			
		||||
	"one-api/service"
 | 
			
		||||
	"strings"
 | 
			
		||||
@@ -88,6 +89,8 @@ func (a *Adaptor) GetModelList() []string {
 | 
			
		||||
		return ai360.ModelList
 | 
			
		||||
	case common.ChannelTypeMoonshot:
 | 
			
		||||
		return moonshot.ModelList
 | 
			
		||||
	case common.ChannelTypeLingYiWanWu:
 | 
			
		||||
		return lingyiwanwu.ModelList
 | 
			
		||||
	default:
 | 
			
		||||
		return ModelList
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -74,6 +74,25 @@ func getZhipuToken(apikey string) string {
 | 
			
		||||
func requestOpenAI2Zhipu(request dto.GeneralOpenAIRequest) *dto.GeneralOpenAIRequest {
 | 
			
		||||
	messages := make([]dto.Message, 0, len(request.Messages))
 | 
			
		||||
	for _, message := range request.Messages {
 | 
			
		||||
		if !message.IsStringContent() {
 | 
			
		||||
			mediaMessages := message.ParseContent()
 | 
			
		||||
			for j, mediaMessage := range mediaMessages {
 | 
			
		||||
				if mediaMessage.Type == dto.ContentTypeImageURL {
 | 
			
		||||
					imageUrl := mediaMessage.ImageUrl.(dto.MessageImageUrl)
 | 
			
		||||
					// check if base64
 | 
			
		||||
					if strings.HasPrefix(imageUrl.Url, "data:image/") {
 | 
			
		||||
						// 去除base64数据的URL前缀(如果有)
 | 
			
		||||
						if idx := strings.Index(imageUrl.Url, ","); idx != -1 {
 | 
			
		||||
							imageUrl.Url = imageUrl.Url[idx+1:]
 | 
			
		||||
						}
 | 
			
		||||
					}
 | 
			
		||||
					mediaMessage.ImageUrl = imageUrl
 | 
			
		||||
					mediaMessages[j] = mediaMessage
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			messageRaw, _ := json.Marshal(mediaMessages)
 | 
			
		||||
			message.Content = messageRaw
 | 
			
		||||
		}
 | 
			
		||||
		messages = append(messages, dto.Message{
 | 
			
		||||
			Role:       message.Role,
 | 
			
		||||
			Content:    message.Content,
 | 
			
		||||
@@ -138,7 +157,7 @@ func streamResponseZhipu2OpenAI(zhipuResponse *ZhipuV4StreamResponse) *dto.ChatC
 | 
			
		||||
		Id:      zhipuResponse.Id,
 | 
			
		||||
		Object:  "chat.completion.chunk",
 | 
			
		||||
		Created: zhipuResponse.Created,
 | 
			
		||||
		Model:   "glm-4",
 | 
			
		||||
		Model:   "glm-4v",
 | 
			
		||||
		Choices: []dto.ChatCompletionsStreamResponseChoice{choice},
 | 
			
		||||
	}
 | 
			
		||||
	return &response
 | 
			
		||||
 
 | 
			
		||||
@@ -16,9 +16,9 @@ func SetWebRouter(router *gin.Engine, buildFS embed.FS, indexPage []byte) {
 | 
			
		||||
	router.Use(gzip.Gzip(gzip.DefaultCompression))
 | 
			
		||||
	router.Use(middleware.GlobalWebRateLimit())
 | 
			
		||||
	router.Use(middleware.Cache())
 | 
			
		||||
	router.Use(static.Serve("/", common.EmbedFolder(buildFS, "web/build")))
 | 
			
		||||
	router.Use(static.Serve("/", common.EmbedFolder(buildFS, "web/dist")))
 | 
			
		||||
	router.NoRoute(func(c *gin.Context) {
 | 
			
		||||
		if strings.HasPrefix(c.Request.RequestURI, "/v1") || strings.HasPrefix(c.Request.RequestURI, "/api") {
 | 
			
		||||
		if strings.HasPrefix(c.Request.RequestURI, "/v1") || strings.HasPrefix(c.Request.RequestURI, "/api") || strings.HasPrefix(c.Request.RequestURI, "/assets") {
 | 
			
		||||
			controller.RelayNotFound(c)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -188,7 +188,6 @@ func DoMidjourneyHttpRequest(c *gin.Context, timeout time.Duration, fullRequestU
 | 
			
		||||
	auth := c.Request.Header.Get("Authorization")
 | 
			
		||||
	if auth != "" {
 | 
			
		||||
		auth = strings.TrimPrefix(auth, "Bearer ")
 | 
			
		||||
		auth = strings.Split(auth, "-")[0]
 | 
			
		||||
		req.Header.Set("mj-api-secret", auth)
 | 
			
		||||
	}
 | 
			
		||||
	defer cancel()
 | 
			
		||||
 
 | 
			
		||||
@@ -29,7 +29,7 @@ func InitTokenEncoders() {
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		common.FatalLog(fmt.Sprintf("failed to get gpt-4 token encoder: %s", err.Error()))
 | 
			
		||||
	}
 | 
			
		||||
	for model, _ := range common.ModelRatio {
 | 
			
		||||
	for model, _ := range common.DefaultModelRatio {
 | 
			
		||||
		if strings.HasPrefix(model, "gpt-3.5") {
 | 
			
		||||
			tokenEncoderMap[model] = gpt35TokenEncoder
 | 
			
		||||
		} else if strings.HasPrefix(model, "gpt-4") {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								web/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								web/.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -10,6 +10,7 @@
 | 
			
		||||
 | 
			
		||||
# production
 | 
			
		||||
/build
 | 
			
		||||
/dist
 | 
			
		||||
 | 
			
		||||
# misc
 | 
			
		||||
.DS_Store
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								web/.prettierrc.mjs 
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								web/.prettierrc.mjs 
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
module.exports = require("@so1ve/prettier-config");
 | 
			
		||||
@@ -18,4 +18,4 @@ Before you start editing, make sure your `Actions on Save` options have `Optimiz
 | 
			
		||||
## Reference
 | 
			
		||||
 | 
			
		||||
1. https://github.com/OIerDb-ng/OIerDb
 | 
			
		||||
2. https://github.com/cornflourblue/react-hooks-redux-registration-login-example
 | 
			
		||||
2. https://github.com/cornflourblue/react-hooks-redux-registration-login-example
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										19
									
								
								web/index.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								web/index.html
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
<!doctype html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
  <head>
 | 
			
		||||
    <meta charset="utf-8" />
 | 
			
		||||
    <link rel="icon" href="/logo.png" />
 | 
			
		||||
    <meta name="viewport" content="width=device-width, initial-scale=1" />
 | 
			
		||||
    <meta name="theme-color" content="#ffffff" />
 | 
			
		||||
    <meta
 | 
			
		||||
      name="description"
 | 
			
		||||
      content="OpenAI 接口聚合管理,支持多种渠道包括 Azure,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用"
 | 
			
		||||
    />
 | 
			
		||||
    <title>New API</title>
 | 
			
		||||
  </head>
 | 
			
		||||
  <body>
 | 
			
		||||
    <noscript>You need to enable JavaScript to run this app.</noscript>
 | 
			
		||||
    <div id="root"></div>
 | 
			
		||||
    <script type="module" src="/src/index.js"></script>
 | 
			
		||||
  </body>
 | 
			
		||||
</html>
 | 
			
		||||
@@ -2,6 +2,7 @@
 | 
			
		||||
  "name": "react-template",
 | 
			
		||||
  "version": "0.1.0",
 | 
			
		||||
  "private": true,
 | 
			
		||||
  "type": "module",
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@douyinfe/semi-icons": "^2.46.1",
 | 
			
		||||
    "@douyinfe/semi-ui": "^2.46.1",
 | 
			
		||||
@@ -16,19 +17,18 @@
 | 
			
		||||
    "react-dropzone": "^14.2.3",
 | 
			
		||||
    "react-fireworks": "^1.0.4",
 | 
			
		||||
    "react-router-dom": "^6.3.0",
 | 
			
		||||
    "react-scripts": "5.0.1",
 | 
			
		||||
    "react-telegram-login": "^1.1.2",
 | 
			
		||||
    "react-toastify": "^9.0.8",
 | 
			
		||||
    "react-turnstile": "^1.0.5",
 | 
			
		||||
    "semantic-ui-css": "^2.5.0",
 | 
			
		||||
    "semantic-ui-react": "^2.1.3",
 | 
			
		||||
    "usehooks-ts": "^2.9.1"
 | 
			
		||||
    "semantic-ui-offline": "^2.5.0",
 | 
			
		||||
    "semantic-ui-react": "^2.1.3"
 | 
			
		||||
  },
 | 
			
		||||
  "scripts": {
 | 
			
		||||
    "start": "react-scripts start",
 | 
			
		||||
    "build": "react-scripts build",
 | 
			
		||||
    "test": "react-scripts test",
 | 
			
		||||
    "eject": "react-scripts eject"
 | 
			
		||||
    "dev": "vite",
 | 
			
		||||
    "build": "vite build",
 | 
			
		||||
    "lint": "prettier . --check",
 | 
			
		||||
    "lint:fix": "prettier . --write",
 | 
			
		||||
    "preview": "vite preview"
 | 
			
		||||
  },
 | 
			
		||||
  "eslintConfig": {
 | 
			
		||||
    "extends": [
 | 
			
		||||
@@ -49,9 +49,11 @@
 | 
			
		||||
    ]
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "prettier": "2.8.8",
 | 
			
		||||
    "@so1ve/prettier-config": "^2.0.0",
 | 
			
		||||
    "@vitejs/plugin-react": "^4.2.1",
 | 
			
		||||
    "prettier": "^3.0.0",
 | 
			
		||||
    "typescript": "4.4.2",
 | 
			
		||||
    "@babel/plugin-proposal-private-property-in-object": "^7.21.11"
 | 
			
		||||
    "vite": "^5.2.0"
 | 
			
		||||
  },
 | 
			
		||||
  "prettier": {
 | 
			
		||||
    "singleQuote": true,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +0,0 @@
 | 
			
		||||
<!DOCTYPE html>
 | 
			
		||||
<html lang="en">
 | 
			
		||||
<head>
 | 
			
		||||
  <meta charset="utf-8" />
 | 
			
		||||
  <link rel="icon" href="logo.png" />
 | 
			
		||||
  <meta name="viewport" content="width=device-width, initial-scale=1" />
 | 
			
		||||
  <meta name="theme-color" content="#ffffff" />
 | 
			
		||||
  <meta
 | 
			
		||||
          name="description"
 | 
			
		||||
          content="OpenAI 接口聚合管理,支持多种渠道包括 Azure,可用于二次分发管理 key,仅单可执行文件,已打包好 Docker 镜像,一键部署,开箱即用"
 | 
			
		||||
  />
 | 
			
		||||
  <title>New API</title>
 | 
			
		||||
</head>
 | 
			
		||||
<body>
 | 
			
		||||
<noscript>You need to enable JavaScript to run this app.</noscript>
 | 
			
		||||
<div id="root"></div>
 | 
			
		||||
</body>
 | 
			
		||||
</html>
 | 
			
		||||
@@ -11,7 +11,7 @@ import EditUser from './pages/User/EditUser';
 | 
			
		||||
import { getLogo, getSystemName } from './helpers';
 | 
			
		||||
import PasswordResetForm from './components/PasswordResetForm';
 | 
			
		||||
import GitHubOAuth from './components/GitHubOAuth';
 | 
			
		||||
import LinuxDoOAuth from "./components/LinuxDoOAuth";
 | 
			
		||||
import LinuxDoOAuth from './components/LinuxDoOAuth';
 | 
			
		||||
import PasswordResetConfirm from './components/PasswordResetConfirm';
 | 
			
		||||
import { UserContext } from './context/User';
 | 
			
		||||
import Channel from './pages/Channel';
 | 
			
		||||
@@ -23,9 +23,10 @@ import Log from './pages/Log';
 | 
			
		||||
import Chat from './pages/Chat';
 | 
			
		||||
import { Layout } from '@douyinfe/semi-ui';
 | 
			
		||||
import Midjourney from './pages/Midjourney';
 | 
			
		||||
import Detail from './pages/Detail';
 | 
			
		||||
// import Detail from './pages/Detail';
 | 
			
		||||
 | 
			
		||||
const Home = lazy(() => import('./pages/Home'));
 | 
			
		||||
const Detail = lazy(() => import('./pages/Detail'));
 | 
			
		||||
const About = lazy(() => import('./pages/About'));
 | 
			
		||||
 | 
			
		||||
function App() {
 | 
			
		||||
@@ -48,7 +49,7 @@ function App() {
 | 
			
		||||
    }
 | 
			
		||||
    let logo = getLogo();
 | 
			
		||||
    if (logo) {
 | 
			
		||||
      let linkElement = document.querySelector('link[rel~=\'icon\']');
 | 
			
		||||
      let linkElement = document.querySelector("link[rel~='icon']");
 | 
			
		||||
      if (linkElement) {
 | 
			
		||||
        linkElement.href = logo;
 | 
			
		||||
      }
 | 
			
		||||
@@ -60,7 +61,7 @@ function App() {
 | 
			
		||||
      <Layout.Content>
 | 
			
		||||
        <Routes>
 | 
			
		||||
          <Route
 | 
			
		||||
            path="/"
 | 
			
		||||
            path='/'
 | 
			
		||||
            element={
 | 
			
		||||
              <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
                <Home />
 | 
			
		||||
@@ -68,7 +69,7 @@ function App() {
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path="/channel"
 | 
			
		||||
            path='/channel'
 | 
			
		||||
            element={
 | 
			
		||||
              <PrivateRoute>
 | 
			
		||||
                <Channel />
 | 
			
		||||
@@ -76,7 +77,7 @@ function App() {
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path="/channel/edit/:id"
 | 
			
		||||
            path='/channel/edit/:id'
 | 
			
		||||
            element={
 | 
			
		||||
              <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
                <EditChannel />
 | 
			
		||||
@@ -84,7 +85,7 @@ function App() {
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path="/channel/add"
 | 
			
		||||
            path='/channel/add'
 | 
			
		||||
            element={
 | 
			
		||||
              <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
                <EditChannel />
 | 
			
		||||
@@ -92,7 +93,7 @@ function App() {
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path="/token"
 | 
			
		||||
            path='/token'
 | 
			
		||||
            element={
 | 
			
		||||
              <PrivateRoute>
 | 
			
		||||
                <Token />
 | 
			
		||||
@@ -100,7 +101,7 @@ function App() {
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path="/redemption"
 | 
			
		||||
            path='/redemption'
 | 
			
		||||
            element={
 | 
			
		||||
              <PrivateRoute>
 | 
			
		||||
                <Redemption />
 | 
			
		||||
@@ -108,7 +109,7 @@ function App() {
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path="/user"
 | 
			
		||||
            path='/user'
 | 
			
		||||
            element={
 | 
			
		||||
              <PrivateRoute>
 | 
			
		||||
                <User />
 | 
			
		||||
@@ -116,7 +117,7 @@ function App() {
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path="/user/edit/:id"
 | 
			
		||||
            path='/user/edit/:id'
 | 
			
		||||
            element={
 | 
			
		||||
              <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
                <EditUser />
 | 
			
		||||
@@ -124,7 +125,7 @@ function App() {
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path="/user/edit"
 | 
			
		||||
            path='/user/edit'
 | 
			
		||||
            element={
 | 
			
		||||
              <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
                <EditUser />
 | 
			
		||||
@@ -132,7 +133,7 @@ function App() {
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path="/user/reset"
 | 
			
		||||
            path='/user/reset'
 | 
			
		||||
            element={
 | 
			
		||||
              <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
                <PasswordResetConfirm />
 | 
			
		||||
@@ -140,7 +141,7 @@ function App() {
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path="/login"
 | 
			
		||||
            path='/login'
 | 
			
		||||
            element={
 | 
			
		||||
              <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
                <LoginForm />
 | 
			
		||||
@@ -148,7 +149,7 @@ function App() {
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path="/register"
 | 
			
		||||
            path='/register'
 | 
			
		||||
            element={
 | 
			
		||||
              <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
                <RegisterForm />
 | 
			
		||||
@@ -156,7 +157,7 @@ function App() {
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path="/reset"
 | 
			
		||||
            path='/reset'
 | 
			
		||||
            element={
 | 
			
		||||
              <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
                <PasswordResetForm />
 | 
			
		||||
@@ -164,7 +165,7 @@ function App() {
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path="/oauth/github"
 | 
			
		||||
            path='/oauth/github'
 | 
			
		||||
            element={
 | 
			
		||||
              <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
                <GitHubOAuth />
 | 
			
		||||
@@ -172,7 +173,7 @@ function App() {
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path="/oauth/linuxdo"
 | 
			
		||||
            path='/oauth/linuxdo'
 | 
			
		||||
            element={
 | 
			
		||||
              <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
                <LinuxDoOAuth />
 | 
			
		||||
@@ -180,7 +181,7 @@ function App() {
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path="/setting"
 | 
			
		||||
            path='/setting'
 | 
			
		||||
            element={
 | 
			
		||||
              <PrivateRoute>
 | 
			
		||||
                <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
@@ -190,7 +191,7 @@ function App() {
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path="/topup"
 | 
			
		||||
            path='/topup'
 | 
			
		||||
            element={
 | 
			
		||||
              <PrivateRoute>
 | 
			
		||||
                <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
@@ -200,7 +201,7 @@ function App() {
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path="/log"
 | 
			
		||||
            path='/log'
 | 
			
		||||
            element={
 | 
			
		||||
              <PrivateRoute>
 | 
			
		||||
                <Log />
 | 
			
		||||
@@ -208,23 +209,27 @@ function App() {
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path="/detail"
 | 
			
		||||
            path='/detail'
 | 
			
		||||
            element={
 | 
			
		||||
              <PrivateRoute>
 | 
			
		||||
                <Detail />
 | 
			
		||||
                <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
                  <Detail />
 | 
			
		||||
                </Suspense>
 | 
			
		||||
              </PrivateRoute>
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path="/midjourney"
 | 
			
		||||
            path='/midjourney'
 | 
			
		||||
            element={
 | 
			
		||||
              <PrivateRoute>
 | 
			
		||||
                <Midjourney />
 | 
			
		||||
                <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
                  <Midjourney />
 | 
			
		||||
                </Suspense>
 | 
			
		||||
              </PrivateRoute>
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path="/about"
 | 
			
		||||
            path='/about'
 | 
			
		||||
            element={
 | 
			
		||||
              <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
                <About />
 | 
			
		||||
@@ -232,16 +237,14 @@ function App() {
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route
 | 
			
		||||
            path="/chat"
 | 
			
		||||
            path='/chat'
 | 
			
		||||
            element={
 | 
			
		||||
              <Suspense fallback={<Loading></Loading>}>
 | 
			
		||||
                <Chat />
 | 
			
		||||
              </Suspense>
 | 
			
		||||
            }
 | 
			
		||||
          />
 | 
			
		||||
          <Route path="*" element={
 | 
			
		||||
            <NotFound />
 | 
			
		||||
          } />
 | 
			
		||||
          <Route path='*' element={<NotFound />} />
 | 
			
		||||
        </Routes>
 | 
			
		||||
      </Layout.Content>
 | 
			
		||||
    </Layout>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,31 +1,39 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { API, isMobile, shouldShowPrompt, showError, showInfo, showSuccess, timestamp2string } from '../helpers';
 | 
			
		||||
import {
 | 
			
		||||
  API,
 | 
			
		||||
  isMobile,
 | 
			
		||||
  shouldShowPrompt,
 | 
			
		||||
  showError,
 | 
			
		||||
  showInfo,
 | 
			
		||||
  showSuccess,
 | 
			
		||||
  timestamp2string,
 | 
			
		||||
} from '../helpers';
 | 
			
		||||
 | 
			
		||||
import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../constants';
 | 
			
		||||
import { renderGroup, renderNumberWithPoint, renderQuota } from '../helpers/render';
 | 
			
		||||
import {
 | 
			
		||||
    Button,
 | 
			
		||||
    Dropdown,
 | 
			
		||||
    Form,
 | 
			
		||||
    InputNumber,
 | 
			
		||||
    Popconfirm,
 | 
			
		||||
    Space,
 | 
			
		||||
    SplitButtonGroup,
 | 
			
		||||
    Switch,
 | 
			
		||||
    Table,
 | 
			
		||||
    Tag,
 | 
			
		||||
    Tooltip,
 | 
			
		||||
    Typography
 | 
			
		||||
  renderGroup,
 | 
			
		||||
  renderNumberWithPoint,
 | 
			
		||||
  renderQuota,
 | 
			
		||||
} from '../helpers/render';
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Dropdown,
 | 
			
		||||
  Form,
 | 
			
		||||
  InputNumber,
 | 
			
		||||
  Popconfirm,
 | 
			
		||||
  Space,
 | 
			
		||||
  SplitButtonGroup,
 | 
			
		||||
  Switch,
 | 
			
		||||
  Table,
 | 
			
		||||
  Tag,
 | 
			
		||||
  Tooltip,
 | 
			
		||||
  Typography,
 | 
			
		||||
} from '@douyinfe/semi-ui';
 | 
			
		||||
import EditChannel from '../pages/Channel/EditChannel';
 | 
			
		||||
import { IconTreeTriangleDown } from '@douyinfe/semi-icons';
 | 
			
		||||
 | 
			
		||||
function renderTimestamp(timestamp) {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {timestamp2string(timestamp)}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
  return <>{timestamp2string(timestamp)}</>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
let type2label = undefined;
 | 
			
		||||
@@ -38,7 +46,11 @@ function renderType(type) {
 | 
			
		||||
    }
 | 
			
		||||
    type2label[0] = { value: 0, text: '未知类型', color: 'grey' };
 | 
			
		||||
  }
 | 
			
		||||
  return <Tag size="large" color={type2label[type]?.color}>{type2label[type]?.text}</Tag>;
 | 
			
		||||
  return (
 | 
			
		||||
    <Tag size='large' color={type2label[type]?.color}>
 | 
			
		||||
      {type2label[type]?.text}
 | 
			
		||||
    </Tag>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const ChannelsTable = () => {
 | 
			
		||||
@@ -50,11 +62,11 @@ const ChannelsTable = () => {
 | 
			
		||||
    // },
 | 
			
		||||
    {
 | 
			
		||||
      title: 'ID',
 | 
			
		||||
      dataIndex: 'id'
 | 
			
		||||
      dataIndex: 'id',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '名称',
 | 
			
		||||
      dataIndex: 'name'
 | 
			
		||||
      dataIndex: 'name',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '分组',
 | 
			
		||||
@@ -63,48 +75,34 @@ const ChannelsTable = () => {
 | 
			
		||||
        return (
 | 
			
		||||
          <div>
 | 
			
		||||
            <Space spacing={2}>
 | 
			
		||||
              {
 | 
			
		||||
                text.split(',').map((item, index) => {
 | 
			
		||||
                  return (renderGroup(item));
 | 
			
		||||
                })
 | 
			
		||||
              }
 | 
			
		||||
              {text.split(',').map((item, index) => {
 | 
			
		||||
                return renderGroup(item);
 | 
			
		||||
              })}
 | 
			
		||||
            </Space>
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '类型',
 | 
			
		||||
      dataIndex: 'type',
 | 
			
		||||
      render: (text, record, index) => {
 | 
			
		||||
        return (
 | 
			
		||||
          <div>
 | 
			
		||||
            {renderType(text)}
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
        return <div>{renderType(text)}</div>;
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '状态',
 | 
			
		||||
      dataIndex: 'status',
 | 
			
		||||
      render: (text, record, index) => {
 | 
			
		||||
        return (
 | 
			
		||||
          <div>
 | 
			
		||||
            {renderStatus(text)}
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
        return <div>{renderStatus(text)}</div>;
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '响应时间',
 | 
			
		||||
      dataIndex: 'response_time',
 | 
			
		||||
      render: (text, record, index) => {
 | 
			
		||||
        return (
 | 
			
		||||
          <div>
 | 
			
		||||
            {renderResponseTime(text)}
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
        return <div>{renderResponseTime(text)}</div>;
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '已用/剩余',
 | 
			
		||||
@@ -114,17 +112,26 @@ const ChannelsTable = () => {
 | 
			
		||||
          <div>
 | 
			
		||||
            <Space spacing={1}>
 | 
			
		||||
              <Tooltip content={'已用额度'}>
 | 
			
		||||
                <Tag color="white" type="ghost" size="large">{renderQuota(record.used_quota)}</Tag>
 | 
			
		||||
                <Tag color='white' type='ghost' size='large'>
 | 
			
		||||
                  {renderQuota(record.used_quota)}
 | 
			
		||||
                </Tag>
 | 
			
		||||
              </Tooltip>
 | 
			
		||||
              <Tooltip content={'剩余额度' + record.balance + ',点击更新'}>
 | 
			
		||||
                <Tag color="white" type="ghost" size="large" onClick={() => {
 | 
			
		||||
                  updateChannelBalance(record);
 | 
			
		||||
                }}>${renderNumberWithPoint(record.balance)}</Tag>
 | 
			
		||||
                <Tag
 | 
			
		||||
                  color='white'
 | 
			
		||||
                  type='ghost'
 | 
			
		||||
                  size='large'
 | 
			
		||||
                  onClick={() => {
 | 
			
		||||
                    updateChannelBalance(record);
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  ${renderNumberWithPoint(record.balance)}
 | 
			
		||||
                </Tag>
 | 
			
		||||
              </Tooltip>
 | 
			
		||||
            </Space>
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '优先级',
 | 
			
		||||
@@ -134,8 +141,8 @@ const ChannelsTable = () => {
 | 
			
		||||
          <div>
 | 
			
		||||
            <InputNumber
 | 
			
		||||
              style={{ width: 70 }}
 | 
			
		||||
              name="priority"
 | 
			
		||||
              onBlur={e => {
 | 
			
		||||
              name='priority'
 | 
			
		||||
              onBlur={(e) => {
 | 
			
		||||
                manageChannel(record.id, 'priority', record, e.target.value);
 | 
			
		||||
              }}
 | 
			
		||||
              keepFocus={true}
 | 
			
		||||
@@ -145,7 +152,7 @@ const ChannelsTable = () => {
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '权重',
 | 
			
		||||
@@ -155,8 +162,8 @@ const ChannelsTable = () => {
 | 
			
		||||
          <div>
 | 
			
		||||
            <InputNumber
 | 
			
		||||
              style={{ width: 70 }}
 | 
			
		||||
              name="weight"
 | 
			
		||||
              onBlur={e => {
 | 
			
		||||
              name='weight'
 | 
			
		||||
              onBlur={(e) => {
 | 
			
		||||
                manageChannel(record.id, 'weight', record, e.target.value);
 | 
			
		||||
              }}
 | 
			
		||||
              keepFocus={true}
 | 
			
		||||
@@ -166,68 +173,90 @@ const ChannelsTable = () => {
 | 
			
		||||
            />
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '',
 | 
			
		||||
      dataIndex: 'operate',
 | 
			
		||||
      render: (text, record, index) => (
 | 
			
		||||
        <div>
 | 
			
		||||
          <SplitButtonGroup style={{ marginRight: 1 }} aria-label="测试操作项目组">
 | 
			
		||||
            <Button theme="light" onClick={() => {
 | 
			
		||||
              testChannel(record, '');
 | 
			
		||||
            }}>测试</Button>
 | 
			
		||||
            <Dropdown trigger="click" position="bottomRight" menu={record.test_models}
 | 
			
		||||
          <SplitButtonGroup
 | 
			
		||||
            style={{ marginRight: 1 }}
 | 
			
		||||
            aria-label='测试操作项目组'
 | 
			
		||||
          >
 | 
			
		||||
            <Button
 | 
			
		||||
              theme='light'
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                testChannel(record, '');
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <Button style={{ padding: '8px 4px' }} type="primary" icon={<IconTreeTriangleDown />}></Button>
 | 
			
		||||
              测试
 | 
			
		||||
            </Button>
 | 
			
		||||
            <Dropdown
 | 
			
		||||
              trigger='click'
 | 
			
		||||
              position='bottomRight'
 | 
			
		||||
              menu={record.test_models}
 | 
			
		||||
            >
 | 
			
		||||
              <Button
 | 
			
		||||
                style={{ padding: '8px 4px' }}
 | 
			
		||||
                type='primary'
 | 
			
		||||
                icon={<IconTreeTriangleDown />}
 | 
			
		||||
              ></Button>
 | 
			
		||||
            </Dropdown>
 | 
			
		||||
          </SplitButtonGroup>
 | 
			
		||||
          {/*<Button theme='light' type='primary' style={{marginRight: 1}} onClick={()=>testChannel(record)}>测试</Button>*/}
 | 
			
		||||
          <Popconfirm
 | 
			
		||||
            title="确定是否要删除此渠道?"
 | 
			
		||||
            content="此修改将不可逆"
 | 
			
		||||
            title='确定是否要删除此渠道?'
 | 
			
		||||
            content='此修改将不可逆'
 | 
			
		||||
            okType={'danger'}
 | 
			
		||||
            position={'left'}
 | 
			
		||||
            onConfirm={() => {
 | 
			
		||||
              manageChannel(record.id, 'delete', record).then(
 | 
			
		||||
                () => {
 | 
			
		||||
                  removeRecord(record.id);
 | 
			
		||||
                }
 | 
			
		||||
              );
 | 
			
		||||
              manageChannel(record.id, 'delete', record).then(() => {
 | 
			
		||||
                removeRecord(record.id);
 | 
			
		||||
              });
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <Button theme="light" type="danger" style={{ marginRight: 1 }}>删除</Button>
 | 
			
		||||
            <Button theme='light' type='danger' style={{ marginRight: 1 }}>
 | 
			
		||||
              删除
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Popconfirm>
 | 
			
		||||
          {
 | 
			
		||||
            record.status === 1 ?
 | 
			
		||||
              <Button theme="light" type="warning" style={{ marginRight: 1 }} onClick={
 | 
			
		||||
                async () => {
 | 
			
		||||
                  manageChannel(
 | 
			
		||||
                    record.id,
 | 
			
		||||
                    'disable',
 | 
			
		||||
                    record
 | 
			
		||||
                  );
 | 
			
		||||
                }
 | 
			
		||||
              }>禁用</Button> :
 | 
			
		||||
              <Button theme="light" type="secondary" style={{ marginRight: 1 }} onClick={
 | 
			
		||||
                async () => {
 | 
			
		||||
                  manageChannel(
 | 
			
		||||
                    record.id,
 | 
			
		||||
                    'enable',
 | 
			
		||||
                    record
 | 
			
		||||
                  );
 | 
			
		||||
                }
 | 
			
		||||
              }>启用</Button>
 | 
			
		||||
          }
 | 
			
		||||
          <Button theme="light" type="tertiary" style={{ marginRight: 1 }} onClick={
 | 
			
		||||
            () => {
 | 
			
		||||
          {record.status === 1 ? (
 | 
			
		||||
            <Button
 | 
			
		||||
              theme='light'
 | 
			
		||||
              type='warning'
 | 
			
		||||
              style={{ marginRight: 1 }}
 | 
			
		||||
              onClick={async () => {
 | 
			
		||||
                manageChannel(record.id, 'disable', record);
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              禁用
 | 
			
		||||
            </Button>
 | 
			
		||||
          ) : (
 | 
			
		||||
            <Button
 | 
			
		||||
              theme='light'
 | 
			
		||||
              type='secondary'
 | 
			
		||||
              style={{ marginRight: 1 }}
 | 
			
		||||
              onClick={async () => {
 | 
			
		||||
                manageChannel(record.id, 'enable', record);
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              启用
 | 
			
		||||
            </Button>
 | 
			
		||||
          )}
 | 
			
		||||
          <Button
 | 
			
		||||
            theme='light'
 | 
			
		||||
            type='tertiary'
 | 
			
		||||
            style={{ marginRight: 1 }}
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              setEditingChannel(record);
 | 
			
		||||
              setShowEdit(true);
 | 
			
		||||
            }
 | 
			
		||||
          }>编辑</Button>
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            编辑
 | 
			
		||||
          </Button>
 | 
			
		||||
        </div>
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
      ),
 | 
			
		||||
    },
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  const [channels, setChannels] = useState([]);
 | 
			
		||||
@@ -240,20 +269,22 @@ const ChannelsTable = () => {
 | 
			
		||||
  const [searching, setSearching] = useState(false);
 | 
			
		||||
  const [updatingBalance, setUpdatingBalance] = useState(false);
 | 
			
		||||
  const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
 | 
			
		||||
  const [showPrompt, setShowPrompt] = useState(shouldShowPrompt('channel-test'));
 | 
			
		||||
  const [showPrompt, setShowPrompt] = useState(
 | 
			
		||||
    shouldShowPrompt('channel-test'),
 | 
			
		||||
  );
 | 
			
		||||
  const [channelCount, setChannelCount] = useState(pageSize);
 | 
			
		||||
  const [groupOptions, setGroupOptions] = useState([]);
 | 
			
		||||
  const [showEdit, setShowEdit] = useState(false);
 | 
			
		||||
  const [enableBatchDelete, setEnableBatchDelete] = useState(false);
 | 
			
		||||
  const [editingChannel, setEditingChannel] = useState({
 | 
			
		||||
    id: undefined
 | 
			
		||||
    id: undefined,
 | 
			
		||||
  });
 | 
			
		||||
  const [selectedChannels, setSelectedChannels] = useState([]);
 | 
			
		||||
 | 
			
		||||
  const removeRecord = id => {
 | 
			
		||||
  const removeRecord = (id) => {
 | 
			
		||||
    let newDataSource = [...channels];
 | 
			
		||||
    if (id != null) {
 | 
			
		||||
      let idx = newDataSource.findIndex(data => data.id === id);
 | 
			
		||||
      let idx = newDataSource.findIndex((data) => data.id === id);
 | 
			
		||||
 | 
			
		||||
      if (idx > -1) {
 | 
			
		||||
        newDataSource.splice(idx, 1);
 | 
			
		||||
@@ -272,7 +303,7 @@ const ChannelsTable = () => {
 | 
			
		||||
          name: item,
 | 
			
		||||
          onClick: () => {
 | 
			
		||||
            testChannel(channels[i], item);
 | 
			
		||||
          }
 | 
			
		||||
          },
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
      channels[i].test_models = test_models;
 | 
			
		||||
@@ -288,7 +319,9 @@ const ChannelsTable = () => {
 | 
			
		||||
 | 
			
		||||
  const loadChannels = async (startIdx, pageSize, idSort) => {
 | 
			
		||||
    setLoading(true);
 | 
			
		||||
    const res = await API.get(`/api/channel/?p=${startIdx}&page_size=${pageSize}&id_sort=${idSort}`);
 | 
			
		||||
    const res = await API.get(
 | 
			
		||||
      `/api/channel/?p=${startIdx}&page_size=${pageSize}&id_sort=${idSort}`,
 | 
			
		||||
    );
 | 
			
		||||
    const { success, message, data } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      if (startIdx === 0) {
 | 
			
		||||
@@ -311,7 +344,8 @@ const ChannelsTable = () => {
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    // console.log('default effect')
 | 
			
		||||
    const localIdSort = localStorage.getItem('id-sort') === 'true';
 | 
			
		||||
    const localPageSize = parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE;
 | 
			
		||||
    const localPageSize =
 | 
			
		||||
      parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE;
 | 
			
		||||
    setIdSort(localIdSort);
 | 
			
		||||
    setPageSize(localPageSize);
 | 
			
		||||
    loadChannels(0, localPageSize, localIdSort)
 | 
			
		||||
@@ -361,7 +395,6 @@ const ChannelsTable = () => {
 | 
			
		||||
      let channel = res.data.data;
 | 
			
		||||
      let newChannels = [...channels];
 | 
			
		||||
      if (action === 'delete') {
 | 
			
		||||
 | 
			
		||||
      } else {
 | 
			
		||||
        record.status = channel.status;
 | 
			
		||||
      }
 | 
			
		||||
@@ -374,22 +407,26 @@ const ChannelsTable = () => {
 | 
			
		||||
  const renderStatus = (status) => {
 | 
			
		||||
    switch (status) {
 | 
			
		||||
      case 1:
 | 
			
		||||
        return <Tag size="large" color="green">已启用</Tag>;
 | 
			
		||||
        return (
 | 
			
		||||
          <Tag size='large' color='green'>
 | 
			
		||||
            已启用
 | 
			
		||||
          </Tag>
 | 
			
		||||
        );
 | 
			
		||||
      case 2:
 | 
			
		||||
        return (
 | 
			
		||||
          <Tag size="large" color="yellow">
 | 
			
		||||
          <Tag size='large' color='yellow'>
 | 
			
		||||
            已禁用
 | 
			
		||||
          </Tag>
 | 
			
		||||
        );
 | 
			
		||||
      case 3:
 | 
			
		||||
        return (
 | 
			
		||||
          <Tag size="large" color="yellow">
 | 
			
		||||
          <Tag size='large' color='yellow'>
 | 
			
		||||
            自动禁用
 | 
			
		||||
          </Tag>
 | 
			
		||||
        );
 | 
			
		||||
      default:
 | 
			
		||||
        return (
 | 
			
		||||
          <Tag size="large" color="grey">
 | 
			
		||||
          <Tag size='large' color='grey'>
 | 
			
		||||
            未知状态
 | 
			
		||||
          </Tag>
 | 
			
		||||
        );
 | 
			
		||||
@@ -400,15 +437,35 @@ const ChannelsTable = () => {
 | 
			
		||||
    let time = responseTime / 1000;
 | 
			
		||||
    time = time.toFixed(2) + ' 秒';
 | 
			
		||||
    if (responseTime === 0) {
 | 
			
		||||
      return <Tag size="large" color="grey">未测试</Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag size='large' color='grey'>
 | 
			
		||||
          未测试
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    } else if (responseTime <= 1000) {
 | 
			
		||||
      return <Tag size="large" color="green">{time}</Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag size='large' color='green'>
 | 
			
		||||
          {time}
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    } else if (responseTime <= 3000) {
 | 
			
		||||
      return <Tag size="large" color="lime">{time}</Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag size='large' color='lime'>
 | 
			
		||||
          {time}
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    } else if (responseTime <= 5000) {
 | 
			
		||||
      return <Tag size="large" color="yellow">{time}</Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag size='large' color='yellow'>
 | 
			
		||||
          {time}
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
      return <Tag size="large" color="red">{time}</Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag size='large' color='red'>
 | 
			
		||||
          {time}
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@@ -420,7 +477,9 @@ const ChannelsTable = () => {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    setSearching(true);
 | 
			
		||||
    const res = await API.get(`/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}&model=${searchModel}`);
 | 
			
		||||
    const res = await API.get(
 | 
			
		||||
      `/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}&model=${searchModel}`,
 | 
			
		||||
    );
 | 
			
		||||
    const { success, message, data } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      setChannels(data);
 | 
			
		||||
@@ -520,14 +579,16 @@ const ChannelsTable = () => {
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  let pageData = channels.slice((activePage - 1) * pageSize, activePage * pageSize);
 | 
			
		||||
  let pageData = channels.slice(
 | 
			
		||||
    (activePage - 1) * pageSize,
 | 
			
		||||
    activePage * pageSize,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const handlePageChange = page => {
 | 
			
		||||
  const handlePageChange = (page) => {
 | 
			
		||||
    setActivePage(page);
 | 
			
		||||
    if (page === Math.ceil(channels.length / pageSize) + 1) {
 | 
			
		||||
      // In this case we have to load more data and then append them.
 | 
			
		||||
      loadChannels(page - 1, pageSize, idSort).then(r => {
 | 
			
		||||
      });
 | 
			
		||||
      loadChannels(page - 1, pageSize, idSort).then((r) => {});
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@@ -547,10 +608,12 @@ const ChannelsTable = () => {
 | 
			
		||||
      let res = await API.get(`/api/group/`);
 | 
			
		||||
      // add 'all' option
 | 
			
		||||
      // res.data.data.unshift('all');
 | 
			
		||||
      setGroupOptions(res.data.data.map((group) => ({
 | 
			
		||||
        label: group,
 | 
			
		||||
        value: group
 | 
			
		||||
      })));
 | 
			
		||||
      setGroupOptions(
 | 
			
		||||
        res.data.data.map((group) => ({
 | 
			
		||||
          label: group,
 | 
			
		||||
          value: group,
 | 
			
		||||
        })),
 | 
			
		||||
      );
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      showError(error.message);
 | 
			
		||||
    }
 | 
			
		||||
@@ -564,27 +627,34 @@ const ChannelsTable = () => {
 | 
			
		||||
    if (record.status !== 1) {
 | 
			
		||||
      return {
 | 
			
		||||
        style: {
 | 
			
		||||
          background: 'var(--semi-color-disabled-border)'
 | 
			
		||||
        }
 | 
			
		||||
          background: 'var(--semi-color-disabled-border)',
 | 
			
		||||
        },
 | 
			
		||||
      };
 | 
			
		||||
    } else {
 | 
			
		||||
      return {};
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <EditChannel refresh={refresh} visible={showEdit} handleClose={closeEdit} editingChannel={editingChannel} />
 | 
			
		||||
      <Form onSubmit={() => {
 | 
			
		||||
        searchChannels(searchKeyword, searchGroup, searchModel);
 | 
			
		||||
      }} labelPosition="left">
 | 
			
		||||
      <EditChannel
 | 
			
		||||
        refresh={refresh}
 | 
			
		||||
        visible={showEdit}
 | 
			
		||||
        handleClose={closeEdit}
 | 
			
		||||
        editingChannel={editingChannel}
 | 
			
		||||
      />
 | 
			
		||||
      <Form
 | 
			
		||||
        onSubmit={() => {
 | 
			
		||||
          searchChannels(searchKeyword, searchGroup, searchModel);
 | 
			
		||||
        }}
 | 
			
		||||
        labelPosition='left'
 | 
			
		||||
      >
 | 
			
		||||
        <div style={{ display: 'flex' }}>
 | 
			
		||||
          <Space>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              field="search_keyword"
 | 
			
		||||
              label="搜索渠道关键词"
 | 
			
		||||
              placeholder="ID,名称和密钥 ..."
 | 
			
		||||
              field='search_keyword'
 | 
			
		||||
              label='搜索渠道关键词'
 | 
			
		||||
              placeholder='ID,名称和密钥 ...'
 | 
			
		||||
              value={searchKeyword}
 | 
			
		||||
              loading={searching}
 | 
			
		||||
              onChange={(v) => {
 | 
			
		||||
@@ -592,21 +662,33 @@ const ChannelsTable = () => {
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              field="search_model"
 | 
			
		||||
              label="模型"
 | 
			
		||||
              placeholder="模型关键字"
 | 
			
		||||
              field='search_model'
 | 
			
		||||
              label='模型'
 | 
			
		||||
              placeholder='模型关键字'
 | 
			
		||||
              value={searchModel}
 | 
			
		||||
              loading={searching}
 | 
			
		||||
              onChange={(v) => {
 | 
			
		||||
                setSearchModel(v.trim());
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Select field="group" label="分组" optionList={groupOptions} onChange={(v) => {
 | 
			
		||||
              setSearchGroup(v);
 | 
			
		||||
              searchChannels(searchKeyword, v, searchModel);
 | 
			
		||||
            }} />
 | 
			
		||||
            <Button label="查询" type="primary" htmlType="submit" className="btn-margin-right"
 | 
			
		||||
                    style={{ marginRight: 8 }}>查询</Button>
 | 
			
		||||
            <Form.Select
 | 
			
		||||
              field='group'
 | 
			
		||||
              label='分组'
 | 
			
		||||
              optionList={groupOptions}
 | 
			
		||||
              onChange={(v) => {
 | 
			
		||||
                setSearchGroup(v);
 | 
			
		||||
                searchChannels(searchKeyword, v, searchModel);
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
            <Button
 | 
			
		||||
              label='查询'
 | 
			
		||||
              type='primary'
 | 
			
		||||
              htmlType='submit'
 | 
			
		||||
              className='btn-margin-right'
 | 
			
		||||
              style={{ marginRight: 8 }}
 | 
			
		||||
            >
 | 
			
		||||
              查询
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Space>
 | 
			
		||||
        </div>
 | 
			
		||||
      </Form>
 | 
			
		||||
@@ -614,80 +696,118 @@ const ChannelsTable = () => {
 | 
			
		||||
        <Space>
 | 
			
		||||
          <Space>
 | 
			
		||||
            <Typography.Text strong>使用ID排序</Typography.Text>
 | 
			
		||||
            <Switch checked={idSort} label="使用ID排序" uncheckedText="关" aria-label="是否用ID排序" onChange={(v) => {
 | 
			
		||||
              localStorage.setItem('id-sort', v + '');
 | 
			
		||||
              setIdSort(v);
 | 
			
		||||
              loadChannels(0, pageSize, v)
 | 
			
		||||
                .then()
 | 
			
		||||
                .catch((reason) => {
 | 
			
		||||
                  showError(reason);
 | 
			
		||||
                });
 | 
			
		||||
            }}></Switch>
 | 
			
		||||
            <Switch
 | 
			
		||||
              checked={idSort}
 | 
			
		||||
              label='使用ID排序'
 | 
			
		||||
              uncheckedText='关'
 | 
			
		||||
              aria-label='是否用ID排序'
 | 
			
		||||
              onChange={(v) => {
 | 
			
		||||
                localStorage.setItem('id-sort', v + '');
 | 
			
		||||
                setIdSort(v);
 | 
			
		||||
                loadChannels(0, pageSize, v)
 | 
			
		||||
                  .then()
 | 
			
		||||
                  .catch((reason) => {
 | 
			
		||||
                    showError(reason);
 | 
			
		||||
                  });
 | 
			
		||||
              }}
 | 
			
		||||
            ></Switch>
 | 
			
		||||
          </Space>
 | 
			
		||||
        </Space>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <Table className={'channel-table'} style={{ marginTop: 15 }} columns={columns} dataSource={pageData} pagination={{
 | 
			
		||||
        currentPage: activePage,
 | 
			
		||||
        pageSize: pageSize,
 | 
			
		||||
        total: channelCount,
 | 
			
		||||
        pageSizeOpts: [10, 20, 50, 100],
 | 
			
		||||
        showSizeChanger: true,
 | 
			
		||||
        formatPageText: (page) => '',
 | 
			
		||||
        onPageSizeChange: (size) => {
 | 
			
		||||
          handlePageSizeChange(size).then();
 | 
			
		||||
        },
 | 
			
		||||
        onPageChange: handlePageChange
 | 
			
		||||
      }} loading={loading} onRow={handleRow} rowSelection={
 | 
			
		||||
        enableBatchDelete ?
 | 
			
		||||
          {
 | 
			
		||||
            onChange: (selectedRowKeys, selectedRows) => {
 | 
			
		||||
              // console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
 | 
			
		||||
              setSelectedChannels(selectedRows);
 | 
			
		||||
            }
 | 
			
		||||
          } : null
 | 
			
		||||
      } />
 | 
			
		||||
      <div style={{
 | 
			
		||||
        display: isMobile() ? '' : 'flex',
 | 
			
		||||
        marginTop: isMobile() ? 0 : -45,
 | 
			
		||||
        zIndex: 999,
 | 
			
		||||
        position: 'relative',
 | 
			
		||||
        pointerEvents: 'none'
 | 
			
		||||
      }}>
 | 
			
		||||
        <Space style={{ pointerEvents: 'auto', marginTop: isMobile() ? 0 : 45 }}>
 | 
			
		||||
          <Button theme="light" type="primary" style={{ marginRight: 8 }} onClick={
 | 
			
		||||
            () => {
 | 
			
		||||
      <Table
 | 
			
		||||
        className={'channel-table'}
 | 
			
		||||
        style={{ marginTop: 15 }}
 | 
			
		||||
        columns={columns}
 | 
			
		||||
        dataSource={pageData}
 | 
			
		||||
        pagination={{
 | 
			
		||||
          currentPage: activePage,
 | 
			
		||||
          pageSize: pageSize,
 | 
			
		||||
          total: channelCount,
 | 
			
		||||
          pageSizeOpts: [10, 20, 50, 100],
 | 
			
		||||
          showSizeChanger: true,
 | 
			
		||||
          formatPageText: (page) => '',
 | 
			
		||||
          onPageSizeChange: (size) => {
 | 
			
		||||
            handlePageSizeChange(size).then();
 | 
			
		||||
          },
 | 
			
		||||
          onPageChange: handlePageChange,
 | 
			
		||||
        }}
 | 
			
		||||
        loading={loading}
 | 
			
		||||
        onRow={handleRow}
 | 
			
		||||
        rowSelection={
 | 
			
		||||
          enableBatchDelete
 | 
			
		||||
            ? {
 | 
			
		||||
                onChange: (selectedRowKeys, selectedRows) => {
 | 
			
		||||
                  // console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows);
 | 
			
		||||
                  setSelectedChannels(selectedRows);
 | 
			
		||||
                },
 | 
			
		||||
              }
 | 
			
		||||
            : null
 | 
			
		||||
        }
 | 
			
		||||
      />
 | 
			
		||||
      <div
 | 
			
		||||
        style={{
 | 
			
		||||
          display: isMobile() ? '' : 'flex',
 | 
			
		||||
          marginTop: isMobile() ? 0 : -45,
 | 
			
		||||
          zIndex: 999,
 | 
			
		||||
          position: 'relative',
 | 
			
		||||
          pointerEvents: 'none',
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        <Space
 | 
			
		||||
          style={{ pointerEvents: 'auto', marginTop: isMobile() ? 0 : 45 }}
 | 
			
		||||
        >
 | 
			
		||||
          <Button
 | 
			
		||||
            theme='light'
 | 
			
		||||
            type='primary'
 | 
			
		||||
            style={{ marginRight: 8 }}
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              setEditingChannel({
 | 
			
		||||
                id: undefined
 | 
			
		||||
                id: undefined,
 | 
			
		||||
              });
 | 
			
		||||
              setShowEdit(true);
 | 
			
		||||
            }
 | 
			
		||||
          }>添加渠道</Button>
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            添加渠道
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Popconfirm
 | 
			
		||||
            title="确定?"
 | 
			
		||||
            title='确定?'
 | 
			
		||||
            okType={'warning'}
 | 
			
		||||
            onConfirm={testAllChannels}
 | 
			
		||||
            position={isMobile() ? 'top' : 'top'}
 | 
			
		||||
          >
 | 
			
		||||
            <Button theme="light" type="warning" style={{ marginRight: 8 }}>测试所有通道</Button>
 | 
			
		||||
            <Button theme='light' type='warning' style={{ marginRight: 8 }}>
 | 
			
		||||
              测试所有通道
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Popconfirm>
 | 
			
		||||
          <Popconfirm
 | 
			
		||||
            title="确定?"
 | 
			
		||||
            title='确定?'
 | 
			
		||||
            okType={'secondary'}
 | 
			
		||||
            onConfirm={updateAllChannelsBalance}
 | 
			
		||||
          >
 | 
			
		||||
            <Button theme="light" type="secondary" style={{ marginRight: 8 }}>更新所有已启用通道余额</Button>
 | 
			
		||||
            <Button theme='light' type='secondary' style={{ marginRight: 8 }}>
 | 
			
		||||
              更新所有已启用通道余额
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Popconfirm>
 | 
			
		||||
          <Popconfirm
 | 
			
		||||
            title="确定是否要删除禁用通道?"
 | 
			
		||||
            content="此修改将不可逆"
 | 
			
		||||
            title='确定是否要删除禁用通道?'
 | 
			
		||||
            content='此修改将不可逆'
 | 
			
		||||
            okType={'danger'}
 | 
			
		||||
            onConfirm={deleteAllDisabledChannels}
 | 
			
		||||
          >
 | 
			
		||||
            <Button theme="light" type="danger" style={{ marginRight: 8 }}>删除禁用通道</Button>
 | 
			
		||||
            <Button theme='light' type='danger' style={{ marginRight: 8 }}>
 | 
			
		||||
              删除禁用通道
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Popconfirm>
 | 
			
		||||
 | 
			
		||||
          <Button theme="light" type="primary" style={{ marginRight: 8 }} onClick={refresh}>刷新</Button>
 | 
			
		||||
          <Button
 | 
			
		||||
            theme='light'
 | 
			
		||||
            type='primary'
 | 
			
		||||
            style={{ marginRight: 8 }}
 | 
			
		||||
            onClick={refresh}
 | 
			
		||||
          >
 | 
			
		||||
            刷新
 | 
			
		||||
          </Button>
 | 
			
		||||
        </Space>
 | 
			
		||||
        {/*<div style={{width: '100%', pointerEvents: 'none', position: 'absolute'}}>*/}
 | 
			
		||||
 | 
			
		||||
@@ -696,28 +816,41 @@ const ChannelsTable = () => {
 | 
			
		||||
      <div style={{ marginTop: 20 }}>
 | 
			
		||||
        <Space>
 | 
			
		||||
          <Typography.Text strong>开启批量删除</Typography.Text>
 | 
			
		||||
          <Switch label="开启批量删除" uncheckedText="关" aria-label="是否开启批量删除" onChange={(v) => {
 | 
			
		||||
            setEnableBatchDelete(v);
 | 
			
		||||
          }}></Switch>
 | 
			
		||||
          <Switch
 | 
			
		||||
            label='开启批量删除'
 | 
			
		||||
            uncheckedText='关'
 | 
			
		||||
            aria-label='是否开启批量删除'
 | 
			
		||||
            onChange={(v) => {
 | 
			
		||||
              setEnableBatchDelete(v);
 | 
			
		||||
            }}
 | 
			
		||||
          ></Switch>
 | 
			
		||||
          <Popconfirm
 | 
			
		||||
            title="确定是否要删除所选通道?"
 | 
			
		||||
            content="此修改将不可逆"
 | 
			
		||||
            title='确定是否要删除所选通道?'
 | 
			
		||||
            content='此修改将不可逆'
 | 
			
		||||
            okType={'danger'}
 | 
			
		||||
            onConfirm={batchDeleteChannels}
 | 
			
		||||
            disabled={!enableBatchDelete}
 | 
			
		||||
            position={'top'}
 | 
			
		||||
          >
 | 
			
		||||
            <Button disabled={!enableBatchDelete} theme="light" type="danger"
 | 
			
		||||
                    style={{ marginRight: 8 }}>删除所选通道</Button>
 | 
			
		||||
            <Button
 | 
			
		||||
              disabled={!enableBatchDelete}
 | 
			
		||||
              theme='light'
 | 
			
		||||
              type='danger'
 | 
			
		||||
              style={{ marginRight: 8 }}
 | 
			
		||||
            >
 | 
			
		||||
              删除所选通道
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Popconfirm>
 | 
			
		||||
          <Popconfirm
 | 
			
		||||
            title="确定是否要修复数据库一致性?"
 | 
			
		||||
            content="进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用"
 | 
			
		||||
            title='确定是否要修复数据库一致性?'
 | 
			
		||||
            content='进行该操作时,可能导致渠道访问错误,请仅在数据库出现问题时使用'
 | 
			
		||||
            okType={'warning'}
 | 
			
		||||
            onConfirm={fixChannelsAbilities}
 | 
			
		||||
            position={'top'}
 | 
			
		||||
          >
 | 
			
		||||
            <Button theme="light" type="secondary" style={{ marginRight: 8 }}>修复数据库一致性</Button>
 | 
			
		||||
            <Button theme='light' type='secondary' style={{ marginRight: 8 }}>
 | 
			
		||||
              修复数据库一致性
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Popconfirm>
 | 
			
		||||
        </Space>
 | 
			
		||||
      </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -32,27 +32,36 @@ const Footer = () => {
 | 
			
		||||
      <Layout.Content style={{ textAlign: 'center' }}>
 | 
			
		||||
        {footer ? (
 | 
			
		||||
          <div
 | 
			
		||||
            className="custom-footer"
 | 
			
		||||
            className='custom-footer'
 | 
			
		||||
            dangerouslySetInnerHTML={{ __html: footer }}
 | 
			
		||||
          ></div>
 | 
			
		||||
        ) : (
 | 
			
		||||
          <div className="custom-footer">
 | 
			
		||||
          <div className='custom-footer'>
 | 
			
		||||
            <a
 | 
			
		||||
              href="https://github.com/Calcium-Ion/new-api"
 | 
			
		||||
              target="_blank" rel="noreferrer"
 | 
			
		||||
              href='https://github.com/Calcium-Ion/new-api'
 | 
			
		||||
              target='_blank'
 | 
			
		||||
              rel='noreferrer'
 | 
			
		||||
            >
 | 
			
		||||
              New API {process.env.REACT_APP_VERSION}{' '}
 | 
			
		||||
              New API {import.meta.env.VITE_REACT_APP_VERSION}{' '}
 | 
			
		||||
            </a>
 | 
			
		||||
            由{' '}
 | 
			
		||||
            <a href="https://github.com/Calcium-Ion" target="_blank" rel="noreferrer">
 | 
			
		||||
            <a
 | 
			
		||||
              href='https://github.com/Calcium-Ion'
 | 
			
		||||
              target='_blank'
 | 
			
		||||
              rel='noreferrer'
 | 
			
		||||
            >
 | 
			
		||||
              Calcium-Ion
 | 
			
		||||
            </a>{' '}
 | 
			
		||||
            开发,基于{' '}
 | 
			
		||||
            <a href="https://github.com/songquanpeng/one-api" target="_blank" rel="noreferrer">
 | 
			
		||||
            <a
 | 
			
		||||
              href='https://github.com/songquanpeng/one-api'
 | 
			
		||||
              target='_blank'
 | 
			
		||||
              rel='noreferrer'
 | 
			
		||||
            >
 | 
			
		||||
              One API v0.5.4
 | 
			
		||||
            </a>{' '}
 | 
			
		||||
            ,本项目根据{' '}
 | 
			
		||||
            <a href="https://opensource.org/licenses/mit-license.php">
 | 
			
		||||
            <a href='https://opensource.org/licenses/mit-license.php'>
 | 
			
		||||
              MIT 许可证
 | 
			
		||||
            </a>{' '}
 | 
			
		||||
            授权
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,9 @@ const GitHubOAuth = () => {
 | 
			
		||||
 | 
			
		||||
  const sendCode = async (code, state, count) => {
 | 
			
		||||
    let aff = localStorage.getItem('aff');
 | 
			
		||||
    const res = await API.get(`/api/oauth/github?code=${code}&state=${state}&aff=${aff}`);
 | 
			
		||||
    const res = await API.get(
 | 
			
		||||
      `/api/oauth/github?code=${code}&state=${state}&aff=${aff}`,
 | 
			
		||||
    );
 | 
			
		||||
    const { success, message, data } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      localStorage.removeItem('aff');
 | 
			
		||||
@@ -60,7 +62,7 @@ const GitHubOAuth = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <Segment style={{ minHeight: '300px' }}>
 | 
			
		||||
      <Dimmer active inverted>
 | 
			
		||||
        <Loader size="large">{prompt}</Loader>
 | 
			
		||||
        <Loader size='large'>{prompt}</Loader>
 | 
			
		||||
      </Dimmer>
 | 
			
		||||
    </Segment>
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
@@ -17,15 +17,15 @@ let headerButtons = [
 | 
			
		||||
    text: '关于',
 | 
			
		||||
    itemKey: 'about',
 | 
			
		||||
    to: '/about',
 | 
			
		||||
    icon: <IconHelpCircle />
 | 
			
		||||
  }
 | 
			
		||||
    icon: <IconHelpCircle />,
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
if (localStorage.getItem('chat_link')) {
 | 
			
		||||
  headerButtons.splice(1, 0, {
 | 
			
		||||
    name: '聊天',
 | 
			
		||||
    to: '/chat',
 | 
			
		||||
    icon: 'comments'
 | 
			
		||||
    icon: 'comments',
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -40,7 +40,11 @@ const HeaderBar = () => {
 | 
			
		||||
  var themeMode = localStorage.getItem('theme-mode');
 | 
			
		||||
  const currentDate = new Date();
 | 
			
		||||
  // enable fireworks on new year(1.1 and 2.9-2.24)
 | 
			
		||||
  const isNewYear = (currentDate.getMonth() === 0 && currentDate.getDate() === 1) || (currentDate.getMonth() === 1 && currentDate.getDate() >= 9 && currentDate.getDate() <= 24);
 | 
			
		||||
  const isNewYear =
 | 
			
		||||
    (currentDate.getMonth() === 0 && currentDate.getDate() === 1) ||
 | 
			
		||||
    (currentDate.getMonth() === 1 &&
 | 
			
		||||
      currentDate.getDate() >= 9 &&
 | 
			
		||||
      currentDate.getDate() <= 24);
 | 
			
		||||
 | 
			
		||||
  async function logout() {
 | 
			
		||||
    setShowSidebar(false);
 | 
			
		||||
@@ -93,7 +97,7 @@ const HeaderBar = () => {
 | 
			
		||||
              const routerMap = {
 | 
			
		||||
                about: '/about',
 | 
			
		||||
                login: '/login',
 | 
			
		||||
                register: '/register'
 | 
			
		||||
                register: '/register',
 | 
			
		||||
              };
 | 
			
		||||
              return (
 | 
			
		||||
                <Link
 | 
			
		||||
@@ -106,52 +110,69 @@ const HeaderBar = () => {
 | 
			
		||||
            }}
 | 
			
		||||
            selectedKeys={[]}
 | 
			
		||||
            // items={headerButtons}
 | 
			
		||||
            onSelect={key => {
 | 
			
		||||
 | 
			
		||||
            }}
 | 
			
		||||
            onSelect={(key) => {}}
 | 
			
		||||
            footer={
 | 
			
		||||
              <>
 | 
			
		||||
                {isNewYear &&
 | 
			
		||||
                {isNewYear && (
 | 
			
		||||
                  // happy new year
 | 
			
		||||
                  <Dropdown
 | 
			
		||||
                    position="bottomRight"
 | 
			
		||||
                    position='bottomRight'
 | 
			
		||||
                    render={
 | 
			
		||||
                      <Dropdown.Menu>
 | 
			
		||||
                        <Dropdown.Item onClick={handleNewYearClick}>Happy New Year!!!</Dropdown.Item>
 | 
			
		||||
                        <Dropdown.Item onClick={handleNewYearClick}>
 | 
			
		||||
                          Happy New Year!!!
 | 
			
		||||
                        </Dropdown.Item>
 | 
			
		||||
                      </Dropdown.Menu>
 | 
			
		||||
                    }
 | 
			
		||||
                  >
 | 
			
		||||
                    <Nav.Item itemKey={'new-year'} text={'🏮'} />
 | 
			
		||||
                  </Dropdown>
 | 
			
		||||
                }
 | 
			
		||||
                )}
 | 
			
		||||
                <Nav.Item itemKey={'about'} icon={<IconHelpCircle />} />
 | 
			
		||||
                <Switch checkedText="🌞" size={'large'} checked={dark} uncheckedText="🌙" onChange={switchMode} />
 | 
			
		||||
                {userState.user ?
 | 
			
		||||
                <Switch
 | 
			
		||||
                  checkedText='🌞'
 | 
			
		||||
                  size={'large'}
 | 
			
		||||
                  checked={dark}
 | 
			
		||||
                  uncheckedText='🌙'
 | 
			
		||||
                  onChange={switchMode}
 | 
			
		||||
                />
 | 
			
		||||
                {userState.user ? (
 | 
			
		||||
                  <>
 | 
			
		||||
                    <Dropdown
 | 
			
		||||
                      position="bottomRight"
 | 
			
		||||
                      position='bottomRight'
 | 
			
		||||
                      render={
 | 
			
		||||
                        <Dropdown.Menu>
 | 
			
		||||
                          <Dropdown.Item onClick={logout}>退出</Dropdown.Item>
 | 
			
		||||
                        </Dropdown.Menu>
 | 
			
		||||
                      }
 | 
			
		||||
                    >
 | 
			
		||||
                      <Avatar size="small" color={stringToColor(userState.user.username)} style={{ margin: 4 }}>
 | 
			
		||||
                      <Avatar
 | 
			
		||||
                        size='small'
 | 
			
		||||
                        color={stringToColor(userState.user.username)}
 | 
			
		||||
                        style={{ margin: 4 }}
 | 
			
		||||
                      >
 | 
			
		||||
                        {userState.user.username[0]}
 | 
			
		||||
                      </Avatar>
 | 
			
		||||
                      <span>{userState.user.username}</span>
 | 
			
		||||
                    </Dropdown>
 | 
			
		||||
                  </>
 | 
			
		||||
                  :
 | 
			
		||||
                ) : (
 | 
			
		||||
                  <>
 | 
			
		||||
                    <Nav.Item itemKey={'login'} text={'登录'} icon={<IconKey />} />
 | 
			
		||||
                    <Nav.Item itemKey={'register'} text={'注册'} icon={<IconUser />} />
 | 
			
		||||
                    <Nav.Item
 | 
			
		||||
                      itemKey={'login'}
 | 
			
		||||
                      text={'登录'}
 | 
			
		||||
                      icon={<IconKey />}
 | 
			
		||||
                    />
 | 
			
		||||
                    <Nav.Item
 | 
			
		||||
                      itemKey={'register'}
 | 
			
		||||
                      text={'注册'}
 | 
			
		||||
                      icon={<IconUser />}
 | 
			
		||||
                    />
 | 
			
		||||
                  </>
 | 
			
		||||
                }
 | 
			
		||||
                )}
 | 
			
		||||
              </>
 | 
			
		||||
            }
 | 
			
		||||
          >
 | 
			
		||||
          </Nav>
 | 
			
		||||
          ></Nav>
 | 
			
		||||
        </div>
 | 
			
		||||
      </Layout>
 | 
			
		||||
    </>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,21 +1,31 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import {Icon} from '@douyinfe/semi-ui';
 | 
			
		||||
import { Icon } from '@douyinfe/semi-ui';
 | 
			
		||||
 | 
			
		||||
const LinuxDoIcon = (props) => {
 | 
			
		||||
    function CustomIcon() {
 | 
			
		||||
        return <svg className='icon' viewBox='0 0 24 24' version='1.1'
 | 
			
		||||
                    xmlns='http://www.w3.org/2000/svg' width='16' height='16' {...props}>
 | 
			
		||||
            <path
 | 
			
		||||
                d="M19.7,17.6c-0.1-0.2-0.2-0.4-0.2-0.6c0-0.4-0.2-0.7-0.5-1c-0.1-0.1-0.3-0.2-0.4-0.2c0.6-1.8-0.3-3.6-1.3-4.9c0,0,0,0,0,0c-0.8-1.2-2-2.1-1.9-3.7c0-1.9,0.2-5.4-3.3-5.1C8.5,2.3,9.5,6,9.4,7.3c0,1.1-0.5,2.2-1.3,3.1c-0.2,0.2-0.4,0.5-0.5,0.7c-1,1.2-1.5,2.8-1.5,4.3c-0.2,0.2-0.4,0.4-0.5,0.6c-0.1,0.1-0.2,0.2-0.2,0.3c-0.1,0.1-0.3,0.2-0.5,0.3c-0.4,0.1-0.7,0.3-0.9,0.7c-0.1,0.3-0.2,0.7-0.1,1.1c0.1,0.2,0.1,0.4,0,0.7c-0.2,0.4-0.2,0.9,0,1.4c0.3,0.4,0.8,0.5,1.5,0.6c0.5,0,1.1,0.2,1.6,0.4l0,0c0.5,0.3,1.1,0.5,1.7,0.5c0.3,0,0.7-0.1,1-0.2c0.3-0.2,0.5-0.4,0.6-0.7c0.4,0,1-0.2,1.7-0.2c0.6,0,1.2,0.2,2,0.1c0,0.1,0,0.2,0.1,0.3c0.2,0.5,0.7,0.9,1.3,1c0.1,0,0.1,0,0.2,0c0.8-0.1,1.6-0.5,2.1-1.1l0,0c0.4-0.4,0.9-0.7,1.4-0.9c0.6-0.3,1-0.5,1.1-1C20.3,18.6,20.1,18.2,19.7,17.6z M12.8,4.8c0.6,0.1,1.1,0.6,1,1.2c0,0.3-0.1,0.6-0.3,0.9c0,0,0,0-0.1,0c-0.2-0.1-0.3-0.1-0.4-0.2c0.1-0.1,0.1-0.3,0.2-0.5c0-0.4-0.2-0.7-0.4-0.7c-0.3,0-0.5,0.3-0.5,0.7c0,0,0,0.1,0,0.1c-0.1-0.1-0.3-0.1-0.4-0.2c0,0,0-0.1,0-0.1C11.8,5.5,12.2,4.9,12.8,4.8z M12.5,6.8c0.1,0.1,0.3,0.2,0.4,0.2c0.1,0,0.3,0.1,0.4,0.2c0.2,0.1,0.4,0.2,0.4,0.5c0,0.3-0.3,0.6-0.9,0.8c-0.2,0.1-0.3,0.1-0.4,0.2c-0.3,0.2-0.6,0.3-1,0.3c-0.3,0-0.6-0.2-0.8-0.4c-0.1-0.1-0.2-0.2-0.4-0.3C10.1,8.2,9.9,8,9.8,7.7c0-0.1,0.1-0.2,0.2-0.3c0.3-0.2,0.4-0.3,0.5-0.4l0.1-0.1c0.2-0.3,0.6-0.5,1-0.5C11.9,6.5,12.2,6.6,12.5,6.8z M10.4,5c0.4,0,0.7,0.4,0.8,1.1c0,0.1,0,0.1,0,0.2c-0.1,0-0.3,0.1-0.4,0.2c0,0,0-0.1,0-0.2c0-0.3-0.2-0.6-0.4-0.5c-0.2,0-0.3,0.3-0.3,0.6c0,0.2,0.1,0.3,0.2,0.4l0,0c0,0-0.1,0.1-0.2,0.1C9.9,6.7,9.7,6.4,9.7,6.1C9.7,5.5,10,5,10.4,5z M9.4,21.1c-0.7,0.3-1.6,0.2-2.2-0.2c-0.6-0.3-1.1-0.4-1.8-0.4c-0.5-0.1-1-0.1-1.1-0.3c-0.1-0.2-0.1-0.5,0.1-1c0.1-0.3,0.1-0.6,0-0.9c-0.1-0.3-0.1-0.5,0-0.8C4.5,17.2,4.7,17.1,5,17c0.3-0.1,0.5-0.2,0.7-0.4c0.1-0.1,0.2-0.2,0.3-0.4c0.3-0.4,0.5-0.6,0.8-0.6c0.6,0.1,1.1,1,1.5,1.9c0.2,0.3,0.4,0.7,0.7,1c0.4,0.5,0.9,1.2,0.9,1.6C9.9,20.6,9.7,20.9,9.4,21.1z M14.3,18.9c0,0.1,0,0.1-0.1,0.2c-1.2,0.9-2.8,1-4.1,0.3c-0.2-0.3-0.4-0.6-0.6-0.9c0.9-0.1,0.7-1.3-1.2-2.5c-2-1.3-0.6-3.7,0.1-4.8c0.1-0.1,0.1,0-0.3,0.8c-0.3,0.6-0.9,2.1-0.1,3.2c0-0.8,0.2-1.6,0.5-2.4c0.7-1.3,1.2-2.8,1.5-4.3c0.1,0.1,0.1,0.1,0.2,0.1c0.1,0.1,0.2,0.2,0.3,0.2c0.2,0.3,0.6,0.4,0.9,0.4c0,0,0.1,0,0.1,0c0.4,0,0.8-0.1,1.1-0.4c0.1-0.1,0.2-0.2,0.4-0.2c0.3-0.1,0.6-0.3,0.9-0.6c0.4,1.3,0.8,2.5,1.4,3.6c0.4,0.8,0.7,1.6,0.9,2.5c0.3,0,0.7,0.1,1,0.3c0.8,0.4,1.1,0.7,1,1.2c-0.1,0-0.1,0-0.2,0c0-0.3-0.2-0.6-0.9-0.9c-0.7-0.3-1.3-0.3-1.5,0.4c-0.1,0-0.2,0.1-0.3,0.1c-0.8,0.4-0.8,1.5-0.9,2.6C14.5,18.2,14.4,18.5,14.3,18.9z M18.9,19.5c-0.6,0.2-1.1,0.6-1.5,1.1c-0.4,0.6-1.1,1-1.9,0.9c-0.4,0-0.8-0.3-0.9-0.7c-0.1-0.6-0.1-1.2,0.2-1.8c0.1-0.4,0.2-0.7,0.3-1.1c0.1-1.2,0.1-1.9,0.6-2.2h0c0,0.5,0.3,0.8,0.7,1c0.5,0,1-0.1,1.4-0.5c0.1,0,0.1,0,0.2,0c0.3,0,0.5,0,0.7,0.2c0.2,0.2,0.3,0.5,0.3,0.7c0,0.3,0.2,0.6,0.3,0.9c0.5,0.5,0.5,0.8,0.5,0.9C19.7,19.1,19.3,19.3,18.9,19.5z M9.9,7.5c-0.1,0-0.1,0-0.1,0.1c0,0,0,0.1,0.1,0.1c0,0,0,0,0,0c0.1,0,0.1,0.1,0.1,0.1c0.3,0.4,0.8,0.6,1.4,0.7c0.5-0.1,1-0.2,1.5-0.6c0.2-0.1,0.4-0.2,0.6-0.3c0.1,0,0.1-0.1,0.1-0.1c0-0.1,0-0.1-0.1-0.1l0,0c-0.2,0.1-0.5,0.2-0.7,0.3c-0.4,0.3-0.9,0.5-1.4,0.5c-0.5,0-0.9-0.3-1.2-0.6C10.1,7.6,10,7.5,9.9,7.5z"
 | 
			
		||||
                fill="currentColor"/>
 | 
			
		||||
        </svg>;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  function CustomIcon() {
 | 
			
		||||
    return (
 | 
			
		||||
        <div>
 | 
			
		||||
            <Icon svg={<CustomIcon/>}/>
 | 
			
		||||
        </div>
 | 
			
		||||
      <svg
 | 
			
		||||
        className='icon'
 | 
			
		||||
        viewBox='0 0 24 24'
 | 
			
		||||
        version='1.1'
 | 
			
		||||
        xmlns='http://www.w3.org/2000/svg'
 | 
			
		||||
        width='16'
 | 
			
		||||
        height='16'
 | 
			
		||||
        {...props}
 | 
			
		||||
      >
 | 
			
		||||
        <path
 | 
			
		||||
          d='M19.7,17.6c-0.1-0.2-0.2-0.4-0.2-0.6c0-0.4-0.2-0.7-0.5-1c-0.1-0.1-0.3-0.2-0.4-0.2c0.6-1.8-0.3-3.6-1.3-4.9c0,0,0,0,0,0c-0.8-1.2-2-2.1-1.9-3.7c0-1.9,0.2-5.4-3.3-5.1C8.5,2.3,9.5,6,9.4,7.3c0,1.1-0.5,2.2-1.3,3.1c-0.2,0.2-0.4,0.5-0.5,0.7c-1,1.2-1.5,2.8-1.5,4.3c-0.2,0.2-0.4,0.4-0.5,0.6c-0.1,0.1-0.2,0.2-0.2,0.3c-0.1,0.1-0.3,0.2-0.5,0.3c-0.4,0.1-0.7,0.3-0.9,0.7c-0.1,0.3-0.2,0.7-0.1,1.1c0.1,0.2,0.1,0.4,0,0.7c-0.2,0.4-0.2,0.9,0,1.4c0.3,0.4,0.8,0.5,1.5,0.6c0.5,0,1.1,0.2,1.6,0.4l0,0c0.5,0.3,1.1,0.5,1.7,0.5c0.3,0,0.7-0.1,1-0.2c0.3-0.2,0.5-0.4,0.6-0.7c0.4,0,1-0.2,1.7-0.2c0.6,0,1.2,0.2,2,0.1c0,0.1,0,0.2,0.1,0.3c0.2,0.5,0.7,0.9,1.3,1c0.1,0,0.1,0,0.2,0c0.8-0.1,1.6-0.5,2.1-1.1l0,0c0.4-0.4,0.9-0.7,1.4-0.9c0.6-0.3,1-0.5,1.1-1C20.3,18.6,20.1,18.2,19.7,17.6z M12.8,4.8c0.6,0.1,1.1,0.6,1,1.2c0,0.3-0.1,0.6-0.3,0.9c0,0,0,0-0.1,0c-0.2-0.1-0.3-0.1-0.4-0.2c0.1-0.1,0.1-0.3,0.2-0.5c0-0.4-0.2-0.7-0.4-0.7c-0.3,0-0.5,0.3-0.5,0.7c0,0,0,0.1,0,0.1c-0.1-0.1-0.3-0.1-0.4-0.2c0,0,0-0.1,0-0.1C11.8,5.5,12.2,4.9,12.8,4.8z M12.5,6.8c0.1,0.1,0.3,0.2,0.4,0.2c0.1,0,0.3,0.1,0.4,0.2c0.2,0.1,0.4,0.2,0.4,0.5c0,0.3-0.3,0.6-0.9,0.8c-0.2,0.1-0.3,0.1-0.4,0.2c-0.3,0.2-0.6,0.3-1,0.3c-0.3,0-0.6-0.2-0.8-0.4c-0.1-0.1-0.2-0.2-0.4-0.3C10.1,8.2,9.9,8,9.8,7.7c0-0.1,0.1-0.2,0.2-0.3c0.3-0.2,0.4-0.3,0.5-0.4l0.1-0.1c0.2-0.3,0.6-0.5,1-0.5C11.9,6.5,12.2,6.6,12.5,6.8z M10.4,5c0.4,0,0.7,0.4,0.8,1.1c0,0.1,0,0.1,0,0.2c-0.1,0-0.3,0.1-0.4,0.2c0,0,0-0.1,0-0.2c0-0.3-0.2-0.6-0.4-0.5c-0.2,0-0.3,0.3-0.3,0.6c0,0.2,0.1,0.3,0.2,0.4l0,0c0,0-0.1,0.1-0.2,0.1C9.9,6.7,9.7,6.4,9.7,6.1C9.7,5.5,10,5,10.4,5z M9.4,21.1c-0.7,0.3-1.6,0.2-2.2-0.2c-0.6-0.3-1.1-0.4-1.8-0.4c-0.5-0.1-1-0.1-1.1-0.3c-0.1-0.2-0.1-0.5,0.1-1c0.1-0.3,0.1-0.6,0-0.9c-0.1-0.3-0.1-0.5,0-0.8C4.5,17.2,4.7,17.1,5,17c0.3-0.1,0.5-0.2,0.7-0.4c0.1-0.1,0.2-0.2,0.3-0.4c0.3-0.4,0.5-0.6,0.8-0.6c0.6,0.1,1.1,1,1.5,1.9c0.2,0.3,0.4,0.7,0.7,1c0.4,0.5,0.9,1.2,0.9,1.6C9.9,20.6,9.7,20.9,9.4,21.1z M14.3,18.9c0,0.1,0,0.1-0.1,0.2c-1.2,0.9-2.8,1-4.1,0.3c-0.2-0.3-0.4-0.6-0.6-0.9c0.9-0.1,0.7-1.3-1.2-2.5c-2-1.3-0.6-3.7,0.1-4.8c0.1-0.1,0.1,0-0.3,0.8c-0.3,0.6-0.9,2.1-0.1,3.2c0-0.8,0.2-1.6,0.5-2.4c0.7-1.3,1.2-2.8,1.5-4.3c0.1,0.1,0.1,0.1,0.2,0.1c0.1,0.1,0.2,0.2,0.3,0.2c0.2,0.3,0.6,0.4,0.9,0.4c0,0,0.1,0,0.1,0c0.4,0,0.8-0.1,1.1-0.4c0.1-0.1,0.2-0.2,0.4-0.2c0.3-0.1,0.6-0.3,0.9-0.6c0.4,1.3,0.8,2.5,1.4,3.6c0.4,0.8,0.7,1.6,0.9,2.5c0.3,0,0.7,0.1,1,0.3c0.8,0.4,1.1,0.7,1,1.2c-0.1,0-0.1,0-0.2,0c0-0.3-0.2-0.6-0.9-0.9c-0.7-0.3-1.3-0.3-1.5,0.4c-0.1,0-0.2,0.1-0.3,0.1c-0.8,0.4-0.8,1.5-0.9,2.6C14.5,18.2,14.4,18.5,14.3,18.9z M18.9,19.5c-0.6,0.2-1.1,0.6-1.5,1.1c-0.4,0.6-1.1,1-1.9,0.9c-0.4,0-0.8-0.3-0.9-0.7c-0.1-0.6-0.1-1.2,0.2-1.8c0.1-0.4,0.2-0.7,0.3-1.1c0.1-1.2,0.1-1.9,0.6-2.2h0c0,0.5,0.3,0.8,0.7,1c0.5,0,1-0.1,1.4-0.5c0.1,0,0.1,0,0.2,0c0.3,0,0.5,0,0.7,0.2c0.2,0.2,0.3,0.5,0.3,0.7c0,0.3,0.2,0.6,0.3,0.9c0.5,0.5,0.5,0.8,0.5,0.9C19.7,19.1,19.3,19.3,18.9,19.5z M9.9,7.5c-0.1,0-0.1,0-0.1,0.1c0,0,0,0.1,0.1,0.1c0,0,0,0,0,0c0.1,0,0.1,0.1,0.1,0.1c0.3,0.4,0.8,0.6,1.4,0.7c0.5-0.1,1-0.2,1.5-0.6c0.2-0.1,0.4-0.2,0.6-0.3c0.1,0,0.1-0.1,0.1-0.1c0-0.1,0-0.1-0.1-0.1l0,0c-0.2,0.1-0.5,0.2-0.7,0.3c-0.4,0.3-0.9,0.5-1.4,0.5c-0.5,0-0.9-0.3-1.2-0.6C10.1,7.6,10,7.5,9.9,7.5z'
 | 
			
		||||
          fill='currentColor'
 | 
			
		||||
        />
 | 
			
		||||
      </svg>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <Icon svg={<CustomIcon />} />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default LinuxDoIcon;
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,9 @@ const LinuxDoOAuth = () => {
 | 
			
		||||
 | 
			
		||||
  const sendCode = async (code, state, count) => {
 | 
			
		||||
    let aff = localStorage.getItem('aff');
 | 
			
		||||
    const res = await API.get(`/api/oauth/linuxdo?code=${code}&state=${state}&aff=${aff}`);
 | 
			
		||||
    const res = await API.get(
 | 
			
		||||
      `/api/oauth/linuxdo?code=${code}&state=${state}&aff=${aff}`,
 | 
			
		||||
    );
 | 
			
		||||
    const { success, message, data } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      localStorage.removeItem('aff');
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,11 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { Dimmer, Loader, Segment } from 'semantic-ui-react';
 | 
			
		||||
import { Spin } from '@douyinfe/semi-ui';
 | 
			
		||||
 | 
			
		||||
const Loading = ({ prompt: name = 'page' }) => {
 | 
			
		||||
  return (
 | 
			
		||||
    <Segment style={{ height: 100 }}>
 | 
			
		||||
      <Dimmer active inverted>
 | 
			
		||||
        <Loader indeterminate>加载{name}中...</Loader>
 | 
			
		||||
      </Dimmer>
 | 
			
		||||
    </Segment>
 | 
			
		||||
    <Spin style={{ height: 100 }} spinning={true}>
 | 
			
		||||
      加载{name}中...
 | 
			
		||||
    </Spin>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,15 @@ import { UserContext } from '../context/User';
 | 
			
		||||
import { API, getLogo, showError, showInfo, showSuccess } from '../helpers';
 | 
			
		||||
import { onGitHubOAuthClicked, onLinuxDoOAuthClicked } from './utils';
 | 
			
		||||
import Turnstile from 'react-turnstile';
 | 
			
		||||
import { Button, Card, Divider, Form, Icon, Layout, Modal } from '@douyinfe/semi-ui';
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Card,
 | 
			
		||||
  Divider,
 | 
			
		||||
  Form,
 | 
			
		||||
  Icon,
 | 
			
		||||
  Layout,
 | 
			
		||||
  Modal,
 | 
			
		||||
} from '@douyinfe/semi-ui';
 | 
			
		||||
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
 | 
			
		||||
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
 | 
			
		||||
import TelegramLoginButton from 'react-telegram-login';
 | 
			
		||||
@@ -17,7 +25,7 @@ const LoginForm = () => {
 | 
			
		||||
  const [inputs, setInputs] = useState({
 | 
			
		||||
    username: '',
 | 
			
		||||
    password: '',
 | 
			
		||||
    wechat_verification_code: ''
 | 
			
		||||
    wechat_verification_code: '',
 | 
			
		||||
  });
 | 
			
		||||
  const [searchParams, setSearchParams] = useSearchParams();
 | 
			
		||||
  const [submitted, setSubmitted] = useState(false);
 | 
			
		||||
@@ -57,7 +65,7 @@ const LoginForm = () => {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const res = await API.get(
 | 
			
		||||
      `/api/oauth/wechat?code=${inputs.wechat_verification_code}`
 | 
			
		||||
      `/api/oauth/wechat?code=${inputs.wechat_verification_code}`,
 | 
			
		||||
    );
 | 
			
		||||
    const { success, message, data } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
@@ -82,17 +90,24 @@ const LoginForm = () => {
 | 
			
		||||
    }
 | 
			
		||||
    setSubmitted(true);
 | 
			
		||||
    if (username && password) {
 | 
			
		||||
      const res = await API.post(`/api/user/login?turnstile=${turnstileToken}`, {
 | 
			
		||||
        username,
 | 
			
		||||
        password
 | 
			
		||||
      });
 | 
			
		||||
      const res = await API.post(
 | 
			
		||||
        `/api/user/login?turnstile=${turnstileToken}`,
 | 
			
		||||
        {
 | 
			
		||||
          username,
 | 
			
		||||
          password,
 | 
			
		||||
        },
 | 
			
		||||
      );
 | 
			
		||||
      const { success, message, data } = res.data;
 | 
			
		||||
      if (success) {
 | 
			
		||||
        userDispatch({ type: 'login', payload: data });
 | 
			
		||||
        localStorage.setItem('user', JSON.stringify(data));
 | 
			
		||||
        showSuccess('登录成功!');
 | 
			
		||||
        if (username === 'root' && password === '123456') {
 | 
			
		||||
          Modal.error({ title: '您正在使用默认密码!', content: '请立刻修改默认密码!', centered: true });
 | 
			
		||||
          Modal.error({
 | 
			
		||||
            title: '您正在使用默认密码!',
 | 
			
		||||
            content: '请立刻修改默认密码!',
 | 
			
		||||
            centered: true,
 | 
			
		||||
          });
 | 
			
		||||
        }
 | 
			
		||||
        navigate('/token');
 | 
			
		||||
      } else {
 | 
			
		||||
@@ -105,7 +120,16 @@ const LoginForm = () => {
 | 
			
		||||
 | 
			
		||||
  // 添加Telegram登录处理函数
 | 
			
		||||
  const onTelegramLoginClicked = async (response) => {
 | 
			
		||||
    const fields = ['id', 'first_name', 'last_name', 'username', 'photo_url', 'auth_date', 'hash', 'lang'];
 | 
			
		||||
    const fields = [
 | 
			
		||||
      'id',
 | 
			
		||||
      'first_name',
 | 
			
		||||
      'last_name',
 | 
			
		||||
      'username',
 | 
			
		||||
      'photo_url',
 | 
			
		||||
      'auth_date',
 | 
			
		||||
      'hash',
 | 
			
		||||
      'lang',
 | 
			
		||||
    ];
 | 
			
		||||
    const params = {};
 | 
			
		||||
    fields.forEach((field) => {
 | 
			
		||||
      if (response[field]) {
 | 
			
		||||
@@ -127,10 +151,15 @@ const LoginForm = () => {
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <Layout>
 | 
			
		||||
        <Layout.Header>
 | 
			
		||||
        </Layout.Header>
 | 
			
		||||
        <Layout.Header></Layout.Header>
 | 
			
		||||
        <Layout.Content>
 | 
			
		||||
          <div style={{ justifyContent: 'center', display: 'flex', marginTop: 120 }}>
 | 
			
		||||
          <div
 | 
			
		||||
            style={{
 | 
			
		||||
              justifyContent: 'center',
 | 
			
		||||
              display: 'flex',
 | 
			
		||||
              marginTop: 120,
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <div style={{ width: 500 }}>
 | 
			
		||||
              <Card>
 | 
			
		||||
                <Title heading={2} style={{ textAlign: 'center' }}>
 | 
			
		||||
@@ -140,60 +169,85 @@ const LoginForm = () => {
 | 
			
		||||
                  <Form.Input
 | 
			
		||||
                    field={'username'}
 | 
			
		||||
                    label={'用户名'}
 | 
			
		||||
                    placeholder="用户名"
 | 
			
		||||
                    name="username"
 | 
			
		||||
                    placeholder='用户名'
 | 
			
		||||
                    name='username'
 | 
			
		||||
                    onChange={(value) => handleChange('username', value)}
 | 
			
		||||
                  />
 | 
			
		||||
                  <Form.Input
 | 
			
		||||
                    field={'password'}
 | 
			
		||||
                    label={'密码'}
 | 
			
		||||
                    placeholder="密码"
 | 
			
		||||
                    name="password"
 | 
			
		||||
                    type="password"
 | 
			
		||||
                    placeholder='密码'
 | 
			
		||||
                    name='password'
 | 
			
		||||
                    type='password'
 | 
			
		||||
                    onChange={(value) => handleChange('password', value)}
 | 
			
		||||
                  />
 | 
			
		||||
 | 
			
		||||
                  <Button theme="solid" style={{ width: '100%' }} type={'primary'} size="large"
 | 
			
		||||
                          htmlType={'submit'} onClick={handleSubmit}>
 | 
			
		||||
                  <Button
 | 
			
		||||
                    theme='solid'
 | 
			
		||||
                    style={{ width: '100%' }}
 | 
			
		||||
                    type={'primary'}
 | 
			
		||||
                    size='large'
 | 
			
		||||
                    htmlType={'submit'}
 | 
			
		||||
                    onClick={handleSubmit}
 | 
			
		||||
                  >
 | 
			
		||||
                    登录
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </Form>
 | 
			
		||||
                <div style={{ display: 'flex', justifyContent: 'space-between', marginTop: 20 }}>
 | 
			
		||||
                <div
 | 
			
		||||
                  style={{
 | 
			
		||||
                    display: 'flex',
 | 
			
		||||
                    justifyContent: 'space-between',
 | 
			
		||||
                    marginTop: 20,
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Text>
 | 
			
		||||
                    没有账号请先 <Link to="/register">注册账号</Link>
 | 
			
		||||
                    没有账号请先 <Link to='/register'>注册账号</Link>
 | 
			
		||||
                  </Text>
 | 
			
		||||
                  <Text>
 | 
			
		||||
                    忘记密码 <Link to="/reset">点击重置</Link>
 | 
			
		||||
                    忘记密码 <Link to='/reset'>点击重置</Link>
 | 
			
		||||
                  </Text>
 | 
			
		||||
                </div>
 | 
			
		||||
                {status.github_oauth || status.linuxdo_oauth || status.wechat_login || status.telegram_oauth ? (
 | 
			
		||||
                {status.github_oauth ||
 | 
			
		||||
                status.linuxdo_oauth ||
 | 
			
		||||
                status.wechat_login ||
 | 
			
		||||
                status.telegram_oauth ? (
 | 
			
		||||
                  <>
 | 
			
		||||
                    <Divider margin="12px" align="center">
 | 
			
		||||
                    <Divider margin='12px' align='center'>
 | 
			
		||||
                      第三方登录
 | 
			
		||||
                    </Divider>
 | 
			
		||||
                    <div style={{ display: 'flex', justifyContent: 'center', marginTop: 20 }}>
 | 
			
		||||
                    <div
 | 
			
		||||
                      style={{
 | 
			
		||||
                        display: 'flex',
 | 
			
		||||
                        justifyContent: 'center',
 | 
			
		||||
                        marginTop: 20,
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                      {status.github_oauth ? (
 | 
			
		||||
                        <Button
 | 
			
		||||
                          type="primary"
 | 
			
		||||
                          type='primary'
 | 
			
		||||
                          icon={<IconGithubLogo />}
 | 
			
		||||
                          onClick={() => onGitHubOAuthClicked(status.github_client_id)}
 | 
			
		||||
                          onClick={() =>
 | 
			
		||||
                            onGitHubOAuthClicked(status.github_client_id)
 | 
			
		||||
                          }
 | 
			
		||||
                        />
 | 
			
		||||
                      ) : (
 | 
			
		||||
                        <></>
 | 
			
		||||
                      )}
 | 
			
		||||
                      {status.linuxdo_oauth ? (
 | 
			
		||||
                        <Button
 | 
			
		||||
                          type="primary"
 | 
			
		||||
                          type='primary'
 | 
			
		||||
                          icon={<LinuxDoIcon />}
 | 
			
		||||
                          style={{color: '#000'}}
 | 
			
		||||
                          onClick={() => onLinuxDoOAuthClicked(status.linuxdo_client_id)}
 | 
			
		||||
                          style={{ color: '#000' }}
 | 
			
		||||
                          onClick={() =>
 | 
			
		||||
                            onLinuxDoOAuthClicked(status.linuxdo_client_id)
 | 
			
		||||
                          }
 | 
			
		||||
                        />
 | 
			
		||||
                      ) : (
 | 
			
		||||
                        <></>
 | 
			
		||||
                      )}
 | 
			
		||||
                      {status.wechat_login ? (
 | 
			
		||||
                        <Button
 | 
			
		||||
                          type="primary"
 | 
			
		||||
                          type='primary'
 | 
			
		||||
                          style={{ color: 'rgba(var(--semi-green-5), 1)' }}
 | 
			
		||||
                          icon={<Icon svg={<WeChatIcon />} />}
 | 
			
		||||
                          onClick={onWeChatLoginClicked}
 | 
			
		||||
@@ -203,7 +257,10 @@ const LoginForm = () => {
 | 
			
		||||
                      )}
 | 
			
		||||
 | 
			
		||||
                      {status.telegram_oauth ? (
 | 
			
		||||
                        <TelegramLoginButton dataOnauth={onTelegramLoginClicked} botName={status.telegram_bot_name} />
 | 
			
		||||
                        <TelegramLoginButton
 | 
			
		||||
                          dataOnauth={onTelegramLoginClicked}
 | 
			
		||||
                          botName={status.telegram_bot_name}
 | 
			
		||||
                        />
 | 
			
		||||
                      ) : (
 | 
			
		||||
                        <></>
 | 
			
		||||
                      )}
 | 
			
		||||
@@ -213,7 +270,7 @@ const LoginForm = () => {
 | 
			
		||||
                  <></>
 | 
			
		||||
                )}
 | 
			
		||||
                <Modal
 | 
			
		||||
                  title="微信扫码登录"
 | 
			
		||||
                  title='微信扫码登录'
 | 
			
		||||
                  visible={showWeChatLoginModal}
 | 
			
		||||
                  maskClosable={true}
 | 
			
		||||
                  onOk={onSubmitWeChatVerificationCode}
 | 
			
		||||
@@ -222,7 +279,13 @@ const LoginForm = () => {
 | 
			
		||||
                  size={'small'}
 | 
			
		||||
                  centered={true}
 | 
			
		||||
                >
 | 
			
		||||
                  <div style={{ display: 'flex', alignItem: 'center', flexDirection: 'column' }}>
 | 
			
		||||
                  <div
 | 
			
		||||
                    style={{
 | 
			
		||||
                      display: 'flex',
 | 
			
		||||
                      alignItem: 'center',
 | 
			
		||||
                      flexDirection: 'column',
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    <img src={status.wechat_qrcode} />
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div style={{ textAlign: 'center' }}>
 | 
			
		||||
@@ -230,19 +293,27 @@ const LoginForm = () => {
 | 
			
		||||
                      微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
 | 
			
		||||
                    </p>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <Form size="large">
 | 
			
		||||
                  <Form size='large'>
 | 
			
		||||
                    <Form.Input
 | 
			
		||||
                      field={'wechat_verification_code'}
 | 
			
		||||
                      placeholder="验证码"
 | 
			
		||||
                      placeholder='验证码'
 | 
			
		||||
                      label={'验证码'}
 | 
			
		||||
                      value={inputs.wechat_verification_code}
 | 
			
		||||
                      onChange={(value) => handleChange('wechat_verification_code', value)}
 | 
			
		||||
                      onChange={(value) =>
 | 
			
		||||
                        handleChange('wechat_verification_code', value)
 | 
			
		||||
                      }
 | 
			
		||||
                    />
 | 
			
		||||
                  </Form>
 | 
			
		||||
                </Modal>
 | 
			
		||||
              </Card>
 | 
			
		||||
              {turnstileEnabled ? (
 | 
			
		||||
                <div style={{ display: 'flex', justifyContent: 'center', marginTop: 20 }}>
 | 
			
		||||
                <div
 | 
			
		||||
                  style={{
 | 
			
		||||
                    display: 'flex',
 | 
			
		||||
                    justifyContent: 'center',
 | 
			
		||||
                    marginTop: 20,
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  <Turnstile
 | 
			
		||||
                    sitekey={turnstileSiteKey}
 | 
			
		||||
                    onVerify={(token) => {
 | 
			
		||||
@@ -255,7 +326,6 @@ const LoginForm = () => {
 | 
			
		||||
              )}
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
        </Layout.Content>
 | 
			
		||||
      </Layout>
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,25 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { API, copy, isAdmin, showError, showSuccess, timestamp2string } from '../helpers';
 | 
			
		||||
import {
 | 
			
		||||
  API,
 | 
			
		||||
  copy,
 | 
			
		||||
  isAdmin,
 | 
			
		||||
  showError,
 | 
			
		||||
  showSuccess,
 | 
			
		||||
  timestamp2string,
 | 
			
		||||
} from '../helpers';
 | 
			
		||||
 | 
			
		||||
import { Avatar, Button, Form, Layout, Modal, Select, Space, Spin, Table, Tag } from '@douyinfe/semi-ui';
 | 
			
		||||
import {
 | 
			
		||||
  Avatar,
 | 
			
		||||
  Button,
 | 
			
		||||
  Form,
 | 
			
		||||
  Layout,
 | 
			
		||||
  Modal,
 | 
			
		||||
  Select,
 | 
			
		||||
  Space,
 | 
			
		||||
  Spin,
 | 
			
		||||
  Table,
 | 
			
		||||
  Tag,
 | 
			
		||||
} from '@douyinfe/semi-ui';
 | 
			
		||||
import { ITEMS_PER_PAGE } from '../constants';
 | 
			
		||||
import { renderNumber, renderQuota, stringToColor } from '../helpers/render';
 | 
			
		||||
import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph';
 | 
			
		||||
@@ -9,131 +27,285 @@ import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph';
 | 
			
		||||
const { Header } = Layout;
 | 
			
		||||
 | 
			
		||||
function renderTimestamp(timestamp) {
 | 
			
		||||
  return (<>
 | 
			
		||||
    {timestamp2string(timestamp)}
 | 
			
		||||
  </>);
 | 
			
		||||
  return <>{timestamp2string(timestamp)}</>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const MODE_OPTIONS = [{ key: 'all', text: '全部用户', value: 'all' }, { key: 'self', text: '当前用户', value: 'self' }];
 | 
			
		||||
const MODE_OPTIONS = [
 | 
			
		||||
  { key: 'all', text: '全部用户', value: 'all' },
 | 
			
		||||
  { key: 'self', text: '当前用户', value: 'self' },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const colors = ['amber', 'blue', 'cyan', 'green', 'grey', 'indigo', 'light-blue', 'lime', 'orange', 'pink', 'purple', 'red', 'teal', 'violet', 'yellow'];
 | 
			
		||||
const colors = [
 | 
			
		||||
  'amber',
 | 
			
		||||
  'blue',
 | 
			
		||||
  'cyan',
 | 
			
		||||
  'green',
 | 
			
		||||
  'grey',
 | 
			
		||||
  'indigo',
 | 
			
		||||
  'light-blue',
 | 
			
		||||
  'lime',
 | 
			
		||||
  'orange',
 | 
			
		||||
  'pink',
 | 
			
		||||
  'purple',
 | 
			
		||||
  'red',
 | 
			
		||||
  'teal',
 | 
			
		||||
  'violet',
 | 
			
		||||
  'yellow',
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
function renderType(type) {
 | 
			
		||||
  switch (type) {
 | 
			
		||||
    case 1:
 | 
			
		||||
      return <Tag color="cyan" size="large"> 充值 </Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='cyan' size='large'>
 | 
			
		||||
          {' '}
 | 
			
		||||
          充值{' '}
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    case 2:
 | 
			
		||||
      return <Tag color="lime" size="large"> 消费 </Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='lime' size='large'>
 | 
			
		||||
          {' '}
 | 
			
		||||
          消费{' '}
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    case 3:
 | 
			
		||||
      return <Tag color="orange" size="large"> 管理 </Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='orange' size='large'>
 | 
			
		||||
          {' '}
 | 
			
		||||
          管理{' '}
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    case 4:
 | 
			
		||||
      return <Tag color="purple" size="large"> 系统 </Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='purple' size='large'>
 | 
			
		||||
          {' '}
 | 
			
		||||
          系统{' '}
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    default:
 | 
			
		||||
      return <Tag color="black" size="large"> 未知 </Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='black' size='large'>
 | 
			
		||||
          {' '}
 | 
			
		||||
          未知{' '}
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function renderIsStream(bool) {
 | 
			
		||||
  if (bool) {
 | 
			
		||||
    return <Tag color="blue" size="large">流</Tag>;
 | 
			
		||||
    return (
 | 
			
		||||
      <Tag color='blue' size='large'>
 | 
			
		||||
        流
 | 
			
		||||
      </Tag>
 | 
			
		||||
    );
 | 
			
		||||
  } else {
 | 
			
		||||
    return <Tag color="purple" size="large">非流</Tag>;
 | 
			
		||||
    return (
 | 
			
		||||
      <Tag color='purple' size='large'>
 | 
			
		||||
        非流
 | 
			
		||||
      </Tag>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function renderUseTime(type) {
 | 
			
		||||
  const time = parseInt(type);
 | 
			
		||||
  if (time < 101) {
 | 
			
		||||
    return <Tag color="green" size="large"> {time} s </Tag>;
 | 
			
		||||
    return (
 | 
			
		||||
      <Tag color='green' size='large'>
 | 
			
		||||
        {' '}
 | 
			
		||||
        {time} s{' '}
 | 
			
		||||
      </Tag>
 | 
			
		||||
    );
 | 
			
		||||
  } else if (time < 300) {
 | 
			
		||||
    return <Tag color="orange" size="large"> {time} s </Tag>;
 | 
			
		||||
    return (
 | 
			
		||||
      <Tag color='orange' size='large'>
 | 
			
		||||
        {' '}
 | 
			
		||||
        {time} s{' '}
 | 
			
		||||
      </Tag>
 | 
			
		||||
    );
 | 
			
		||||
  } else {
 | 
			
		||||
    return <Tag color="red" size="large"> {time} s </Tag>;
 | 
			
		||||
    return (
 | 
			
		||||
      <Tag color='red' size='large'>
 | 
			
		||||
        {' '}
 | 
			
		||||
        {time} s{' '}
 | 
			
		||||
      </Tag>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const LogsTable = () => {
 | 
			
		||||
  const columns = [{
 | 
			
		||||
    title: '时间', dataIndex: 'timestamp2string'
 | 
			
		||||
  }, {
 | 
			
		||||
    title: '渠道',
 | 
			
		||||
    dataIndex: 'channel',
 | 
			
		||||
    className: isAdmin() ? 'tableShow' : 'tableHiddle',
 | 
			
		||||
    render: (text, record, index) => {
 | 
			
		||||
      return (isAdminUser ? record.type === 0 || record.type === 2 ? <div>
 | 
			
		||||
        {<Tag color={colors[parseInt(text) % colors.length]} size="large"> {text} </Tag>}
 | 
			
		||||
      </div> : <></> : <></>);
 | 
			
		||||
    }
 | 
			
		||||
  }, {
 | 
			
		||||
    title: '用户',
 | 
			
		||||
    dataIndex: 'username',
 | 
			
		||||
    className: isAdmin() ? 'tableShow' : 'tableHiddle',
 | 
			
		||||
    render: (text, record, index) => {
 | 
			
		||||
      return (isAdminUser ? <div>
 | 
			
		||||
        <Avatar size="small" color={stringToColor(text)} style={{ marginRight: 4 }}
 | 
			
		||||
                onClick={() => showUserInfo(record.user_id)}>
 | 
			
		||||
          {typeof text === 'string' && text.slice(0, 1)}
 | 
			
		||||
        </Avatar>
 | 
			
		||||
        {text}
 | 
			
		||||
      </div> : <></>);
 | 
			
		||||
    }
 | 
			
		||||
  }, {
 | 
			
		||||
    title: '令牌', dataIndex: 'token_name', render: (text, record, index) => {
 | 
			
		||||
      return (record.type === 0 || record.type === 2 ? <div>
 | 
			
		||||
        <Tag color="grey" size="large" onClick={() => {
 | 
			
		||||
          copyText(text);
 | 
			
		||||
        }}> {text} </Tag>
 | 
			
		||||
      </div> : <></>);
 | 
			
		||||
    }
 | 
			
		||||
  }, {
 | 
			
		||||
    title: '类型', dataIndex: 'type', render: (text, record, index) => {
 | 
			
		||||
      return (<div>
 | 
			
		||||
        {renderType(text)}
 | 
			
		||||
      </div>);
 | 
			
		||||
    }
 | 
			
		||||
  }, {
 | 
			
		||||
    title: '模型', dataIndex: 'model_name', render: (text, record, index) => {
 | 
			
		||||
      return (record.type === 0 || record.type === 2 ? <div>
 | 
			
		||||
        <Tag color={stringToColor(text)} size="large" onClick={() => {
 | 
			
		||||
          copyText(text);
 | 
			
		||||
        }}> {text} </Tag>
 | 
			
		||||
      </div> : <></>);
 | 
			
		||||
    }
 | 
			
		||||
  }, {
 | 
			
		||||
    title: '用时', dataIndex: 'use_time', render: (text, record, index) => {
 | 
			
		||||
      return (<div>
 | 
			
		||||
        <Space>
 | 
			
		||||
          {renderUseTime(text)}
 | 
			
		||||
          {renderIsStream(record.is_stream)}
 | 
			
		||||
        </Space>
 | 
			
		||||
      </div>);
 | 
			
		||||
    }
 | 
			
		||||
  }, {
 | 
			
		||||
    title: '提示', dataIndex: 'prompt_tokens', render: (text, record, index) => {
 | 
			
		||||
      return (record.type === 0 || record.type === 2 ? <div>
 | 
			
		||||
        {<span> {text} </span>}
 | 
			
		||||
      </div> : <></>);
 | 
			
		||||
    }
 | 
			
		||||
  }, {
 | 
			
		||||
    title: '补全', dataIndex: 'completion_tokens', render: (text, record, index) => {
 | 
			
		||||
      return (parseInt(text) > 0 && (record.type === 0 || record.type === 2) ? <div>
 | 
			
		||||
        {<span> {text} </span>}
 | 
			
		||||
      </div> : <></>);
 | 
			
		||||
    }
 | 
			
		||||
  }, {
 | 
			
		||||
    title: '花费', dataIndex: 'quota', render: (text, record, index) => {
 | 
			
		||||
      return (record.type === 0 || record.type === 2 ? <div>
 | 
			
		||||
        {renderQuota(text, 6)}
 | 
			
		||||
      </div> : <></>);
 | 
			
		||||
    }
 | 
			
		||||
  }, {
 | 
			
		||||
    title: '详情', dataIndex: 'content', render: (text, record, index) => {
 | 
			
		||||
      return <Paragraph ellipsis={{ rows: 2, showTooltip: { type: 'popover', opts: { style: { width: 240 } } } }}
 | 
			
		||||
                        style={{ maxWidth: 240 }}>
 | 
			
		||||
        {text}
 | 
			
		||||
      </Paragraph>;
 | 
			
		||||
    }
 | 
			
		||||
  }];
 | 
			
		||||
  const columns = [
 | 
			
		||||
    {
 | 
			
		||||
      title: '时间',
 | 
			
		||||
      dataIndex: 'timestamp2string',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '渠道',
 | 
			
		||||
      dataIndex: 'channel',
 | 
			
		||||
      className: isAdmin() ? 'tableShow' : 'tableHiddle',
 | 
			
		||||
      render: (text, record, index) => {
 | 
			
		||||
        return isAdminUser ? (
 | 
			
		||||
          record.type === 0 || record.type === 2 ? (
 | 
			
		||||
            <div>
 | 
			
		||||
              {
 | 
			
		||||
                <Tag
 | 
			
		||||
                  color={colors[parseInt(text) % colors.length]}
 | 
			
		||||
                  size='large'
 | 
			
		||||
                >
 | 
			
		||||
                  {' '}
 | 
			
		||||
                  {text}{' '}
 | 
			
		||||
                </Tag>
 | 
			
		||||
              }
 | 
			
		||||
            </div>
 | 
			
		||||
          ) : (
 | 
			
		||||
            <></>
 | 
			
		||||
          )
 | 
			
		||||
        ) : (
 | 
			
		||||
          <></>
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '用户',
 | 
			
		||||
      dataIndex: 'username',
 | 
			
		||||
      className: isAdmin() ? 'tableShow' : 'tableHiddle',
 | 
			
		||||
      render: (text, record, index) => {
 | 
			
		||||
        return isAdminUser ? (
 | 
			
		||||
          <div>
 | 
			
		||||
            <Avatar
 | 
			
		||||
              size='small'
 | 
			
		||||
              color={stringToColor(text)}
 | 
			
		||||
              style={{ marginRight: 4 }}
 | 
			
		||||
              onClick={() => showUserInfo(record.user_id)}
 | 
			
		||||
            >
 | 
			
		||||
              {typeof text === 'string' && text.slice(0, 1)}
 | 
			
		||||
            </Avatar>
 | 
			
		||||
            {text}
 | 
			
		||||
          </div>
 | 
			
		||||
        ) : (
 | 
			
		||||
          <></>
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '令牌',
 | 
			
		||||
      dataIndex: 'token_name',
 | 
			
		||||
      render: (text, record, index) => {
 | 
			
		||||
        return record.type === 0 || record.type === 2 ? (
 | 
			
		||||
          <div>
 | 
			
		||||
            <Tag
 | 
			
		||||
              color='grey'
 | 
			
		||||
              size='large'
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                copyText(text);
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              {' '}
 | 
			
		||||
              {text}{' '}
 | 
			
		||||
            </Tag>
 | 
			
		||||
          </div>
 | 
			
		||||
        ) : (
 | 
			
		||||
          <></>
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '类型',
 | 
			
		||||
      dataIndex: 'type',
 | 
			
		||||
      render: (text, record, index) => {
 | 
			
		||||
        return <div>{renderType(text)}</div>;
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '模型',
 | 
			
		||||
      dataIndex: 'model_name',
 | 
			
		||||
      render: (text, record, index) => {
 | 
			
		||||
        return record.type === 0 || record.type === 2 ? (
 | 
			
		||||
          <div>
 | 
			
		||||
            <Tag
 | 
			
		||||
              color={stringToColor(text)}
 | 
			
		||||
              size='large'
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                copyText(text);
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              {' '}
 | 
			
		||||
              {text}{' '}
 | 
			
		||||
            </Tag>
 | 
			
		||||
          </div>
 | 
			
		||||
        ) : (
 | 
			
		||||
          <></>
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '用时',
 | 
			
		||||
      dataIndex: 'use_time',
 | 
			
		||||
      render: (text, record, index) => {
 | 
			
		||||
        return (
 | 
			
		||||
          <div>
 | 
			
		||||
            <Space>
 | 
			
		||||
              {renderUseTime(text)}
 | 
			
		||||
              {renderIsStream(record.is_stream)}
 | 
			
		||||
            </Space>
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '提示',
 | 
			
		||||
      dataIndex: 'prompt_tokens',
 | 
			
		||||
      render: (text, record, index) => {
 | 
			
		||||
        return record.type === 0 || record.type === 2 ? (
 | 
			
		||||
          <div>{<span> {text} </span>}</div>
 | 
			
		||||
        ) : (
 | 
			
		||||
          <></>
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '补全',
 | 
			
		||||
      dataIndex: 'completion_tokens',
 | 
			
		||||
      render: (text, record, index) => {
 | 
			
		||||
        return parseInt(text) > 0 &&
 | 
			
		||||
          (record.type === 0 || record.type === 2) ? (
 | 
			
		||||
          <div>{<span> {text} </span>}</div>
 | 
			
		||||
        ) : (
 | 
			
		||||
          <></>
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '花费',
 | 
			
		||||
      dataIndex: 'quota',
 | 
			
		||||
      render: (text, record, index) => {
 | 
			
		||||
        return record.type === 0 || record.type === 2 ? (
 | 
			
		||||
          <div>{renderQuota(text, 6)}</div>
 | 
			
		||||
        ) : (
 | 
			
		||||
          <></>
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '详情',
 | 
			
		||||
      dataIndex: 'content',
 | 
			
		||||
      render: (text, record, index) => {
 | 
			
		||||
        return (
 | 
			
		||||
          <Paragraph
 | 
			
		||||
            ellipsis={{
 | 
			
		||||
              rows: 2,
 | 
			
		||||
              showTooltip: { type: 'popover', opts: { style: { width: 240 } } },
 | 
			
		||||
            }}
 | 
			
		||||
            style={{ maxWidth: 240 }}
 | 
			
		||||
          >
 | 
			
		||||
            {text}
 | 
			
		||||
          </Paragraph>
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  const [logs, setLogs] = useState([]);
 | 
			
		||||
  const [showStat, setShowStat] = useState(false);
 | 
			
		||||
@@ -154,12 +326,20 @@ const LogsTable = () => {
 | 
			
		||||
    model_name: '',
 | 
			
		||||
    start_timestamp: timestamp2string(now.getTime() / 1000 - 86400),
 | 
			
		||||
    end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
 | 
			
		||||
    channel: ''
 | 
			
		||||
    channel: '',
 | 
			
		||||
  });
 | 
			
		||||
  const { username, token_name, model_name, start_timestamp, end_timestamp, channel } = inputs;
 | 
			
		||||
  const {
 | 
			
		||||
    username,
 | 
			
		||||
    token_name,
 | 
			
		||||
    model_name,
 | 
			
		||||
    start_timestamp,
 | 
			
		||||
    end_timestamp,
 | 
			
		||||
    channel,
 | 
			
		||||
  } = inputs;
 | 
			
		||||
 | 
			
		||||
  const [stat, setStat] = useState({
 | 
			
		||||
    quota: 0, token: 0
 | 
			
		||||
    quota: 0,
 | 
			
		||||
    token: 0,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const handleInputChange = (value, name) => {
 | 
			
		||||
@@ -169,7 +349,9 @@ const LogsTable = () => {
 | 
			
		||||
  const getLogSelfStat = async () => {
 | 
			
		||||
    let localStartTimestamp = Date.parse(start_timestamp) / 1000;
 | 
			
		||||
    let localEndTimestamp = Date.parse(end_timestamp) / 1000;
 | 
			
		||||
    let res = await API.get(`/api/log/self/stat?type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`);
 | 
			
		||||
    let res = await API.get(
 | 
			
		||||
      `/api/log/self/stat?type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`,
 | 
			
		||||
    );
 | 
			
		||||
    const { success, message, data } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      setStat(data);
 | 
			
		||||
@@ -181,7 +363,9 @@ const LogsTable = () => {
 | 
			
		||||
  const getLogStat = async () => {
 | 
			
		||||
    let localStartTimestamp = Date.parse(start_timestamp) / 1000;
 | 
			
		||||
    let localEndTimestamp = Date.parse(end_timestamp) / 1000;
 | 
			
		||||
    let res = await API.get(`/api/log/stat?type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}`);
 | 
			
		||||
    let res = await API.get(
 | 
			
		||||
      `/api/log/stat?type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}`,
 | 
			
		||||
    );
 | 
			
		||||
    const { success, message, data } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      setStat(data);
 | 
			
		||||
@@ -209,12 +393,16 @@ const LogsTable = () => {
 | 
			
		||||
    const { success, message, data } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      Modal.info({
 | 
			
		||||
        title: '用户信息', content: <div style={{ padding: 12 }}>
 | 
			
		||||
          <p>用户名: {data.username}</p>
 | 
			
		||||
          <p>余额: {renderQuota(data.quota)}</p>
 | 
			
		||||
          <p>已用额度:{renderQuota(data.used_quota)}</p>
 | 
			
		||||
          <p>请求次数:{renderNumber(data.request_count)}</p>
 | 
			
		||||
        </div>, centered: true
 | 
			
		||||
        title: '用户信息',
 | 
			
		||||
        content: (
 | 
			
		||||
          <div style={{ padding: 12 }}>
 | 
			
		||||
            <p>用户名: {data.username}</p>
 | 
			
		||||
            <p>余额: {renderQuota(data.quota)}</p>
 | 
			
		||||
            <p>已用额度:{renderQuota(data.used_quota)}</p>
 | 
			
		||||
            <p>请求次数:{renderNumber(data.request_count)}</p>
 | 
			
		||||
          </div>
 | 
			
		||||
        ),
 | 
			
		||||
        centered: true,
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      showError(message);
 | 
			
		||||
@@ -259,14 +447,16 @@ const LogsTable = () => {
 | 
			
		||||
    setLoading(false);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const pageData = logs.slice((activePage - 1) * pageSize, activePage * pageSize);
 | 
			
		||||
  const pageData = logs.slice(
 | 
			
		||||
    (activePage - 1) * pageSize,
 | 
			
		||||
    activePage * pageSize,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const handlePageChange = page => {
 | 
			
		||||
  const handlePageChange = (page) => {
 | 
			
		||||
    setActivePage(page);
 | 
			
		||||
    if (page === Math.ceil(logs.length / pageSize) + 1) {
 | 
			
		||||
      // In this case we have to load more data and then append them.
 | 
			
		||||
      loadLogs(page - 1, pageSize, logType).then(r => {
 | 
			
		||||
      });
 | 
			
		||||
      loadLogs(page - 1, pageSize, logType).then((r) => {});
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@@ -298,7 +488,8 @@ const LogsTable = () => {
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    // console.log('default effect')
 | 
			
		||||
    const localPageSize = parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE;
 | 
			
		||||
    const localPageSize =
 | 
			
		||||
      parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE;
 | 
			
		||||
    setPageSize(localPageSize);
 | 
			
		||||
    loadLogs(0, localPageSize)
 | 
			
		||||
      .then()
 | 
			
		||||
@@ -326,74 +517,136 @@ const LogsTable = () => {
 | 
			
		||||
    setSearching(false);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (<>
 | 
			
		||||
    <Layout>
 | 
			
		||||
      <Header>
 | 
			
		||||
        <Spin spinning={loadingStat}>
 | 
			
		||||
          <h3>使用明细(总消耗额度:
 | 
			
		||||
            <span onClick={handleEyeClick} style={{
 | 
			
		||||
              cursor: 'pointer', color: 'gray'
 | 
			
		||||
            }}>{showStat ? renderQuota(stat.quota) : '点击查看'}</span>
 | 
			
		||||
            )
 | 
			
		||||
          </h3>
 | 
			
		||||
        </Spin>
 | 
			
		||||
      </Header>
 | 
			
		||||
      <Form layout="horizontal" style={{ marginTop: 10 }}>
 | 
			
		||||
        <>
 | 
			
		||||
          <Form.Input field="token_name" label="令牌名称" style={{ width: 176 }} value={token_name}
 | 
			
		||||
                      placeholder={'可选值'} name="token_name"
 | 
			
		||||
                      onChange={value => handleInputChange(value, 'token_name')} />
 | 
			
		||||
          <Form.Input field="model_name" label="模型名称" style={{ width: 176 }} value={model_name}
 | 
			
		||||
                      placeholder="可选值"
 | 
			
		||||
                      name="model_name"
 | 
			
		||||
                      onChange={value => handleInputChange(value, 'model_name')} />
 | 
			
		||||
          <Form.DatePicker field="start_timestamp" label="起始时间" style={{ width: 272 }}
 | 
			
		||||
                           initValue={start_timestamp}
 | 
			
		||||
                           value={start_timestamp} type="dateTime"
 | 
			
		||||
                           name="start_timestamp"
 | 
			
		||||
                           onChange={value => handleInputChange(value, 'start_timestamp')} />
 | 
			
		||||
          <Form.DatePicker field="end_timestamp" fluid label="结束时间" style={{ width: 272 }}
 | 
			
		||||
                           initValue={end_timestamp}
 | 
			
		||||
                           value={end_timestamp} type="dateTime"
 | 
			
		||||
                           name="end_timestamp"
 | 
			
		||||
                           onChange={value => handleInputChange(value, 'end_timestamp')} />
 | 
			
		||||
          {isAdminUser && <>
 | 
			
		||||
            <Form.Input field="channel" label="渠道 ID" style={{ width: 176 }} value={channel}
 | 
			
		||||
                        placeholder="可选值" name="channel"
 | 
			
		||||
                        onChange={value => handleInputChange(value, 'channel')} />
 | 
			
		||||
            <Form.Input field="username" label="用户名称" style={{ width: 176 }} value={username}
 | 
			
		||||
                        placeholder={'可选值'} name="username"
 | 
			
		||||
                        onChange={value => handleInputChange(value, 'username')} />
 | 
			
		||||
          </>}
 | 
			
		||||
          <Form.Section>
 | 
			
		||||
            <Button label="查询" type="primary" htmlType="submit" className="btn-margin-right"
 | 
			
		||||
                    onClick={refresh} loading={loading}>查询</Button>
 | 
			
		||||
          </Form.Section>
 | 
			
		||||
        </>
 | 
			
		||||
      </Form>
 | 
			
		||||
      <Table style={{ marginTop: 5 }} columns={columns} dataSource={pageData} pagination={{
 | 
			
		||||
        currentPage: activePage,
 | 
			
		||||
        pageSize: pageSize,
 | 
			
		||||
        total: logCount,
 | 
			
		||||
        pageSizeOpts: [10, 20, 50, 100],
 | 
			
		||||
        showSizeChanger: true,
 | 
			
		||||
        onPageSizeChange: (size) => {
 | 
			
		||||
          handlePageSizeChange(size).then();
 | 
			
		||||
        },
 | 
			
		||||
        onPageChange: handlePageChange
 | 
			
		||||
      }} />
 | 
			
		||||
      <Select defaultValue="0" style={{ width: 120 }} onChange={(value) => {
 | 
			
		||||
        setLogType(parseInt(value));
 | 
			
		||||
        refresh(parseInt(value)).then();
 | 
			
		||||
      }}>
 | 
			
		||||
        <Select.Option value="0">全部</Select.Option>
 | 
			
		||||
        <Select.Option value="1">充值</Select.Option>
 | 
			
		||||
        <Select.Option value="2">消费</Select.Option>
 | 
			
		||||
        <Select.Option value="3">管理</Select.Option>
 | 
			
		||||
        <Select.Option value="4">系统</Select.Option>
 | 
			
		||||
      </Select>
 | 
			
		||||
    </Layout>
 | 
			
		||||
  </>);
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Layout>
 | 
			
		||||
        <Header>
 | 
			
		||||
          <Spin spinning={loadingStat}>
 | 
			
		||||
            <h3>
 | 
			
		||||
              使用明细(总消耗额度:
 | 
			
		||||
              <span
 | 
			
		||||
                onClick={handleEyeClick}
 | 
			
		||||
                style={{
 | 
			
		||||
                  cursor: 'pointer',
 | 
			
		||||
                  color: 'gray',
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                {showStat ? renderQuota(stat.quota) : '点击查看'}
 | 
			
		||||
              </span>
 | 
			
		||||
              )
 | 
			
		||||
            </h3>
 | 
			
		||||
          </Spin>
 | 
			
		||||
        </Header>
 | 
			
		||||
        <Form layout='horizontal' style={{ marginTop: 10 }}>
 | 
			
		||||
          <>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              field='token_name'
 | 
			
		||||
              label='令牌名称'
 | 
			
		||||
              style={{ width: 176 }}
 | 
			
		||||
              value={token_name}
 | 
			
		||||
              placeholder={'可选值'}
 | 
			
		||||
              name='token_name'
 | 
			
		||||
              onChange={(value) => handleInputChange(value, 'token_name')}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              field='model_name'
 | 
			
		||||
              label='模型名称'
 | 
			
		||||
              style={{ width: 176 }}
 | 
			
		||||
              value={model_name}
 | 
			
		||||
              placeholder='可选值'
 | 
			
		||||
              name='model_name'
 | 
			
		||||
              onChange={(value) => handleInputChange(value, 'model_name')}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.DatePicker
 | 
			
		||||
              field='start_timestamp'
 | 
			
		||||
              label='起始时间'
 | 
			
		||||
              style={{ width: 272 }}
 | 
			
		||||
              initValue={start_timestamp}
 | 
			
		||||
              value={start_timestamp}
 | 
			
		||||
              type='dateTime'
 | 
			
		||||
              name='start_timestamp'
 | 
			
		||||
              onChange={(value) => handleInputChange(value, 'start_timestamp')}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.DatePicker
 | 
			
		||||
              field='end_timestamp'
 | 
			
		||||
              fluid
 | 
			
		||||
              label='结束时间'
 | 
			
		||||
              style={{ width: 272 }}
 | 
			
		||||
              initValue={end_timestamp}
 | 
			
		||||
              value={end_timestamp}
 | 
			
		||||
              type='dateTime'
 | 
			
		||||
              name='end_timestamp'
 | 
			
		||||
              onChange={(value) => handleInputChange(value, 'end_timestamp')}
 | 
			
		||||
            />
 | 
			
		||||
            {isAdminUser && (
 | 
			
		||||
              <>
 | 
			
		||||
                <Form.Input
 | 
			
		||||
                  field='channel'
 | 
			
		||||
                  label='渠道 ID'
 | 
			
		||||
                  style={{ width: 176 }}
 | 
			
		||||
                  value={channel}
 | 
			
		||||
                  placeholder='可选值'
 | 
			
		||||
                  name='channel'
 | 
			
		||||
                  onChange={(value) => handleInputChange(value, 'channel')}
 | 
			
		||||
                />
 | 
			
		||||
                <Form.Input
 | 
			
		||||
                  field='username'
 | 
			
		||||
                  label='用户名称'
 | 
			
		||||
                  style={{ width: 176 }}
 | 
			
		||||
                  value={username}
 | 
			
		||||
                  placeholder={'可选值'}
 | 
			
		||||
                  name='username'
 | 
			
		||||
                  onChange={(value) => handleInputChange(value, 'username')}
 | 
			
		||||
                />
 | 
			
		||||
              </>
 | 
			
		||||
            )}
 | 
			
		||||
            <Form.Section>
 | 
			
		||||
              <Button
 | 
			
		||||
                label='查询'
 | 
			
		||||
                type='primary'
 | 
			
		||||
                htmlType='submit'
 | 
			
		||||
                className='btn-margin-right'
 | 
			
		||||
                onClick={refresh}
 | 
			
		||||
                loading={loading}
 | 
			
		||||
              >
 | 
			
		||||
                查询
 | 
			
		||||
              </Button>
 | 
			
		||||
            </Form.Section>
 | 
			
		||||
          </>
 | 
			
		||||
        </Form>
 | 
			
		||||
        <Table
 | 
			
		||||
          style={{ marginTop: 5 }}
 | 
			
		||||
          columns={columns}
 | 
			
		||||
          dataSource={pageData}
 | 
			
		||||
          pagination={{
 | 
			
		||||
            currentPage: activePage,
 | 
			
		||||
            pageSize: pageSize,
 | 
			
		||||
            total: logCount,
 | 
			
		||||
            pageSizeOpts: [10, 20, 50, 100],
 | 
			
		||||
            showSizeChanger: true,
 | 
			
		||||
            onPageSizeChange: (size) => {
 | 
			
		||||
              handlePageSizeChange(size).then();
 | 
			
		||||
            },
 | 
			
		||||
            onPageChange: handlePageChange,
 | 
			
		||||
          }}
 | 
			
		||||
        />
 | 
			
		||||
        <Select
 | 
			
		||||
          defaultValue='0'
 | 
			
		||||
          style={{ width: 120 }}
 | 
			
		||||
          onChange={(value) => {
 | 
			
		||||
            setLogType(parseInt(value));
 | 
			
		||||
            refresh(parseInt(value)).then();
 | 
			
		||||
          }}
 | 
			
		||||
        >
 | 
			
		||||
          <Select.Option value='0'>全部</Select.Option>
 | 
			
		||||
          <Select.Option value='1'>充值</Select.Option>
 | 
			
		||||
          <Select.Option value='2'>消费</Select.Option>
 | 
			
		||||
          <Select.Option value='3'>管理</Select.Option>
 | 
			
		||||
          <Select.Option value='4'>系统</Select.Option>
 | 
			
		||||
        </Select>
 | 
			
		||||
      </Layout>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default LogsTable;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,86 +1,226 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { API, copy, isAdmin, showError, showSuccess, timestamp2string } from '../helpers';
 | 
			
		||||
import {
 | 
			
		||||
  API,
 | 
			
		||||
  copy,
 | 
			
		||||
  isAdmin,
 | 
			
		||||
  showError,
 | 
			
		||||
  showSuccess,
 | 
			
		||||
  timestamp2string,
 | 
			
		||||
} from '../helpers';
 | 
			
		||||
 | 
			
		||||
import { Banner, Button, Form, ImagePreview, Layout, Modal, Progress, Table, Tag, Typography } from '@douyinfe/semi-ui';
 | 
			
		||||
import {
 | 
			
		||||
  Banner,
 | 
			
		||||
  Button,
 | 
			
		||||
  Form,
 | 
			
		||||
  ImagePreview,
 | 
			
		||||
  Layout,
 | 
			
		||||
  Modal,
 | 
			
		||||
  Progress,
 | 
			
		||||
  Table,
 | 
			
		||||
  Tag,
 | 
			
		||||
  Typography,
 | 
			
		||||
} from '@douyinfe/semi-ui';
 | 
			
		||||
import { ITEMS_PER_PAGE } from '../constants';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const colors = ['amber', 'blue', 'cyan', 'green', 'grey', 'indigo',
 | 
			
		||||
  'light-blue', 'lime', 'orange', 'pink',
 | 
			
		||||
  'purple', 'red', 'teal', 'violet', 'yellow'
 | 
			
		||||
const colors = [
 | 
			
		||||
  'amber',
 | 
			
		||||
  'blue',
 | 
			
		||||
  'cyan',
 | 
			
		||||
  'green',
 | 
			
		||||
  'grey',
 | 
			
		||||
  'indigo',
 | 
			
		||||
  'light-blue',
 | 
			
		||||
  'lime',
 | 
			
		||||
  'orange',
 | 
			
		||||
  'pink',
 | 
			
		||||
  'purple',
 | 
			
		||||
  'red',
 | 
			
		||||
  'teal',
 | 
			
		||||
  'violet',
 | 
			
		||||
  'yellow',
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
function renderType(type) {
 | 
			
		||||
  switch (type) {
 | 
			
		||||
    case 'IMAGINE':
 | 
			
		||||
      return <Tag color="blue" size="large">绘图</Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='blue' size='large'>
 | 
			
		||||
          绘图
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    case 'UPSCALE':
 | 
			
		||||
      return <Tag color="orange" size="large">放大</Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='orange' size='large'>
 | 
			
		||||
          放大
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    case 'VARIATION':
 | 
			
		||||
      return <Tag color="purple" size="large">变换</Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='purple' size='large'>
 | 
			
		||||
          变换
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    case 'HIGH_VARIATION':
 | 
			
		||||
      return <Tag color="purple" size="large">强变换</Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='purple' size='large'>
 | 
			
		||||
          强变换
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    case 'LOW_VARIATION':
 | 
			
		||||
      return <Tag color="purple" size="large">弱变换</Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='purple' size='large'>
 | 
			
		||||
          弱变换
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    case 'PAN':
 | 
			
		||||
      return <Tag color="cyan" size="large">平移</Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='cyan' size='large'>
 | 
			
		||||
          平移
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    case 'DESCRIBE':
 | 
			
		||||
      return <Tag color="yellow" size="large">图生文</Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='yellow' size='large'>
 | 
			
		||||
          图生文
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    case 'BLEND':
 | 
			
		||||
      return <Tag color="lime" size="large">图混合</Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='lime' size='large'>
 | 
			
		||||
          图混合
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    case 'SHORTEN':
 | 
			
		||||
      return <Tag color="pink" size="large">缩词</Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='pink' size='large'>
 | 
			
		||||
          缩词
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    case 'REROLL':
 | 
			
		||||
      return <Tag color="indigo" size="large">重绘</Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='indigo' size='large'>
 | 
			
		||||
          重绘
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    case 'INPAINT':
 | 
			
		||||
      return <Tag color="violet" size="large">局部重绘-提交</Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='violet' size='large'>
 | 
			
		||||
          局部重绘-提交
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    case 'ZOOM':
 | 
			
		||||
      return <Tag color="teal" size="large">变焦</Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='teal' size='large'>
 | 
			
		||||
          变焦
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    case 'CUSTOM_ZOOM':
 | 
			
		||||
      return <Tag color="teal" size="large">自定义变焦-提交</Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='teal' size='large'>
 | 
			
		||||
          自定义变焦-提交
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    case 'MODAL':
 | 
			
		||||
      return <Tag color="green" size="large">窗口处理</Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='green' size='large'>
 | 
			
		||||
          窗口处理
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    case 'SWAP_FACE':
 | 
			
		||||
      return <Tag color="light-green" size="large">换脸</Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='light-green' size='large'>
 | 
			
		||||
          换脸
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    default:
 | 
			
		||||
      return <Tag color="white" size="large">未知</Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='white' size='large'>
 | 
			
		||||
          未知
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function renderCode(code) {
 | 
			
		||||
  switch (code) {
 | 
			
		||||
    case 1:
 | 
			
		||||
      return <Tag color="green" size="large">已提交</Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='green' size='large'>
 | 
			
		||||
          已提交
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    case 21:
 | 
			
		||||
      return <Tag color="lime" size="large">等待中</Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='lime' size='large'>
 | 
			
		||||
          等待中
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    case 22:
 | 
			
		||||
      return <Tag color="orange" size="large">重复提交</Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='orange' size='large'>
 | 
			
		||||
          重复提交
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    case 0:
 | 
			
		||||
      return <Tag color="yellow" size="large">未提交</Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='yellow' size='large'>
 | 
			
		||||
          未提交
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    default:
 | 
			
		||||
      return <Tag color="white" size="large">未知</Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='white' size='large'>
 | 
			
		||||
          未知
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function renderStatus(type) {
 | 
			
		||||
  // Ensure all cases are string literals by adding quotes.
 | 
			
		||||
  switch (type) {
 | 
			
		||||
    case 'SUCCESS':
 | 
			
		||||
      return <Tag color="green" size="large">成功</Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='green' size='large'>
 | 
			
		||||
          成功
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    case 'NOT_START':
 | 
			
		||||
      return <Tag color="grey" size="large">未启动</Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='grey' size='large'>
 | 
			
		||||
          未启动
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    case 'SUBMITTED':
 | 
			
		||||
      return <Tag color="yellow" size="large">队列中</Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='yellow' size='large'>
 | 
			
		||||
          队列中
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    case 'IN_PROGRESS':
 | 
			
		||||
      return <Tag color="blue" size="large">执行中</Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='blue' size='large'>
 | 
			
		||||
          执行中
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    case 'FAILURE':
 | 
			
		||||
      return <Tag color="red" size="large">失败</Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='red' size='large'>
 | 
			
		||||
          失败
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    case 'MODAL':
 | 
			
		||||
      return <Tag color="yellow" size="large">窗口等待</Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='yellow' size='large'>
 | 
			
		||||
          窗口等待
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    default:
 | 
			
		||||
      return <Tag color="white" size="large">未知</Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='white' size='large'>
 | 
			
		||||
          未知
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -97,7 +237,6 @@ const renderTimestamp = (timestampInSeconds) => {
 | 
			
		||||
  return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; // 格式化输出
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
const LogsTable = () => {
 | 
			
		||||
  const [isModalOpen, setIsModalOpen] = useState(false);
 | 
			
		||||
  const [modalContent, setModalContent] = useState('');
 | 
			
		||||
@@ -106,12 +245,8 @@ const LogsTable = () => {
 | 
			
		||||
      title: '提交时间',
 | 
			
		||||
      dataIndex: 'submit_time',
 | 
			
		||||
      render: (text, record, index) => {
 | 
			
		||||
        return (
 | 
			
		||||
          <div>
 | 
			
		||||
            {renderTimestamp(text / 1000)}
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
        return <div>{renderTimestamp(text / 1000)}</div>;
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '渠道',
 | 
			
		||||
@@ -119,61 +254,50 @@ const LogsTable = () => {
 | 
			
		||||
      className: isAdmin() ? 'tableShow' : 'tableHiddle',
 | 
			
		||||
      render: (text, record, index) => {
 | 
			
		||||
        return (
 | 
			
		||||
 | 
			
		||||
          <div>
 | 
			
		||||
            <Tag color={colors[parseInt(text) % colors.length]} size="large" onClick={() => {
 | 
			
		||||
              copyText(text); // 假设copyText是用于文本复制的函数
 | 
			
		||||
            }}> {text} </Tag>
 | 
			
		||||
            <Tag
 | 
			
		||||
              color={colors[parseInt(text) % colors.length]}
 | 
			
		||||
              size='large'
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                copyText(text); // 假设copyText是用于文本复制的函数
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              {' '}
 | 
			
		||||
              {text}{' '}
 | 
			
		||||
            </Tag>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '类型',
 | 
			
		||||
      dataIndex: 'action',
 | 
			
		||||
      render: (text, record, index) => {
 | 
			
		||||
        return (
 | 
			
		||||
          <div>
 | 
			
		||||
            {renderType(text)}
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
        return <div>{renderType(text)}</div>;
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '任务ID',
 | 
			
		||||
      dataIndex: 'mj_id',
 | 
			
		||||
      render: (text, record, index) => {
 | 
			
		||||
        return (
 | 
			
		||||
          <div>
 | 
			
		||||
            {text}
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
        return <div>{text}</div>;
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '提交结果',
 | 
			
		||||
      dataIndex: 'code',
 | 
			
		||||
      className: isAdmin() ? 'tableShow' : 'tableHiddle',
 | 
			
		||||
      render: (text, record, index) => {
 | 
			
		||||
        return (
 | 
			
		||||
          <div>
 | 
			
		||||
            {renderCode(text)}
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
        return <div>{renderCode(text)}</div>;
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '任务状态',
 | 
			
		||||
      dataIndex: 'status',
 | 
			
		||||
      className: isAdmin() ? 'tableShow' : 'tableHiddle',
 | 
			
		||||
      render: (text, record, index) => {
 | 
			
		||||
        return (
 | 
			
		||||
          <div>
 | 
			
		||||
            {renderStatus(text)}
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
        return <div>{renderStatus(text)}</div>;
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '进度',
 | 
			
		||||
@@ -183,13 +307,20 @@ const LogsTable = () => {
 | 
			
		||||
          <div>
 | 
			
		||||
            {
 | 
			
		||||
              // 转换例如100%为数字100,如果text未定义,返回0
 | 
			
		||||
              <Progress stroke={record.status === 'FAILURE' ? 'var(--semi-color-warning)' : null}
 | 
			
		||||
                        percent={text ? parseInt(text.replace('%', '')) : 0} showInfo={true}
 | 
			
		||||
                        aria-label="drawing progress" />
 | 
			
		||||
              <Progress
 | 
			
		||||
                stroke={
 | 
			
		||||
                  record.status === 'FAILURE'
 | 
			
		||||
                    ? 'var(--semi-color-warning)'
 | 
			
		||||
                    : null
 | 
			
		||||
                }
 | 
			
		||||
                percent={text ? parseInt(text.replace('%', '')) : 0}
 | 
			
		||||
                showInfo={true}
 | 
			
		||||
                aria-label='drawing progress'
 | 
			
		||||
              />
 | 
			
		||||
            }
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '结果图片',
 | 
			
		||||
@@ -201,14 +332,14 @@ const LogsTable = () => {
 | 
			
		||||
        return (
 | 
			
		||||
          <Button
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              setModalImageUrl(text);  // 更新图片URL状态
 | 
			
		||||
              setIsModalOpenurl(true);    // 打开模态框
 | 
			
		||||
              setModalImageUrl(text); // 更新图片URL状态
 | 
			
		||||
              setIsModalOpenurl(true); // 打开模态框
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            查看图片
 | 
			
		||||
          </Button>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: 'Prompt',
 | 
			
		||||
@@ -231,7 +362,7 @@ const LogsTable = () => {
 | 
			
		||||
            {text}
 | 
			
		||||
          </Typography.Text>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: 'PromptEn',
 | 
			
		||||
@@ -254,7 +385,7 @@ const LogsTable = () => {
 | 
			
		||||
            {text}
 | 
			
		||||
          </Typography.Text>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '失败原因',
 | 
			
		||||
@@ -277,9 +408,8 @@ const LogsTable = () => {
 | 
			
		||||
            {text}
 | 
			
		||||
          </Typography.Text>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  const [logs, setLogs] = useState([]);
 | 
			
		||||
@@ -299,20 +429,19 @@ const LogsTable = () => {
 | 
			
		||||
    channel_id: '',
 | 
			
		||||
    mj_id: '',
 | 
			
		||||
    start_timestamp: timestamp2string(now.getTime() / 1000 - 2592000),
 | 
			
		||||
    end_timestamp: timestamp2string(now.getTime() / 1000 + 3600)
 | 
			
		||||
    end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
 | 
			
		||||
  });
 | 
			
		||||
  const { channel_id, mj_id, start_timestamp, end_timestamp } = inputs;
 | 
			
		||||
 | 
			
		||||
  const [stat, setStat] = useState({
 | 
			
		||||
    quota: 0,
 | 
			
		||||
    token: 0
 | 
			
		||||
    token: 0,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const handleInputChange = (value, name) => {
 | 
			
		||||
    setInputs((inputs) => ({ ...inputs, [name]: value }));
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  const setLogsFormat = (logs) => {
 | 
			
		||||
    for (let i = 0; i < logs.length; i++) {
 | 
			
		||||
      logs[i].timestamp2string = timestamp2string(logs[i].created_at);
 | 
			
		||||
@@ -351,14 +480,16 @@ const LogsTable = () => {
 | 
			
		||||
    setLoading(false);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const pageData = logs.slice((activePage - 1) * ITEMS_PER_PAGE, activePage * ITEMS_PER_PAGE);
 | 
			
		||||
  const pageData = logs.slice(
 | 
			
		||||
    (activePage - 1) * ITEMS_PER_PAGE,
 | 
			
		||||
    activePage * ITEMS_PER_PAGE,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const handlePageChange = page => {
 | 
			
		||||
  const handlePageChange = (page) => {
 | 
			
		||||
    setActivePage(page);
 | 
			
		||||
    if (page === Math.ceil(logs.length / ITEMS_PER_PAGE) + 1) {
 | 
			
		||||
      // In this case we have to load more data and then append them.
 | 
			
		||||
      loadLogs(page - 1).then(r => {
 | 
			
		||||
      });
 | 
			
		||||
      loadLogs(page - 1).then((r) => {});
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@@ -390,46 +521,83 @@ const LogsTable = () => {
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
 | 
			
		||||
      <Layout>
 | 
			
		||||
        {isAdminUser && showBanner ? <Banner
 | 
			
		||||
          type="info"
 | 
			
		||||
          description="当前未开启Midjourney回调,部分项目可能无法获得绘图结果,可在运营设置中开启。"
 | 
			
		||||
        /> : <></>
 | 
			
		||||
        }
 | 
			
		||||
        <Form layout="horizontal" style={{ marginTop: 10 }}>
 | 
			
		||||
        {isAdminUser && showBanner ? (
 | 
			
		||||
          <Banner
 | 
			
		||||
            type='info'
 | 
			
		||||
            description='当前未开启Midjourney回调,部分项目可能无法获得绘图结果,可在运营设置中开启。'
 | 
			
		||||
          />
 | 
			
		||||
        ) : (
 | 
			
		||||
          <></>
 | 
			
		||||
        )}
 | 
			
		||||
        <Form layout='horizontal' style={{ marginTop: 10 }}>
 | 
			
		||||
          <>
 | 
			
		||||
            <Form.Input field="channel_id" label="渠道 ID" style={{ width: 176 }} value={channel_id}
 | 
			
		||||
                        placeholder={'可选值'} name="channel_id"
 | 
			
		||||
                        onChange={value => handleInputChange(value, 'channel_id')} />
 | 
			
		||||
            <Form.Input field="mj_id" label="任务 ID" style={{ width: 176 }} value={mj_id}
 | 
			
		||||
                        placeholder="可选值"
 | 
			
		||||
                        name="mj_id"
 | 
			
		||||
                        onChange={value => handleInputChange(value, 'mj_id')} />
 | 
			
		||||
            <Form.DatePicker field="start_timestamp" label="起始时间" style={{ width: 272 }}
 | 
			
		||||
                             initValue={start_timestamp}
 | 
			
		||||
                             value={start_timestamp} type="dateTime"
 | 
			
		||||
                             name="start_timestamp"
 | 
			
		||||
                             onChange={value => handleInputChange(value, 'start_timestamp')} />
 | 
			
		||||
            <Form.DatePicker field="end_timestamp" fluid label="结束时间" style={{ width: 272 }}
 | 
			
		||||
                             initValue={end_timestamp}
 | 
			
		||||
                             value={end_timestamp} type="dateTime"
 | 
			
		||||
                             name="end_timestamp"
 | 
			
		||||
                             onChange={value => handleInputChange(value, 'end_timestamp')} />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              field='channel_id'
 | 
			
		||||
              label='渠道 ID'
 | 
			
		||||
              style={{ width: 176 }}
 | 
			
		||||
              value={channel_id}
 | 
			
		||||
              placeholder={'可选值'}
 | 
			
		||||
              name='channel_id'
 | 
			
		||||
              onChange={(value) => handleInputChange(value, 'channel_id')}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              field='mj_id'
 | 
			
		||||
              label='任务 ID'
 | 
			
		||||
              style={{ width: 176 }}
 | 
			
		||||
              value={mj_id}
 | 
			
		||||
              placeholder='可选值'
 | 
			
		||||
              name='mj_id'
 | 
			
		||||
              onChange={(value) => handleInputChange(value, 'mj_id')}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.DatePicker
 | 
			
		||||
              field='start_timestamp'
 | 
			
		||||
              label='起始时间'
 | 
			
		||||
              style={{ width: 272 }}
 | 
			
		||||
              initValue={start_timestamp}
 | 
			
		||||
              value={start_timestamp}
 | 
			
		||||
              type='dateTime'
 | 
			
		||||
              name='start_timestamp'
 | 
			
		||||
              onChange={(value) => handleInputChange(value, 'start_timestamp')}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.DatePicker
 | 
			
		||||
              field='end_timestamp'
 | 
			
		||||
              fluid
 | 
			
		||||
              label='结束时间'
 | 
			
		||||
              style={{ width: 272 }}
 | 
			
		||||
              initValue={end_timestamp}
 | 
			
		||||
              value={end_timestamp}
 | 
			
		||||
              type='dateTime'
 | 
			
		||||
              name='end_timestamp'
 | 
			
		||||
              onChange={(value) => handleInputChange(value, 'end_timestamp')}
 | 
			
		||||
            />
 | 
			
		||||
 | 
			
		||||
            <Form.Section>
 | 
			
		||||
              <Button label="查询" type="primary" htmlType="submit" className="btn-margin-right"
 | 
			
		||||
                      onClick={refresh}>查询</Button>
 | 
			
		||||
              <Button
 | 
			
		||||
                label='查询'
 | 
			
		||||
                type='primary'
 | 
			
		||||
                htmlType='submit'
 | 
			
		||||
                className='btn-margin-right'
 | 
			
		||||
                onClick={refresh}
 | 
			
		||||
              >
 | 
			
		||||
                查询
 | 
			
		||||
              </Button>
 | 
			
		||||
            </Form.Section>
 | 
			
		||||
          </>
 | 
			
		||||
        </Form>
 | 
			
		||||
        <Table style={{ marginTop: 5 }} columns={columns} dataSource={pageData} pagination={{
 | 
			
		||||
          currentPage: activePage,
 | 
			
		||||
          pageSize: ITEMS_PER_PAGE,
 | 
			
		||||
          total: logCount,
 | 
			
		||||
          pageSizeOpts: [10, 20, 50, 100],
 | 
			
		||||
          onPageChange: handlePageChange
 | 
			
		||||
        }} loading={loading} />
 | 
			
		||||
        <Table
 | 
			
		||||
          style={{ marginTop: 5 }}
 | 
			
		||||
          columns={columns}
 | 
			
		||||
          dataSource={pageData}
 | 
			
		||||
          pagination={{
 | 
			
		||||
            currentPage: activePage,
 | 
			
		||||
            pageSize: ITEMS_PER_PAGE,
 | 
			
		||||
            total: logCount,
 | 
			
		||||
            pageSizeOpts: [10, 20, 50, 100],
 | 
			
		||||
            onPageChange: handlePageChange,
 | 
			
		||||
          }}
 | 
			
		||||
          loading={loading}
 | 
			
		||||
        />
 | 
			
		||||
        <Modal
 | 
			
		||||
          visible={isModalOpen}
 | 
			
		||||
          onOk={() => setIsModalOpen(false)}
 | 
			
		||||
@@ -445,7 +613,6 @@ const LogsTable = () => {
 | 
			
		||||
          visible={isModalOpenurl}
 | 
			
		||||
          onVisibleChange={(visible) => setIsModalOpenurl(visible)}
 | 
			
		||||
        />
 | 
			
		||||
 | 
			
		||||
      </Layout>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,12 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { Divider, Form, Grid, Header } from 'semantic-ui-react';
 | 
			
		||||
import { API, showError, showSuccess, timestamp2string, verifyJSON } from '../helpers';
 | 
			
		||||
import {
 | 
			
		||||
  API,
 | 
			
		||||
  showError,
 | 
			
		||||
  showSuccess,
 | 
			
		||||
  timestamp2string,
 | 
			
		||||
  verifyJSON,
 | 
			
		||||
} from '../helpers';
 | 
			
		||||
 | 
			
		||||
const OperationSetting = () => {
 | 
			
		||||
  let now = new Date();
 | 
			
		||||
@@ -35,16 +41,18 @@ const OperationSetting = () => {
 | 
			
		||||
    DataExportDefaultTime: 'hour',
 | 
			
		||||
    DataExportInterval: 5,
 | 
			
		||||
    DefaultCollapseSidebar: '', // 默认折叠侧边栏
 | 
			
		||||
    RetryTimes: 0
 | 
			
		||||
    RetryTimes: 0,
 | 
			
		||||
  });
 | 
			
		||||
  const [originInputs, setOriginInputs] = useState({});
 | 
			
		||||
  let [loading, setLoading] = useState(false);
 | 
			
		||||
  let [historyTimestamp, setHistoryTimestamp] = useState(timestamp2string(now.getTime() / 1000 - 30 * 24 * 3600)); // a month ago
 | 
			
		||||
  let [historyTimestamp, setHistoryTimestamp] = useState(
 | 
			
		||||
    timestamp2string(now.getTime() / 1000 - 30 * 24 * 3600),
 | 
			
		||||
  ); // a month ago
 | 
			
		||||
  // 精确时间选项(小时,天,周)
 | 
			
		||||
  const timeOptions = [
 | 
			
		||||
    { key: 'hour', text: '小时', value: 'hour' },
 | 
			
		||||
    { key: 'day', text: '天', value: 'day' },
 | 
			
		||||
    { key: 'week', text: '周', value: 'week' }
 | 
			
		||||
    { key: 'week', text: '周', value: 'week' },
 | 
			
		||||
  ];
 | 
			
		||||
  const getOptions = async () => {
 | 
			
		||||
    const res = await API.get('/api/option/');
 | 
			
		||||
@@ -52,7 +60,11 @@ const OperationSetting = () => {
 | 
			
		||||
    if (success) {
 | 
			
		||||
      let newInputs = {};
 | 
			
		||||
      data.forEach((item) => {
 | 
			
		||||
        if (item.key === 'ModelRatio' || item.key === 'GroupRatio' || item.key === 'ModelPrice') {
 | 
			
		||||
        if (
 | 
			
		||||
          item.key === 'ModelRatio' ||
 | 
			
		||||
          item.key === 'GroupRatio' ||
 | 
			
		||||
          item.key === 'ModelPrice'
 | 
			
		||||
        ) {
 | 
			
		||||
          item.value = JSON.stringify(JSON.parse(item.value), null, 2);
 | 
			
		||||
        }
 | 
			
		||||
        newInputs[item.key] = item.value;
 | 
			
		||||
@@ -79,7 +91,7 @@ const OperationSetting = () => {
 | 
			
		||||
    console.log(key, value);
 | 
			
		||||
    const res = await API.put('/api/option/', {
 | 
			
		||||
      key,
 | 
			
		||||
      value
 | 
			
		||||
      value,
 | 
			
		||||
    });
 | 
			
		||||
    const { success, message } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
@@ -91,7 +103,12 @@ const OperationSetting = () => {
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleInputChange = async (e, { name, value }) => {
 | 
			
		||||
    if (name.endsWith('Enabled') || name === 'DataExportInterval' || name === 'DataExportDefaultTime' || name === 'DefaultCollapseSidebar') {
 | 
			
		||||
    if (
 | 
			
		||||
      name.endsWith('Enabled') ||
 | 
			
		||||
      name === 'DataExportInterval' ||
 | 
			
		||||
      name === 'DataExportDefaultTime' ||
 | 
			
		||||
      name === 'DefaultCollapseSidebar'
 | 
			
		||||
    ) {
 | 
			
		||||
      if (name === 'DataExportDefaultTime') {
 | 
			
		||||
        localStorage.setItem('data_export_default_time', value);
 | 
			
		||||
      } else if (name === 'MjNotifyEnabled') {
 | 
			
		||||
@@ -106,11 +123,22 @@ const OperationSetting = () => {
 | 
			
		||||
  const submitConfig = async (group) => {
 | 
			
		||||
    switch (group) {
 | 
			
		||||
      case 'monitor':
 | 
			
		||||
        if (originInputs['ChannelDisableThreshold'] !== inputs.ChannelDisableThreshold) {
 | 
			
		||||
          await updateOption('ChannelDisableThreshold', inputs.ChannelDisableThreshold);
 | 
			
		||||
        if (
 | 
			
		||||
          originInputs['ChannelDisableThreshold'] !==
 | 
			
		||||
          inputs.ChannelDisableThreshold
 | 
			
		||||
        ) {
 | 
			
		||||
          await updateOption(
 | 
			
		||||
            'ChannelDisableThreshold',
 | 
			
		||||
            inputs.ChannelDisableThreshold,
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
        if (originInputs['QuotaRemindThreshold'] !== inputs.QuotaRemindThreshold) {
 | 
			
		||||
          await updateOption('QuotaRemindThreshold', inputs.QuotaRemindThreshold);
 | 
			
		||||
        if (
 | 
			
		||||
          originInputs['QuotaRemindThreshold'] !== inputs.QuotaRemindThreshold
 | 
			
		||||
        ) {
 | 
			
		||||
          await updateOption(
 | 
			
		||||
            'QuotaRemindThreshold',
 | 
			
		||||
            inputs.QuotaRemindThreshold,
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
      case 'ratio':
 | 
			
		||||
@@ -177,7 +205,9 @@ const OperationSetting = () => {
 | 
			
		||||
 | 
			
		||||
  const deleteHistoryLogs = async () => {
 | 
			
		||||
    console.log(inputs);
 | 
			
		||||
    const res = await API.delete(`/api/log/?target_timestamp=${Date.parse(historyTimestamp) / 1000}`);
 | 
			
		||||
    const res = await API.delete(
 | 
			
		||||
      `/api/log/?target_timestamp=${Date.parse(historyTimestamp) / 1000}`,
 | 
			
		||||
    );
 | 
			
		||||
    const { success, message, data } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      showSuccess(`${data} 条日志已清理!`);
 | 
			
		||||
@@ -189,131 +219,129 @@ const OperationSetting = () => {
 | 
			
		||||
    <Grid columns={1}>
 | 
			
		||||
      <Grid.Column>
 | 
			
		||||
        <Form loading={loading}>
 | 
			
		||||
          <Header as="h3">
 | 
			
		||||
            通用设置
 | 
			
		||||
          </Header>
 | 
			
		||||
          <Header as='h3'>通用设置</Header>
 | 
			
		||||
          <Form.Group widths={4}>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label="充值链接"
 | 
			
		||||
              name="TopUpLink"
 | 
			
		||||
              label='充值链接'
 | 
			
		||||
              name='TopUpLink'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              autoComplete="new-password"
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.TopUpLink}
 | 
			
		||||
              type="link"
 | 
			
		||||
              placeholder="例如发卡网站的购买链接"
 | 
			
		||||
              type='link'
 | 
			
		||||
              placeholder='例如发卡网站的购买链接'
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label="默认聊天页面链接"
 | 
			
		||||
              name="ChatLink"
 | 
			
		||||
              label='默认聊天页面链接'
 | 
			
		||||
              name='ChatLink'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              autoComplete="new-password"
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.ChatLink}
 | 
			
		||||
              type="link"
 | 
			
		||||
              placeholder="例如 ChatGPT Next Web 的部署地址"
 | 
			
		||||
              type='link'
 | 
			
		||||
              placeholder='例如 ChatGPT Next Web 的部署地址'
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label="聊天页面2链接"
 | 
			
		||||
              name="ChatLink2"
 | 
			
		||||
              label='聊天页面2链接'
 | 
			
		||||
              name='ChatLink2'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              autoComplete="new-password"
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.ChatLink2}
 | 
			
		||||
              type="link"
 | 
			
		||||
              placeholder="例如 ChatGPT Web & Midjourney 的部署地址"
 | 
			
		||||
              type='link'
 | 
			
		||||
              placeholder='例如 ChatGPT Web & Midjourney 的部署地址'
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label="单位美元额度"
 | 
			
		||||
              name="QuotaPerUnit"
 | 
			
		||||
              label='单位美元额度'
 | 
			
		||||
              name='QuotaPerUnit'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              autoComplete="new-password"
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.QuotaPerUnit}
 | 
			
		||||
              type="number"
 | 
			
		||||
              step="0.01"
 | 
			
		||||
              placeholder="一单位货币能兑换的额度"
 | 
			
		||||
              type='number'
 | 
			
		||||
              step='0.01'
 | 
			
		||||
              placeholder='一单位货币能兑换的额度'
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label="失败重试次数"
 | 
			
		||||
              name="RetryTimes"
 | 
			
		||||
              label='失败重试次数'
 | 
			
		||||
              name='RetryTimes'
 | 
			
		||||
              type={'number'}
 | 
			
		||||
              step="1"
 | 
			
		||||
              min="0"
 | 
			
		||||
              step='1'
 | 
			
		||||
              min='0'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              autoComplete="new-password"
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.RetryTimes}
 | 
			
		||||
              placeholder="失败重试次数"
 | 
			
		||||
              placeholder='失败重试次数'
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Group inline>
 | 
			
		||||
            <Form.Checkbox
 | 
			
		||||
              checked={inputs.DisplayInCurrencyEnabled === 'true'}
 | 
			
		||||
              label="以货币形式显示额度"
 | 
			
		||||
              name="DisplayInCurrencyEnabled"
 | 
			
		||||
              label='以货币形式显示额度'
 | 
			
		||||
              name='DisplayInCurrencyEnabled'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Checkbox
 | 
			
		||||
              checked={inputs.DisplayTokenStatEnabled === 'true'}
 | 
			
		||||
              label="Billing 相关 API 显示令牌额度而非用户额度"
 | 
			
		||||
              name="DisplayTokenStatEnabled"
 | 
			
		||||
              label='Billing 相关 API 显示令牌额度而非用户额度'
 | 
			
		||||
              name='DisplayTokenStatEnabled'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Checkbox
 | 
			
		||||
              checked={inputs.DefaultCollapseSidebar === 'true'}
 | 
			
		||||
              label="默认折叠侧边栏"
 | 
			
		||||
              name="DefaultCollapseSidebar"
 | 
			
		||||
              label='默认折叠侧边栏'
 | 
			
		||||
              name='DefaultCollapseSidebar'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Button onClick={() => {
 | 
			
		||||
            submitConfig('general').then();
 | 
			
		||||
          }}>保存通用设置</Form.Button>
 | 
			
		||||
          <Form.Button
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              submitConfig('general').then();
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            保存通用设置
 | 
			
		||||
          </Form.Button>
 | 
			
		||||
          <Divider />
 | 
			
		||||
          <Header as="h3">
 | 
			
		||||
            绘图设置
 | 
			
		||||
          </Header>
 | 
			
		||||
          <Header as='h3'>绘图设置</Header>
 | 
			
		||||
          <Form.Group inline>
 | 
			
		||||
            <Form.Checkbox
 | 
			
		||||
              checked={inputs.DrawingEnabled === 'true'}
 | 
			
		||||
              label="启用绘图功能"
 | 
			
		||||
              name="DrawingEnabled"
 | 
			
		||||
              label='启用绘图功能'
 | 
			
		||||
              name='DrawingEnabled'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Checkbox
 | 
			
		||||
              checked={inputs.MjNotifyEnabled === 'true'}
 | 
			
		||||
              label="允许回调(会泄露服务器ip地址)"
 | 
			
		||||
              name="MjNotifyEnabled"
 | 
			
		||||
              label='允许回调(会泄露服务器ip地址)'
 | 
			
		||||
              name='MjNotifyEnabled'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Divider />
 | 
			
		||||
          <Header as="h3">
 | 
			
		||||
            屏蔽词过滤设置
 | 
			
		||||
          </Header>
 | 
			
		||||
          <Header as='h3'>屏蔽词过滤设置</Header>
 | 
			
		||||
          <Form.Group inline>
 | 
			
		||||
            <Form.Checkbox
 | 
			
		||||
              checked={inputs.CheckSensitiveEnabled === 'true'}
 | 
			
		||||
              label="启用屏蔽词过滤功能"
 | 
			
		||||
              name="CheckSensitiveEnabled"
 | 
			
		||||
              label='启用屏蔽词过滤功能'
 | 
			
		||||
              name='CheckSensitiveEnabled'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Group inline>
 | 
			
		||||
            <Form.Checkbox
 | 
			
		||||
              checked={inputs.CheckSensitiveOnPromptEnabled === 'true'}
 | 
			
		||||
              label="启用prompt检查"
 | 
			
		||||
              name="CheckSensitiveOnPromptEnabled"
 | 
			
		||||
              label='启用prompt检查'
 | 
			
		||||
              name='CheckSensitiveOnPromptEnabled'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Checkbox
 | 
			
		||||
              checked={inputs.CheckSensitiveOnCompletionEnabled === 'true'}
 | 
			
		||||
              label="启用生成内容检查"
 | 
			
		||||
              name="CheckSensitiveOnCompletionEnabled"
 | 
			
		||||
              label='启用生成内容检查'
 | 
			
		||||
              name='CheckSensitiveOnCompletionEnabled'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Group inline>
 | 
			
		||||
            <Form.Checkbox
 | 
			
		||||
              checked={inputs.StopOnSensitiveEnabled === 'true'}
 | 
			
		||||
              label="在检测到屏蔽词时,立刻停止生成,否则替换屏蔽词"
 | 
			
		||||
              name="StopOnSensitiveEnabled"
 | 
			
		||||
              label='在检测到屏蔽词时,立刻停止生成,否则替换屏蔽词'
 | 
			
		||||
              name='StopOnSensitiveEnabled'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
@@ -328,210 +356,223 @@ const OperationSetting = () => {
 | 
			
		||||
          {/*    placeholder="例如:10"*/}
 | 
			
		||||
          {/*  />*/}
 | 
			
		||||
          {/*</Form.Group>*/}
 | 
			
		||||
          <Form.Group widths="equal">
 | 
			
		||||
          <Form.Group widths='equal'>
 | 
			
		||||
            <Form.TextArea
 | 
			
		||||
              label="屏蔽词列表,一行一个屏蔽词,不需要符号分割"
 | 
			
		||||
              name="SensitiveWords"
 | 
			
		||||
              label='屏蔽词列表,一行一个屏蔽词,不需要符号分割'
 | 
			
		||||
              name='SensitiveWords'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
 | 
			
		||||
              value={inputs.SensitiveWords}
 | 
			
		||||
              placeholder="一行一个屏蔽词"
 | 
			
		||||
              placeholder='一行一个屏蔽词'
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Button onClick={() => {
 | 
			
		||||
            submitConfig('words').then();
 | 
			
		||||
          }}>保存屏蔽词设置</Form.Button>
 | 
			
		||||
          <Form.Button
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              submitConfig('words').then();
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            保存屏蔽词设置
 | 
			
		||||
          </Form.Button>
 | 
			
		||||
          <Divider />
 | 
			
		||||
          <Header as="h3">
 | 
			
		||||
            日志设置
 | 
			
		||||
          </Header>
 | 
			
		||||
          <Header as='h3'>日志设置</Header>
 | 
			
		||||
          <Form.Group inline>
 | 
			
		||||
            <Form.Checkbox
 | 
			
		||||
              checked={inputs.LogConsumeEnabled === 'true'}
 | 
			
		||||
              label="启用额度消费日志记录"
 | 
			
		||||
              name="LogConsumeEnabled"
 | 
			
		||||
              label='启用额度消费日志记录'
 | 
			
		||||
              name='LogConsumeEnabled'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Group widths={4}>
 | 
			
		||||
            <Form.Input label="目标时间" value={historyTimestamp} type="datetime-local"
 | 
			
		||||
                        name="history_timestamp"
 | 
			
		||||
                        onChange={(e, { name, value }) => {
 | 
			
		||||
                          setHistoryTimestamp(value);
 | 
			
		||||
                        }} />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label='目标时间'
 | 
			
		||||
              value={historyTimestamp}
 | 
			
		||||
              type='datetime-local'
 | 
			
		||||
              name='history_timestamp'
 | 
			
		||||
              onChange={(e, { name, value }) => {
 | 
			
		||||
                setHistoryTimestamp(value);
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Button onClick={() => {
 | 
			
		||||
            deleteHistoryLogs().then();
 | 
			
		||||
          }}>清理历史日志</Form.Button>
 | 
			
		||||
          <Form.Button
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              deleteHistoryLogs().then();
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            清理历史日志
 | 
			
		||||
          </Form.Button>
 | 
			
		||||
          <Divider />
 | 
			
		||||
          <Header as="h3">
 | 
			
		||||
            数据看板
 | 
			
		||||
          </Header>
 | 
			
		||||
          <Header as='h3'>数据看板</Header>
 | 
			
		||||
          <Form.Checkbox
 | 
			
		||||
            checked={inputs.DataExportEnabled === 'true'}
 | 
			
		||||
            label="启用数据看板(实验性)"
 | 
			
		||||
            name="DataExportEnabled"
 | 
			
		||||
            label='启用数据看板(实验性)'
 | 
			
		||||
            name='DataExportEnabled'
 | 
			
		||||
            onChange={handleInputChange}
 | 
			
		||||
          />
 | 
			
		||||
          <Form.Group>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label="数据看板更新间隔(分钟,设置过短会影响数据库性能)"
 | 
			
		||||
              name="DataExportInterval"
 | 
			
		||||
              label='数据看板更新间隔(分钟,设置过短会影响数据库性能)'
 | 
			
		||||
              name='DataExportInterval'
 | 
			
		||||
              type={'number'}
 | 
			
		||||
              step="1"
 | 
			
		||||
              min="1"
 | 
			
		||||
              step='1'
 | 
			
		||||
              min='1'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              autoComplete="new-password"
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.DataExportInterval}
 | 
			
		||||
              placeholder="数据看板更新间隔(分钟,设置过短会影响数据库性能)"
 | 
			
		||||
              placeholder='数据看板更新间隔(分钟,设置过短会影响数据库性能)'
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Select
 | 
			
		||||
              label="数据看板默认时间粒度(仅修改展示粒度,统计精确到小时)"
 | 
			
		||||
              label='数据看板默认时间粒度(仅修改展示粒度,统计精确到小时)'
 | 
			
		||||
              options={timeOptions}
 | 
			
		||||
              name="DataExportDefaultTime"
 | 
			
		||||
              name='DataExportDefaultTime'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              autoComplete="new-password"
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.DataExportDefaultTime}
 | 
			
		||||
              placeholder="数据看板默认时间粒度"
 | 
			
		||||
              placeholder='数据看板默认时间粒度'
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Divider />
 | 
			
		||||
          <Header as="h3">
 | 
			
		||||
            监控设置
 | 
			
		||||
          </Header>
 | 
			
		||||
          <Header as='h3'>监控设置</Header>
 | 
			
		||||
          <Form.Group widths={3}>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label="最长响应时间"
 | 
			
		||||
              name="ChannelDisableThreshold"
 | 
			
		||||
              label='最长响应时间'
 | 
			
		||||
              name='ChannelDisableThreshold'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              autoComplete="new-password"
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.ChannelDisableThreshold}
 | 
			
		||||
              type="number"
 | 
			
		||||
              min="0"
 | 
			
		||||
              placeholder="单位秒,当运行通道全部测试时,超过此时间将自动禁用通道"
 | 
			
		||||
              type='number'
 | 
			
		||||
              min='0'
 | 
			
		||||
              placeholder='单位秒,当运行通道全部测试时,超过此时间将自动禁用通道'
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label="额度提醒阈值"
 | 
			
		||||
              name="QuotaRemindThreshold"
 | 
			
		||||
              label='额度提醒阈值'
 | 
			
		||||
              name='QuotaRemindThreshold'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              autoComplete="new-password"
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.QuotaRemindThreshold}
 | 
			
		||||
              type="number"
 | 
			
		||||
              min="0"
 | 
			
		||||
              placeholder="低于此额度时将发送邮件提醒用户"
 | 
			
		||||
              type='number'
 | 
			
		||||
              min='0'
 | 
			
		||||
              placeholder='低于此额度时将发送邮件提醒用户'
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Group inline>
 | 
			
		||||
            <Form.Checkbox
 | 
			
		||||
              checked={inputs.AutomaticDisableChannelEnabled === 'true'}
 | 
			
		||||
              label="失败时自动禁用通道"
 | 
			
		||||
              name="AutomaticDisableChannelEnabled"
 | 
			
		||||
              label='失败时自动禁用通道'
 | 
			
		||||
              name='AutomaticDisableChannelEnabled'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Checkbox
 | 
			
		||||
              checked={inputs.AutomaticEnableChannelEnabled === 'true'}
 | 
			
		||||
              label="成功时自动启用通道"
 | 
			
		||||
              name="AutomaticEnableChannelEnabled"
 | 
			
		||||
              label='成功时自动启用通道'
 | 
			
		||||
              name='AutomaticEnableChannelEnabled'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Button onClick={() => {
 | 
			
		||||
            submitConfig('monitor').then();
 | 
			
		||||
          }}>保存监控设置</Form.Button>
 | 
			
		||||
          <Form.Button
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              submitConfig('monitor').then();
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            保存监控设置
 | 
			
		||||
          </Form.Button>
 | 
			
		||||
          <Divider />
 | 
			
		||||
          <Header as="h3">
 | 
			
		||||
            额度设置
 | 
			
		||||
          </Header>
 | 
			
		||||
          <Header as='h3'>额度设置</Header>
 | 
			
		||||
          <Form.Group widths={4}>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label="新用户初始额度"
 | 
			
		||||
              name="QuotaForNewUser"
 | 
			
		||||
              label='新用户初始额度'
 | 
			
		||||
              name='QuotaForNewUser'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              autoComplete="new-password"
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.QuotaForNewUser}
 | 
			
		||||
              type="number"
 | 
			
		||||
              min="0"
 | 
			
		||||
              placeholder="例如:100"
 | 
			
		||||
              type='number'
 | 
			
		||||
              min='0'
 | 
			
		||||
              placeholder='例如:100'
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label="请求预扣费额度"
 | 
			
		||||
              name="PreConsumedQuota"
 | 
			
		||||
              label='请求预扣费额度'
 | 
			
		||||
              name='PreConsumedQuota'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              autoComplete="new-password"
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.PreConsumedQuota}
 | 
			
		||||
              type="number"
 | 
			
		||||
              min="0"
 | 
			
		||||
              placeholder="请求结束后多退少补"
 | 
			
		||||
              type='number'
 | 
			
		||||
              min='0'
 | 
			
		||||
              placeholder='请求结束后多退少补'
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label="邀请新用户奖励额度"
 | 
			
		||||
              name="QuotaForInviter"
 | 
			
		||||
              label='邀请新用户奖励额度'
 | 
			
		||||
              name='QuotaForInviter'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              autoComplete="new-password"
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.QuotaForInviter}
 | 
			
		||||
              type="number"
 | 
			
		||||
              min="0"
 | 
			
		||||
              placeholder="例如:2000"
 | 
			
		||||
              type='number'
 | 
			
		||||
              min='0'
 | 
			
		||||
              placeholder='例如:2000'
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label="新用户使用邀请码奖励额度"
 | 
			
		||||
              name="QuotaForInvitee"
 | 
			
		||||
              label='新用户使用邀请码奖励额度'
 | 
			
		||||
              name='QuotaForInvitee'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              autoComplete="new-password"
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.QuotaForInvitee}
 | 
			
		||||
              type="number"
 | 
			
		||||
              min="0"
 | 
			
		||||
              placeholder="例如:1000"
 | 
			
		||||
              type='number'
 | 
			
		||||
              min='0'
 | 
			
		||||
              placeholder='例如:1000'
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Button onClick={() => {
 | 
			
		||||
            submitConfig('quota').then();
 | 
			
		||||
          }}>保存额度设置</Form.Button>
 | 
			
		||||
          <Form.Button
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              submitConfig('quota').then();
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            保存额度设置
 | 
			
		||||
          </Form.Button>
 | 
			
		||||
          <Divider />
 | 
			
		||||
          <Header as="h3">
 | 
			
		||||
            倍率设置
 | 
			
		||||
          </Header>
 | 
			
		||||
          <Form.Group widths="equal">
 | 
			
		||||
          <Header as='h3'>倍率设置</Header>
 | 
			
		||||
          <Form.Group widths='equal'>
 | 
			
		||||
            <Form.TextArea
 | 
			
		||||
              label="模型固定价格(一次调用消耗多少刀,优先级大于模型倍率)"
 | 
			
		||||
              name="ModelPrice"
 | 
			
		||||
              label='模型固定价格(一次调用消耗多少刀,优先级大于模型倍率)'
 | 
			
		||||
              name='ModelPrice'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
 | 
			
		||||
              autoComplete="new-password"
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.ModelPrice}
 | 
			
		||||
              placeholder='为一个 JSON 文本,键为模型名称,值为一次调用消耗多少刀,比如 "gpt-4-gizmo-*": 0.1,一次消耗0.1刀'
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Group widths="equal">
 | 
			
		||||
          <Form.Group widths='equal'>
 | 
			
		||||
            <Form.TextArea
 | 
			
		||||
              label="模型倍率"
 | 
			
		||||
              name="ModelRatio"
 | 
			
		||||
              label='模型倍率'
 | 
			
		||||
              name='ModelRatio'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
 | 
			
		||||
              autoComplete="new-password"
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.ModelRatio}
 | 
			
		||||
              placeholder="为一个 JSON 文本,键为模型名称,值为倍率"
 | 
			
		||||
              placeholder='为一个 JSON 文本,键为模型名称,值为倍率'
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Group widths="equal">
 | 
			
		||||
          <Form.Group widths='equal'>
 | 
			
		||||
            <Form.TextArea
 | 
			
		||||
              label="分组倍率"
 | 
			
		||||
              name="GroupRatio"
 | 
			
		||||
              label='分组倍率'
 | 
			
		||||
              name='GroupRatio'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
 | 
			
		||||
              autoComplete="new-password"
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.GroupRatio}
 | 
			
		||||
              placeholder="为一个 JSON 文本,键为分组名称,值为倍率"
 | 
			
		||||
              placeholder='为一个 JSON 文本,键为分组名称,值为倍率'
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Button onClick={() => {
 | 
			
		||||
            submitConfig('ratio').then();
 | 
			
		||||
          }}>保存倍率设置</Form.Button>
 | 
			
		||||
          <Form.Button
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              submitConfig('ratio').then();
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            保存倍率设置
 | 
			
		||||
          </Form.Button>
 | 
			
		||||
        </Form>
 | 
			
		||||
      </Grid.Column>
 | 
			
		||||
    </Grid>
 | 
			
		||||
  )
 | 
			
		||||
    ;
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default OperationSetting;
 | 
			
		||||
 
 | 
			
		||||
@@ -10,21 +10,20 @@ const OtherSetting = () => {
 | 
			
		||||
    Logo: '',
 | 
			
		||||
    Footer: '',
 | 
			
		||||
    About: '',
 | 
			
		||||
    HomePageContent: ''
 | 
			
		||||
    HomePageContent: '',
 | 
			
		||||
  });
 | 
			
		||||
  let [loading, setLoading] = useState(false);
 | 
			
		||||
  const [showUpdateModal, setShowUpdateModal] = useState(false);
 | 
			
		||||
  const [updateData, setUpdateData] = useState({
 | 
			
		||||
    tag_name: '',
 | 
			
		||||
    content: ''
 | 
			
		||||
    content: '',
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  const updateOption = async (key, value) => {
 | 
			
		||||
    setLoading(true);
 | 
			
		||||
    const res = await API.put('/api/option/', {
 | 
			
		||||
      key,
 | 
			
		||||
      value
 | 
			
		||||
      value,
 | 
			
		||||
    });
 | 
			
		||||
    const { success, message } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
@@ -41,7 +40,7 @@ const OtherSetting = () => {
 | 
			
		||||
    Logo: false,
 | 
			
		||||
    HomePageContent: false,
 | 
			
		||||
    About: false,
 | 
			
		||||
    Footer: false
 | 
			
		||||
    Footer: false,
 | 
			
		||||
  });
 | 
			
		||||
  const handleInputChange = async (value, e) => {
 | 
			
		||||
    const name = e.target.id;
 | 
			
		||||
@@ -68,14 +67,20 @@ const OtherSetting = () => {
 | 
			
		||||
  //  个性化设置 - SystemName
 | 
			
		||||
  const submitSystemName = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      setLoadingInput((loadingInput) => ({ ...loadingInput, SystemName: true }));
 | 
			
		||||
      setLoadingInput((loadingInput) => ({
 | 
			
		||||
        ...loadingInput,
 | 
			
		||||
        SystemName: true,
 | 
			
		||||
      }));
 | 
			
		||||
      await updateOption('SystemName', inputs.SystemName);
 | 
			
		||||
      showSuccess('系统名称已更新');
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('系统名称更新失败', error);
 | 
			
		||||
      showError('系统名称更新失败');
 | 
			
		||||
    } finally {
 | 
			
		||||
      setLoadingInput((loadingInput) => ({ ...loadingInput, SystemName: false }));
 | 
			
		||||
      setLoadingInput((loadingInput) => ({
 | 
			
		||||
        ...loadingInput,
 | 
			
		||||
        SystemName: false,
 | 
			
		||||
      }));
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@@ -95,14 +100,20 @@ const OtherSetting = () => {
 | 
			
		||||
  // 个性化设置 - 首页内容
 | 
			
		||||
  const submitOption = async (key) => {
 | 
			
		||||
    try {
 | 
			
		||||
      setLoadingInput((loadingInput) => ({ ...loadingInput, HomePageContent: true }));
 | 
			
		||||
      setLoadingInput((loadingInput) => ({
 | 
			
		||||
        ...loadingInput,
 | 
			
		||||
        HomePageContent: true,
 | 
			
		||||
      }));
 | 
			
		||||
      await updateOption(key, inputs[key]);
 | 
			
		||||
      showSuccess('首页内容已更新');
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      console.error('首页内容更新失败', error);
 | 
			
		||||
      showError('首页内容更新失败');
 | 
			
		||||
    } finally {
 | 
			
		||||
      setLoadingInput((loadingInput) => ({ ...loadingInput, HomePageContent: false }));
 | 
			
		||||
      setLoadingInput((loadingInput) => ({
 | 
			
		||||
        ...loadingInput,
 | 
			
		||||
        HomePageContent: false,
 | 
			
		||||
      }));
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
  // 个性化设置 - 关于
 | 
			
		||||
@@ -132,15 +143,13 @@ const OtherSetting = () => {
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  const openGitHubRelease = () => {
 | 
			
		||||
    window.location =
 | 
			
		||||
      'https://github.com/songquanpeng/one-api/releases/latest';
 | 
			
		||||
    window.location = 'https://github.com/songquanpeng/one-api/releases/latest';
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const checkUpdate = async () => {
 | 
			
		||||
    const res = await API.get(
 | 
			
		||||
      'https://api.github.com/repos/songquanpeng/one-api/releases/latest'
 | 
			
		||||
      'https://api.github.com/repos/songquanpeng/one-api/releases/latest',
 | 
			
		||||
    );
 | 
			
		||||
    const { tag_name, body } = res.data;
 | 
			
		||||
    if (tag_name === process.env.REACT_APP_VERSION) {
 | 
			
		||||
@@ -148,7 +157,7 @@ const OtherSetting = () => {
 | 
			
		||||
    } else {
 | 
			
		||||
      setUpdateData({
 | 
			
		||||
        tag_name: tag_name,
 | 
			
		||||
        content: marked.parse(body)
 | 
			
		||||
        content: marked.parse(body),
 | 
			
		||||
      });
 | 
			
		||||
      setShowUpdateModal(true);
 | 
			
		||||
    }
 | 
			
		||||
@@ -175,13 +184,15 @@ const OtherSetting = () => {
 | 
			
		||||
    getOptions();
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Row>
 | 
			
		||||
      <Col span={24}>
 | 
			
		||||
        {/* 通用设置 */}
 | 
			
		||||
        <Form values={inputs} getFormApi={formAPI => formAPISettingGeneral.current = formAPI}
 | 
			
		||||
              style={{ marginBottom: 15 }}>
 | 
			
		||||
        <Form
 | 
			
		||||
          values={inputs}
 | 
			
		||||
          getFormApi={(formAPI) => (formAPISettingGeneral.current = formAPI)}
 | 
			
		||||
          style={{ marginBottom: 15 }}
 | 
			
		||||
        >
 | 
			
		||||
          <Form.Section text={'通用设置'}>
 | 
			
		||||
            <Form.TextArea
 | 
			
		||||
              label={'公告'}
 | 
			
		||||
@@ -191,12 +202,17 @@ const OtherSetting = () => {
 | 
			
		||||
              style={{ fontFamily: 'JetBrains Mono, Consolas' }}
 | 
			
		||||
              autosize={{ minRows: 6, maxRows: 12 }}
 | 
			
		||||
            />
 | 
			
		||||
            <Button onClick={submitNotice} loading={loadingInput['Notice']}>设置公告</Button>
 | 
			
		||||
            <Button onClick={submitNotice} loading={loadingInput['Notice']}>
 | 
			
		||||
              设置公告
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Form.Section>
 | 
			
		||||
        </Form>
 | 
			
		||||
        {/* 个性化设置 */}
 | 
			
		||||
        <Form values={inputs} getFormApi={formAPI => formAPIPersonalization.current = formAPI}
 | 
			
		||||
              style={{ marginBottom: 15 }}>
 | 
			
		||||
        <Form
 | 
			
		||||
          values={inputs}
 | 
			
		||||
          getFormApi={(formAPI) => (formAPIPersonalization.current = formAPI)}
 | 
			
		||||
          style={{ marginBottom: 15 }}
 | 
			
		||||
        >
 | 
			
		||||
          <Form.Section text={'个性化设置'}>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label={'系统名称'}
 | 
			
		||||
@@ -204,48 +220,69 @@ const OtherSetting = () => {
 | 
			
		||||
              field={'SystemName'}
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
            <Button onClick={submitSystemName} loading={loadingInput['SystemName']}>设置系统名称</Button>
 | 
			
		||||
            <Button
 | 
			
		||||
              onClick={submitSystemName}
 | 
			
		||||
              loading={loadingInput['SystemName']}
 | 
			
		||||
            >
 | 
			
		||||
              设置系统名称
 | 
			
		||||
            </Button>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label={'Logo 图片地址'}
 | 
			
		||||
              placeholder={'在此输入 Logo 图片地址'}
 | 
			
		||||
              field={'Logo'}
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
            <Button onClick={submitLogo} loading={loadingInput['Logo']}>设置 Logo</Button>
 | 
			
		||||
            <Button onClick={submitLogo} loading={loadingInput['Logo']}>
 | 
			
		||||
              设置 Logo
 | 
			
		||||
            </Button>
 | 
			
		||||
            <Form.TextArea
 | 
			
		||||
              label={'首页内容'}
 | 
			
		||||
              placeholder={'在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页。'}
 | 
			
		||||
              placeholder={
 | 
			
		||||
                '在此输入首页内容,支持 Markdown & HTML 代码,设置后首页的状态信息将不再显示。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为首页。'
 | 
			
		||||
              }
 | 
			
		||||
              field={'HomePageContent'}
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              style={{ fontFamily: 'JetBrains Mono, Consolas' }}
 | 
			
		||||
              autosize={{ minRows: 6, maxRows: 12 }}
 | 
			
		||||
            />
 | 
			
		||||
            <Button onClick={() => submitOption('HomePageContent')}
 | 
			
		||||
                    loading={loadingInput['HomePageContent']}>设置首页内容</Button>
 | 
			
		||||
            <Button
 | 
			
		||||
              onClick={() => submitOption('HomePageContent')}
 | 
			
		||||
              loading={loadingInput['HomePageContent']}
 | 
			
		||||
            >
 | 
			
		||||
              设置首页内容
 | 
			
		||||
            </Button>
 | 
			
		||||
            <Form.TextArea
 | 
			
		||||
              label={'关于'}
 | 
			
		||||
              placeholder={'在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面。'}
 | 
			
		||||
              placeholder={
 | 
			
		||||
                '在此输入新的关于内容,支持 Markdown & HTML 代码。如果输入的是一个链接,则会使用该链接作为 iframe 的 src 属性,这允许你设置任意网页作为关于页面。'
 | 
			
		||||
              }
 | 
			
		||||
              field={'About'}
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              style={{ fontFamily: 'JetBrains Mono, Consolas' }}
 | 
			
		||||
              autosize={{ minRows: 6, maxRows: 12 }}
 | 
			
		||||
            />
 | 
			
		||||
            <Button onClick={submitAbout} loading={loadingInput['About']}>设置关于</Button>
 | 
			
		||||
            <Button onClick={submitAbout} loading={loadingInput['About']}>
 | 
			
		||||
              设置关于
 | 
			
		||||
            </Button>
 | 
			
		||||
            {/*  */}
 | 
			
		||||
            <Banner
 | 
			
		||||
              fullMode={false}
 | 
			
		||||
              type="info"
 | 
			
		||||
              description="移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目。"
 | 
			
		||||
              type='info'
 | 
			
		||||
              description='移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目。'
 | 
			
		||||
              closeIcon={null}
 | 
			
		||||
              style={{ marginTop: 15 }}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label={'页脚'}
 | 
			
		||||
              placeholder={'在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码'}
 | 
			
		||||
              placeholder={
 | 
			
		||||
                '在此输入新的页脚,留空则使用默认页脚,支持 HTML 代码'
 | 
			
		||||
              }
 | 
			
		||||
              field={'Footer'}
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
            <Button onClick={submitFooter} loading={loadingInput['Footer']}>设置页脚</Button>
 | 
			
		||||
            <Button onClick={submitFooter} loading={loadingInput['Footer']}>
 | 
			
		||||
              设置页脚
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Form.Section>
 | 
			
		||||
        </Form>
 | 
			
		||||
      </Col>
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ import { useSearchParams } from 'react-router-dom';
 | 
			
		||||
const PasswordResetConfirm = () => {
 | 
			
		||||
  const [inputs, setInputs] = useState({
 | 
			
		||||
    email: '',
 | 
			
		||||
    token: ''
 | 
			
		||||
    token: '',
 | 
			
		||||
  });
 | 
			
		||||
  const { email, token } = inputs;
 | 
			
		||||
 | 
			
		||||
@@ -23,7 +23,7 @@ const PasswordResetConfirm = () => {
 | 
			
		||||
    let email = searchParams.get('email');
 | 
			
		||||
    setInputs({
 | 
			
		||||
      token,
 | 
			
		||||
      email
 | 
			
		||||
      email,
 | 
			
		||||
    });
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
@@ -46,7 +46,7 @@ const PasswordResetConfirm = () => {
 | 
			
		||||
    setLoading(true);
 | 
			
		||||
    const res = await API.post(`/api/user/reset`, {
 | 
			
		||||
      email,
 | 
			
		||||
      token
 | 
			
		||||
      token,
 | 
			
		||||
    });
 | 
			
		||||
    const { success, message } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
@@ -61,29 +61,29 @@ const PasswordResetConfirm = () => {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Grid textAlign="center" style={{ marginTop: '48px' }}>
 | 
			
		||||
    <Grid textAlign='center' style={{ marginTop: '48px' }}>
 | 
			
		||||
      <Grid.Column style={{ maxWidth: 450 }}>
 | 
			
		||||
        <Header as="h2" color="" textAlign="center">
 | 
			
		||||
          <Image src="/logo.png" /> 密码重置确认
 | 
			
		||||
        <Header as='h2' color='' textAlign='center'>
 | 
			
		||||
          <Image src='/logo.png' /> 密码重置确认
 | 
			
		||||
        </Header>
 | 
			
		||||
        <Form size="large">
 | 
			
		||||
        <Form size='large'>
 | 
			
		||||
          <Segment>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              fluid
 | 
			
		||||
              icon="mail"
 | 
			
		||||
              iconPosition="left"
 | 
			
		||||
              placeholder="邮箱地址"
 | 
			
		||||
              name="email"
 | 
			
		||||
              icon='mail'
 | 
			
		||||
              iconPosition='left'
 | 
			
		||||
              placeholder='邮箱地址'
 | 
			
		||||
              name='email'
 | 
			
		||||
              value={email}
 | 
			
		||||
              readOnly
 | 
			
		||||
            />
 | 
			
		||||
            {newPassword && (
 | 
			
		||||
              <Form.Input
 | 
			
		||||
                fluid
 | 
			
		||||
                icon="lock"
 | 
			
		||||
                iconPosition="left"
 | 
			
		||||
                placeholder="新密码"
 | 
			
		||||
                name="newPassword"
 | 
			
		||||
                icon='lock'
 | 
			
		||||
                iconPosition='left'
 | 
			
		||||
                placeholder='新密码'
 | 
			
		||||
                name='newPassword'
 | 
			
		||||
                value={newPassword}
 | 
			
		||||
                readOnly
 | 
			
		||||
                onClick={(e) => {
 | 
			
		||||
@@ -94,9 +94,9 @@ const PasswordResetConfirm = () => {
 | 
			
		||||
              />
 | 
			
		||||
            )}
 | 
			
		||||
            <Button
 | 
			
		||||
              color="green"
 | 
			
		||||
              color='green'
 | 
			
		||||
              fluid
 | 
			
		||||
              size="large"
 | 
			
		||||
              size='large'
 | 
			
		||||
              onClick={handleSubmit}
 | 
			
		||||
              loading={loading}
 | 
			
		||||
              disabled={disableButton}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ import Turnstile from 'react-turnstile';
 | 
			
		||||
 | 
			
		||||
const PasswordResetForm = () => {
 | 
			
		||||
  const [inputs, setInputs] = useState({
 | 
			
		||||
    email: ''
 | 
			
		||||
    email: '',
 | 
			
		||||
  });
 | 
			
		||||
  const { email } = inputs;
 | 
			
		||||
 | 
			
		||||
@@ -31,7 +31,7 @@ const PasswordResetForm = () => {
 | 
			
		||||
 | 
			
		||||
  function handleChange(e) {
 | 
			
		||||
    const { name, value } = e.target;
 | 
			
		||||
    setInputs(inputs => ({ ...inputs, [name]: value }));
 | 
			
		||||
    setInputs((inputs) => ({ ...inputs, [name]: value }));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async function handleSubmit(e) {
 | 
			
		||||
@@ -43,7 +43,7 @@ const PasswordResetForm = () => {
 | 
			
		||||
    }
 | 
			
		||||
    setLoading(true);
 | 
			
		||||
    const res = await API.get(
 | 
			
		||||
      `/api/reset_password?email=${email}&turnstile=${turnstileToken}`
 | 
			
		||||
      `/api/reset_password?email=${email}&turnstile=${turnstileToken}`,
 | 
			
		||||
    );
 | 
			
		||||
    const { success, message } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
@@ -56,19 +56,19 @@ const PasswordResetForm = () => {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Grid textAlign="center" style={{ marginTop: '48px' }}>
 | 
			
		||||
    <Grid textAlign='center' style={{ marginTop: '48px' }}>
 | 
			
		||||
      <Grid.Column style={{ maxWidth: 450 }}>
 | 
			
		||||
        <Header as="h2" color="" textAlign="center">
 | 
			
		||||
          <Image src="/logo.png" /> 密码重置
 | 
			
		||||
        <Header as='h2' color='' textAlign='center'>
 | 
			
		||||
          <Image src='/logo.png' /> 密码重置
 | 
			
		||||
        </Header>
 | 
			
		||||
        <Form size="large">
 | 
			
		||||
        <Form size='large'>
 | 
			
		||||
          <Segment>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              fluid
 | 
			
		||||
              icon="mail"
 | 
			
		||||
              iconPosition="left"
 | 
			
		||||
              placeholder="邮箱地址"
 | 
			
		||||
              name="email"
 | 
			
		||||
              icon='mail'
 | 
			
		||||
              iconPosition='left'
 | 
			
		||||
              placeholder='邮箱地址'
 | 
			
		||||
              name='email'
 | 
			
		||||
              value={email}
 | 
			
		||||
              onChange={handleChange}
 | 
			
		||||
            />
 | 
			
		||||
@@ -83,9 +83,9 @@ const PasswordResetForm = () => {
 | 
			
		||||
              <></>
 | 
			
		||||
            )}
 | 
			
		||||
            <Button
 | 
			
		||||
              color="green"
 | 
			
		||||
              color='green'
 | 
			
		||||
              fluid
 | 
			
		||||
              size="large"
 | 
			
		||||
              size='large'
 | 
			
		||||
              onClick={handleSubmit}
 | 
			
		||||
              loading={loading}
 | 
			
		||||
              disabled={disableButton}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,13 @@
 | 
			
		||||
import React, { useContext, useEffect, useState } from 'react';
 | 
			
		||||
import { useNavigate } from 'react-router-dom';
 | 
			
		||||
import { API, copy, isRoot, showError, showInfo, showSuccess } from '../helpers';
 | 
			
		||||
import {
 | 
			
		||||
  API,
 | 
			
		||||
  copy,
 | 
			
		||||
  isRoot,
 | 
			
		||||
  showError,
 | 
			
		||||
  showInfo,
 | 
			
		||||
  showSuccess,
 | 
			
		||||
} from '../helpers';
 | 
			
		||||
import Turnstile from 'react-turnstile';
 | 
			
		||||
import { UserContext } from '../context/User';
 | 
			
		||||
import { onGitHubOAuthClicked, onLinuxDoOAuthClicked } from './utils';
 | 
			
		||||
@@ -17,9 +24,14 @@ import {
 | 
			
		||||
  Modal,
 | 
			
		||||
  Space,
 | 
			
		||||
  Tag,
 | 
			
		||||
  Typography
 | 
			
		||||
  Typography,
 | 
			
		||||
} from '@douyinfe/semi-ui';
 | 
			
		||||
import { getQuotaPerUnit, renderQuota, renderQuotaWithPrompt, stringToColor } from '../helpers/render';
 | 
			
		||||
import {
 | 
			
		||||
  getQuotaPerUnit,
 | 
			
		||||
  renderQuota,
 | 
			
		||||
  renderQuotaWithPrompt,
 | 
			
		||||
  stringToColor,
 | 
			
		||||
} from '../helpers/render';
 | 
			
		||||
import TelegramLoginButton from 'react-telegram-login';
 | 
			
		||||
 | 
			
		||||
const PersonalSetting = () => {
 | 
			
		||||
@@ -32,7 +44,7 @@ const PersonalSetting = () => {
 | 
			
		||||
    email: '',
 | 
			
		||||
    self_account_deletion_confirmation: '',
 | 
			
		||||
    set_new_password: '',
 | 
			
		||||
    set_new_password_confirmation: ''
 | 
			
		||||
    set_new_password_confirmation: '',
 | 
			
		||||
  });
 | 
			
		||||
  const [status, setStatus] = useState({});
 | 
			
		||||
  const [showChangePasswordModal, setShowChangePasswordModal] = useState(false);
 | 
			
		||||
@@ -67,11 +79,9 @@ const PersonalSetting = () => {
 | 
			
		||||
        setTurnstileSiteKey(status.turnstile_site_key);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    getUserData().then(
 | 
			
		||||
      (res) => {
 | 
			
		||||
        console.log(userState);
 | 
			
		||||
      }
 | 
			
		||||
    );
 | 
			
		||||
    getUserData().then((res) => {
 | 
			
		||||
      console.log(userState);
 | 
			
		||||
    });
 | 
			
		||||
    loadModels().then();
 | 
			
		||||
    getAffLink().then();
 | 
			
		||||
    setTransferAmount(getQuotaPerUnit());
 | 
			
		||||
@@ -173,7 +183,7 @@ const PersonalSetting = () => {
 | 
			
		||||
  const bindWeChat = async () => {
 | 
			
		||||
    if (inputs.wechat_verification_code === '') return;
 | 
			
		||||
    const res = await API.get(
 | 
			
		||||
      `/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`
 | 
			
		||||
      `/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`,
 | 
			
		||||
    );
 | 
			
		||||
    const { success, message } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
@@ -189,12 +199,9 @@ const PersonalSetting = () => {
 | 
			
		||||
      showError('两次输入的密码不一致!');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const res = await API.put(
 | 
			
		||||
      `/api/user/self`,
 | 
			
		||||
      {
 | 
			
		||||
        password: inputs.set_new_password
 | 
			
		||||
      }
 | 
			
		||||
    );
 | 
			
		||||
    const res = await API.put(`/api/user/self`, {
 | 
			
		||||
      password: inputs.set_new_password,
 | 
			
		||||
    });
 | 
			
		||||
    const { success, message } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      showSuccess('密码修改成功!');
 | 
			
		||||
@@ -210,12 +217,9 @@ const PersonalSetting = () => {
 | 
			
		||||
      showError('划转金额最低为' + renderQuota(getQuotaPerUnit()));
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const res = await API.post(
 | 
			
		||||
      `/api/user/aff_transfer`,
 | 
			
		||||
      {
 | 
			
		||||
        quota: transferAmount
 | 
			
		||||
      }
 | 
			
		||||
    );
 | 
			
		||||
    const res = await API.post(`/api/user/aff_transfer`, {
 | 
			
		||||
      quota: transferAmount,
 | 
			
		||||
    });
 | 
			
		||||
    const { success, message } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      showSuccess(message);
 | 
			
		||||
@@ -238,7 +242,7 @@ const PersonalSetting = () => {
 | 
			
		||||
    }
 | 
			
		||||
    setLoading(true);
 | 
			
		||||
    const res = await API.get(
 | 
			
		||||
      `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`
 | 
			
		||||
      `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`,
 | 
			
		||||
    );
 | 
			
		||||
    const { success, message } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
@@ -256,7 +260,7 @@ const PersonalSetting = () => {
 | 
			
		||||
    }
 | 
			
		||||
    setLoading(true);
 | 
			
		||||
    const res = await API.get(
 | 
			
		||||
      `/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`
 | 
			
		||||
      `/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`,
 | 
			
		||||
    );
 | 
			
		||||
    const { success, message } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
@@ -295,7 +299,7 @@ const PersonalSetting = () => {
 | 
			
		||||
      <Layout>
 | 
			
		||||
        <Layout.Content>
 | 
			
		||||
          <Modal
 | 
			
		||||
            title="请输入要划转的数量"
 | 
			
		||||
            title='请输入要划转的数量'
 | 
			
		||||
            visible={openTransfer}
 | 
			
		||||
            onOk={transfer}
 | 
			
		||||
            onCancel={handleCancel}
 | 
			
		||||
@@ -305,13 +309,25 @@ const PersonalSetting = () => {
 | 
			
		||||
          >
 | 
			
		||||
            <div style={{ marginTop: 20 }}>
 | 
			
		||||
              <Typography.Text>{`可用额度${renderQuotaWithPrompt(userState?.user?.aff_quota)}`}</Typography.Text>
 | 
			
		||||
              <Input style={{ marginTop: 5 }} value={userState?.user?.aff_quota} disabled={true}></Input>
 | 
			
		||||
              <Input
 | 
			
		||||
                style={{ marginTop: 5 }}
 | 
			
		||||
                value={userState?.user?.aff_quota}
 | 
			
		||||
                disabled={true}
 | 
			
		||||
              ></Input>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div style={{ marginTop: 20 }}>
 | 
			
		||||
              <Typography.Text>{`划转额度${renderQuotaWithPrompt(transferAmount)} 最低` + renderQuota(getQuotaPerUnit())}</Typography.Text>
 | 
			
		||||
              <Typography.Text>
 | 
			
		||||
                {`划转额度${renderQuotaWithPrompt(transferAmount)} 最低` +
 | 
			
		||||
                  renderQuota(getQuotaPerUnit())}
 | 
			
		||||
              </Typography.Text>
 | 
			
		||||
              <div>
 | 
			
		||||
                <InputNumber min={0} style={{ marginTop: 5 }} value={transferAmount}
 | 
			
		||||
                             onChange={(value) => setTransferAmount(value)} disabled={false}></InputNumber>
 | 
			
		||||
                <InputNumber
 | 
			
		||||
                  min={0}
 | 
			
		||||
                  style={{ marginTop: 5 }}
 | 
			
		||||
                  value={transferAmount}
 | 
			
		||||
                  onChange={(value) => setTransferAmount(value)}
 | 
			
		||||
                  disabled={false}
 | 
			
		||||
                ></InputNumber>
 | 
			
		||||
              </div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </Modal>
 | 
			
		||||
@@ -319,27 +335,45 @@ const PersonalSetting = () => {
 | 
			
		||||
            <Card
 | 
			
		||||
              title={
 | 
			
		||||
                <Card.Meta
 | 
			
		||||
                  avatar={<Avatar size="default" color={stringToColor(getUsername())}
 | 
			
		||||
                                  style={{ marginRight: 4 }}>
 | 
			
		||||
                    {typeof getUsername() === 'string' && getUsername().slice(0, 1)}
 | 
			
		||||
                  </Avatar>}
 | 
			
		||||
                  avatar={
 | 
			
		||||
                    <Avatar
 | 
			
		||||
                      size='default'
 | 
			
		||||
                      color={stringToColor(getUsername())}
 | 
			
		||||
                      style={{ marginRight: 4 }}
 | 
			
		||||
                    >
 | 
			
		||||
                      {typeof getUsername() === 'string' &&
 | 
			
		||||
                        getUsername().slice(0, 1)}
 | 
			
		||||
                    </Avatar>
 | 
			
		||||
                  }
 | 
			
		||||
                  title={<Typography.Text>{getUsername()}</Typography.Text>}
 | 
			
		||||
                  description={isRoot() ? <Tag color="red">管理员</Tag> : <Tag color="blue">普通用户</Tag>}
 | 
			
		||||
                  description={
 | 
			
		||||
                    isRoot() ? (
 | 
			
		||||
                      <Tag color='red'>管理员</Tag>
 | 
			
		||||
                    ) : (
 | 
			
		||||
                      <Tag color='blue'>普通用户</Tag>
 | 
			
		||||
                    )
 | 
			
		||||
                  }
 | 
			
		||||
                ></Card.Meta>
 | 
			
		||||
              }
 | 
			
		||||
              headerExtraContent={
 | 
			
		||||
                <>
 | 
			
		||||
                  <Space vertical align="start">
 | 
			
		||||
                    <Tag color="green">{'ID: ' + userState?.user?.id}</Tag>
 | 
			
		||||
                    <Tag color="blue">{userState?.user?.group}</Tag>
 | 
			
		||||
                  <Space vertical align='start'>
 | 
			
		||||
                    <Tag color='green'>{'ID: ' + userState?.user?.id}</Tag>
 | 
			
		||||
                    <Tag color='blue'>{userState?.user?.group}</Tag>
 | 
			
		||||
                  </Space>
 | 
			
		||||
                </>
 | 
			
		||||
              }
 | 
			
		||||
              footer={
 | 
			
		||||
                <Descriptions row>
 | 
			
		||||
                  <Descriptions.Item itemKey="当前余额">{renderQuota(userState?.user?.quota)}</Descriptions.Item>
 | 
			
		||||
                  <Descriptions.Item itemKey="历史消耗">{renderQuota(userState?.user?.used_quota)}</Descriptions.Item>
 | 
			
		||||
                  <Descriptions.Item itemKey="请求次数">{userState.user?.request_count}</Descriptions.Item>
 | 
			
		||||
                  <Descriptions.Item itemKey='当前余额'>
 | 
			
		||||
                    {renderQuota(userState?.user?.quota)}
 | 
			
		||||
                  </Descriptions.Item>
 | 
			
		||||
                  <Descriptions.Item itemKey='历史消耗'>
 | 
			
		||||
                    {renderQuota(userState?.user?.used_quota)}
 | 
			
		||||
                  </Descriptions.Item>
 | 
			
		||||
                  <Descriptions.Item itemKey='请求次数'>
 | 
			
		||||
                    {userState.user?.request_count}
 | 
			
		||||
                  </Descriptions.Item>
 | 
			
		||||
                </Descriptions>
 | 
			
		||||
              }
 | 
			
		||||
            >
 | 
			
		||||
@@ -347,15 +381,18 @@ const PersonalSetting = () => {
 | 
			
		||||
              <div style={{ marginTop: 10 }}>
 | 
			
		||||
                <Space wrap>
 | 
			
		||||
                  {models.map((model) => (
 | 
			
		||||
                    <Tag key={model} color="cyan" onClick={() => {
 | 
			
		||||
                      copyText(model);
 | 
			
		||||
                    }}>
 | 
			
		||||
                    <Tag
 | 
			
		||||
                      key={model}
 | 
			
		||||
                      color='cyan'
 | 
			
		||||
                      onClick={() => {
 | 
			
		||||
                        copyText(model);
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                      {model}
 | 
			
		||||
                    </Tag>
 | 
			
		||||
                  ))}
 | 
			
		||||
                </Space>
 | 
			
		||||
              </div>
 | 
			
		||||
 | 
			
		||||
            </Card>
 | 
			
		||||
            <Card
 | 
			
		||||
              footer={
 | 
			
		||||
@@ -373,210 +410,286 @@ const PersonalSetting = () => {
 | 
			
		||||
              <Typography.Title heading={6}>邀请信息</Typography.Title>
 | 
			
		||||
              <div style={{ marginTop: 10 }}>
 | 
			
		||||
                <Descriptions row>
 | 
			
		||||
                  <Descriptions.Item itemKey="待使用收益">
 | 
			
		||||
                                        <span style={{ color: 'rgba(var(--semi-red-5), 1)' }}>
 | 
			
		||||
                                            {
 | 
			
		||||
                                              renderQuota(userState?.user?.aff_quota)
 | 
			
		||||
                                            }
 | 
			
		||||
                                        </span>
 | 
			
		||||
                    <Button type={'secondary'} onClick={() => setOpenTransfer(true)} size={'small'}
 | 
			
		||||
                            style={{ marginLeft: 10 }}>划转</Button>
 | 
			
		||||
                  <Descriptions.Item itemKey='待使用收益'>
 | 
			
		||||
                    <span style={{ color: 'rgba(var(--semi-red-5), 1)' }}>
 | 
			
		||||
                      {renderQuota(userState?.user?.aff_quota)}
 | 
			
		||||
                    </span>
 | 
			
		||||
                    <Button
 | 
			
		||||
                      type={'secondary'}
 | 
			
		||||
                      onClick={() => setOpenTransfer(true)}
 | 
			
		||||
                      size={'small'}
 | 
			
		||||
                      style={{ marginLeft: 10 }}
 | 
			
		||||
                    >
 | 
			
		||||
                      划转
 | 
			
		||||
                    </Button>
 | 
			
		||||
                  </Descriptions.Item>
 | 
			
		||||
                  <Descriptions.Item itemKey='总收益'>
 | 
			
		||||
                    {renderQuota(userState?.user?.aff_history_quota)}
 | 
			
		||||
                  </Descriptions.Item>
 | 
			
		||||
                  <Descriptions.Item itemKey='邀请人数'>
 | 
			
		||||
                    {userState?.user?.aff_count}
 | 
			
		||||
                  </Descriptions.Item>
 | 
			
		||||
                  <Descriptions.Item
 | 
			
		||||
                    itemKey="总收益">{renderQuota(userState?.user?.aff_history_quota)}</Descriptions.Item>
 | 
			
		||||
                  <Descriptions.Item itemKey="邀请人数">{userState?.user?.aff_count}</Descriptions.Item>
 | 
			
		||||
                </Descriptions>
 | 
			
		||||
              </div>
 | 
			
		||||
            </Card>
 | 
			
		||||
            <Card>
 | 
			
		||||
              <Typography.Title heading={6}>个人信息</Typography.Title>
 | 
			
		||||
              <div style={{marginTop: 20}}>
 | 
			
		||||
              <div style={{ marginTop: 20 }}>
 | 
			
		||||
                <Typography.Text strong>邮箱</Typography.Text>
 | 
			
		||||
                <div style={{display: 'flex', justifyContent: 'space-between'}}>
 | 
			
		||||
                <div
 | 
			
		||||
                  style={{ display: 'flex', justifyContent: 'space-between' }}
 | 
			
		||||
                >
 | 
			
		||||
                  <div>
 | 
			
		||||
                    <Input
 | 
			
		||||
                        value={userState.user && userState.user.email !== '' ? userState.user.email : '未绑定'}
 | 
			
		||||
                        readonly={true}
 | 
			
		||||
                      value={
 | 
			
		||||
                        userState.user && userState.user.email !== ''
 | 
			
		||||
                          ? userState.user.email
 | 
			
		||||
                          : '未绑定'
 | 
			
		||||
                      }
 | 
			
		||||
                      readonly={true}
 | 
			
		||||
                    ></Input>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div>
 | 
			
		||||
                    <Button onClick={() => {
 | 
			
		||||
                      setShowEmailBindModal(true);
 | 
			
		||||
                    }}>{
 | 
			
		||||
                      userState.user && userState.user.email !== '' ? '修改绑定' : '绑定邮箱'
 | 
			
		||||
                    }</Button>
 | 
			
		||||
                    <Button
 | 
			
		||||
                      onClick={() => {
 | 
			
		||||
                        setShowEmailBindModal(true);
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                      {userState.user && userState.user.email !== ''
 | 
			
		||||
                        ? '修改绑定'
 | 
			
		||||
                        : '绑定邮箱'}
 | 
			
		||||
                    </Button>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div style={{marginTop: 10}}>
 | 
			
		||||
              <div style={{ marginTop: 10 }}>
 | 
			
		||||
                <Typography.Text strong>微信</Typography.Text>
 | 
			
		||||
                <div style={{display: 'flex', justifyContent: 'space-between'}}>
 | 
			
		||||
                <div
 | 
			
		||||
                  style={{ display: 'flex', justifyContent: 'space-between' }}
 | 
			
		||||
                >
 | 
			
		||||
                  <div>
 | 
			
		||||
                    <Input
 | 
			
		||||
                        value={userState.user && userState.user.wechat_id !== '' ? '已绑定' : '未绑定'}
 | 
			
		||||
                        readonly={true}
 | 
			
		||||
                      value={
 | 
			
		||||
                        userState.user && userState.user.wechat_id !== ''
 | 
			
		||||
                          ? '已绑定'
 | 
			
		||||
                          : '未绑定'
 | 
			
		||||
                      }
 | 
			
		||||
                      readonly={true}
 | 
			
		||||
                    ></Input>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div>
 | 
			
		||||
                    <Button disabled={(userState.user && userState.user.wechat_id !== '') || !status.wechat_login}>
 | 
			
		||||
                      {
 | 
			
		||||
                        status.wechat_login ? '绑定' : '未启用'
 | 
			
		||||
                    <Button
 | 
			
		||||
                      disabled={
 | 
			
		||||
                        (userState.user && userState.user.wechat_id !== '') ||
 | 
			
		||||
                        !status.wechat_login
 | 
			
		||||
                      }
 | 
			
		||||
                    >
 | 
			
		||||
                      {status.wechat_login ? '绑定' : '未启用'}
 | 
			
		||||
                    </Button>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div style={{marginTop: 10}}>
 | 
			
		||||
              <div style={{ marginTop: 10 }}>
 | 
			
		||||
                <Typography.Text strong>GitHub</Typography.Text>
 | 
			
		||||
                <div style={{display: 'flex', justifyContent: 'space-between'}}>
 | 
			
		||||
                <div
 | 
			
		||||
                  style={{ display: 'flex', justifyContent: 'space-between' }}
 | 
			
		||||
                >
 | 
			
		||||
                  <div>
 | 
			
		||||
                    <Input
 | 
			
		||||
                        value={userState.user && userState.user.github_id !== '' ? userState.user.github_id : '未绑定'}
 | 
			
		||||
                        readonly={true}
 | 
			
		||||
                      value={
 | 
			
		||||
                        userState.user && userState.user.github_id !== ''
 | 
			
		||||
                          ? userState.user.github_id
 | 
			
		||||
                          : '未绑定'
 | 
			
		||||
                      }
 | 
			
		||||
                      readonly={true}
 | 
			
		||||
                    ></Input>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div>
 | 
			
		||||
                    <Button
 | 
			
		||||
                        onClick={() => {
 | 
			
		||||
                          onGitHubOAuthClicked(status.github_client_id);
 | 
			
		||||
                        }}
 | 
			
		||||
                        disabled={(userState.user && userState.user.github_id !== '') || !status.github_oauth}
 | 
			
		||||
                    >
 | 
			
		||||
                      {
 | 
			
		||||
                        status.github_oauth ? '绑定' : '未启用'
 | 
			
		||||
                      onClick={() => {
 | 
			
		||||
                        onGitHubOAuthClicked(status.github_client_id);
 | 
			
		||||
                      }}
 | 
			
		||||
                      disabled={
 | 
			
		||||
                        (userState.user && userState.user.github_id !== '') ||
 | 
			
		||||
                        !status.github_oauth
 | 
			
		||||
                      }
 | 
			
		||||
                    >
 | 
			
		||||
                      {status.github_oauth ? '绑定' : '未启用'}
 | 
			
		||||
                    </Button>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div style={{marginTop: 10}}>
 | 
			
		||||
              <div style={{ marginTop: 10 }}>
 | 
			
		||||
                <Typography.Text strong>LINUX DO</Typography.Text>
 | 
			
		||||
                <div style={{display: 'flex', justifyContent: 'space-between'}}>
 | 
			
		||||
                <div
 | 
			
		||||
                  style={{ display: 'flex', justifyContent: 'space-between' }}
 | 
			
		||||
                >
 | 
			
		||||
                  <div>
 | 
			
		||||
                    <Input
 | 
			
		||||
                        value={userState.user && userState.user.linuxdo_id !== '' ? userState.user.linuxdo_id + '(' + userState.user.linuxdo_level + '级)' : '未绑定'}
 | 
			
		||||
                        readonly={true}
 | 
			
		||||
                      value={
 | 
			
		||||
                        userState.user && userState.user.linuxdo_id !== ''
 | 
			
		||||
                          ? userState.user.linuxdo_id +
 | 
			
		||||
                            '(' +
 | 
			
		||||
                            userState.user.linuxdo_level +
 | 
			
		||||
                            '级)'
 | 
			
		||||
                          : '未绑定'
 | 
			
		||||
                      }
 | 
			
		||||
                      readonly={true}
 | 
			
		||||
                    ></Input>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div>
 | 
			
		||||
                    <Button
 | 
			
		||||
                        onClick={() => {
 | 
			
		||||
                          onLinuxDoOAuthClicked(status.linuxdo_client_id);
 | 
			
		||||
                        }}
 | 
			
		||||
                        disabled={(userState.user && userState.user.linuxdo_id !== '') || !status.linuxdo_oauth}
 | 
			
		||||
                    >
 | 
			
		||||
                      {
 | 
			
		||||
                        status.linuxdo_oauth ? '绑定' : '未启用'
 | 
			
		||||
                      onClick={() => {
 | 
			
		||||
                        onLinuxDoOAuthClicked(status.linuxdo_client_id);
 | 
			
		||||
                      }}
 | 
			
		||||
                      disabled={
 | 
			
		||||
                        (userState.user && userState.user.linuxdo_id !== '') ||
 | 
			
		||||
                        !status.linuxdo_oauth
 | 
			
		||||
                      }
 | 
			
		||||
                    >
 | 
			
		||||
                      {status.linuxdo_oauth ? '绑定' : '未启用'}
 | 
			
		||||
                    </Button>
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
 | 
			
		||||
              <div style={{marginTop: 10}}>
 | 
			
		||||
              <div style={{ marginTop: 10 }}>
 | 
			
		||||
                <Typography.Text strong>Telegram</Typography.Text>
 | 
			
		||||
                <div style={{display: 'flex', justifyContent: 'space-between'}}>
 | 
			
		||||
                <div
 | 
			
		||||
                  style={{ display: 'flex', justifyContent: 'space-between' }}
 | 
			
		||||
                >
 | 
			
		||||
                  <div>
 | 
			
		||||
                    <Input
 | 
			
		||||
                        value={userState.user && userState.user.telegram_id !== '' ? userState.user.telegram_id : '未绑定'}
 | 
			
		||||
                        readonly={true}
 | 
			
		||||
                      value={
 | 
			
		||||
                        userState.user && userState.user.telegram_id !== ''
 | 
			
		||||
                          ? userState.user.telegram_id
 | 
			
		||||
                          : '未绑定'
 | 
			
		||||
                      }
 | 
			
		||||
                      readonly={true}
 | 
			
		||||
                    ></Input>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <div>
 | 
			
		||||
                    {status.telegram_oauth ?
 | 
			
		||||
                        userState.user.telegram_id !== '' ? <Button disabled={true}>已绑定</Button>
 | 
			
		||||
                            : <TelegramLoginButton dataAuthUrl="/api/oauth/telegram/bind"
 | 
			
		||||
                                                   botName={status.telegram_bot_name}/>
 | 
			
		||||
                        : <Button disabled={true}>未启用</Button>
 | 
			
		||||
                    }
 | 
			
		||||
                    {status.telegram_oauth ? (
 | 
			
		||||
                      userState.user.telegram_id !== '' ? (
 | 
			
		||||
                        <Button disabled={true}>已绑定</Button>
 | 
			
		||||
                      ) : (
 | 
			
		||||
                        <TelegramLoginButton
 | 
			
		||||
                          dataAuthUrl='/api/oauth/telegram/bind'
 | 
			
		||||
                          botName={status.telegram_bot_name}
 | 
			
		||||
                        />
 | 
			
		||||
                      )
 | 
			
		||||
                    ) : (
 | 
			
		||||
                      <Button disabled={true}>未启用</Button>
 | 
			
		||||
                    )}
 | 
			
		||||
                  </div>
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
 | 
			
		||||
              <div style={{marginTop: 10}}>
 | 
			
		||||
              <div style={{ marginTop: 10 }}>
 | 
			
		||||
                <Space>
 | 
			
		||||
                  <Button onClick={generateAccessToken}>生成系统访问令牌</Button>
 | 
			
		||||
                  <Button onClick={() => {
 | 
			
		||||
                    setShowChangePasswordModal(true);
 | 
			
		||||
                  }}>修改密码</Button>
 | 
			
		||||
                  <Button type={'danger'} onClick={() => {
 | 
			
		||||
                    setShowAccountDeleteModal(true);
 | 
			
		||||
                  }}>删除个人账户</Button>
 | 
			
		||||
                  <Button onClick={generateAccessToken}>
 | 
			
		||||
                    生成系统访问令牌
 | 
			
		||||
                  </Button>
 | 
			
		||||
                  <Button
 | 
			
		||||
                    onClick={() => {
 | 
			
		||||
                      setShowChangePasswordModal(true);
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    修改密码
 | 
			
		||||
                  </Button>
 | 
			
		||||
                  <Button
 | 
			
		||||
                    type={'danger'}
 | 
			
		||||
                    onClick={() => {
 | 
			
		||||
                      setShowAccountDeleteModal(true);
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    删除个人账户
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </Space>
 | 
			
		||||
 | 
			
		||||
                {systemToken && (
 | 
			
		||||
                    <Input
 | 
			
		||||
                        readOnly
 | 
			
		||||
                        value={systemToken}
 | 
			
		||||
                        onClick={handleSystemTokenClick}
 | 
			
		||||
                        style={{marginTop: '10px'}}
 | 
			
		||||
                    />
 | 
			
		||||
                  <Input
 | 
			
		||||
                    readOnly
 | 
			
		||||
                    value={systemToken}
 | 
			
		||||
                    onClick={handleSystemTokenClick}
 | 
			
		||||
                    style={{ marginTop: '10px' }}
 | 
			
		||||
                  />
 | 
			
		||||
                )}
 | 
			
		||||
                {status.wechat_login && (
 | 
			
		||||
                  <Button
 | 
			
		||||
                    onClick={() => {
 | 
			
		||||
                      setShowWeChatBindModal(true);
 | 
			
		||||
                    }}
 | 
			
		||||
                  >
 | 
			
		||||
                    绑定微信账号
 | 
			
		||||
                  </Button>
 | 
			
		||||
                )}
 | 
			
		||||
                {
 | 
			
		||||
                    status.wechat_login && (
 | 
			
		||||
                        <Button
 | 
			
		||||
                            onClick={() => {
 | 
			
		||||
                              setShowWeChatBindModal(true);
 | 
			
		||||
                            }}
 | 
			
		||||
                        >
 | 
			
		||||
                          绑定微信账号
 | 
			
		||||
                        </Button>
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
                <Modal
 | 
			
		||||
                    onCancel={() => setShowWeChatBindModal(false)}
 | 
			
		||||
                    // onOpen={() => setShowWeChatBindModal(true)}
 | 
			
		||||
                    visible={showWeChatBindModal}
 | 
			
		||||
                    size={'mini'}
 | 
			
		||||
                  onCancel={() => setShowWeChatBindModal(false)}
 | 
			
		||||
                  // onOpen={() => setShowWeChatBindModal(true)}
 | 
			
		||||
                  visible={showWeChatBindModal}
 | 
			
		||||
                  size={'mini'}
 | 
			
		||||
                >
 | 
			
		||||
                  <Image src={status.wechat_qrcode}/>
 | 
			
		||||
                  <div style={{textAlign: 'center'}}>
 | 
			
		||||
                  <Image src={status.wechat_qrcode} />
 | 
			
		||||
                  <div style={{ textAlign: 'center' }}>
 | 
			
		||||
                    <p>
 | 
			
		||||
                      微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)
 | 
			
		||||
                    </p>
 | 
			
		||||
                  </div>
 | 
			
		||||
                  <Input
 | 
			
		||||
                      placeholder="验证码"
 | 
			
		||||
                      name="wechat_verification_code"
 | 
			
		||||
                      value={inputs.wechat_verification_code}
 | 
			
		||||
                      onChange={(v) => handleInputChange('wechat_verification_code', v)}
 | 
			
		||||
                    placeholder='验证码'
 | 
			
		||||
                    name='wechat_verification_code'
 | 
			
		||||
                    value={inputs.wechat_verification_code}
 | 
			
		||||
                    onChange={(v) =>
 | 
			
		||||
                      handleInputChange('wechat_verification_code', v)
 | 
			
		||||
                    }
 | 
			
		||||
                  />
 | 
			
		||||
                  <Button color="" fluid size="large" onClick={bindWeChat}>
 | 
			
		||||
                  <Button color='' fluid size='large' onClick={bindWeChat}>
 | 
			
		||||
                    绑定
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </Modal>
 | 
			
		||||
              </div>
 | 
			
		||||
            </Card>
 | 
			
		||||
            <Modal
 | 
			
		||||
                onCancel={() => setShowEmailBindModal(false)}
 | 
			
		||||
                // onOpen={() => setShowEmailBindModal(true)}
 | 
			
		||||
                onOk={bindEmail}
 | 
			
		||||
                visible={showEmailBindModal}
 | 
			
		||||
                size={'small'}
 | 
			
		||||
                centered={true}
 | 
			
		||||
                maskClosable={false}
 | 
			
		||||
              onCancel={() => setShowEmailBindModal(false)}
 | 
			
		||||
              // onOpen={() => setShowEmailBindModal(true)}
 | 
			
		||||
              onOk={bindEmail}
 | 
			
		||||
              visible={showEmailBindModal}
 | 
			
		||||
              size={'small'}
 | 
			
		||||
              centered={true}
 | 
			
		||||
              maskClosable={false}
 | 
			
		||||
            >
 | 
			
		||||
              <Typography.Title heading={6}>绑定邮箱地址</Typography.Title>
 | 
			
		||||
              <div style={{marginTop: 20, display: 'flex', justifyContent: 'space-between'}}>
 | 
			
		||||
              <div
 | 
			
		||||
                style={{
 | 
			
		||||
                  marginTop: 20,
 | 
			
		||||
                  display: 'flex',
 | 
			
		||||
                  justifyContent: 'space-between',
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <Input
 | 
			
		||||
                    fluid
 | 
			
		||||
                    placeholder="输入邮箱地址"
 | 
			
		||||
                    onChange={(value) => handleInputChange('email', value)}
 | 
			
		||||
                    name="email"
 | 
			
		||||
                    type="email"
 | 
			
		||||
                  fluid
 | 
			
		||||
                  placeholder='输入邮箱地址'
 | 
			
		||||
                  onChange={(value) => handleInputChange('email', value)}
 | 
			
		||||
                  name='email'
 | 
			
		||||
                  type='email'
 | 
			
		||||
                />
 | 
			
		||||
                <Button onClick={sendVerificationCode}
 | 
			
		||||
                        disabled={disableButton || loading}>
 | 
			
		||||
                <Button
 | 
			
		||||
                  onClick={sendVerificationCode}
 | 
			
		||||
                  disabled={disableButton || loading}
 | 
			
		||||
                >
 | 
			
		||||
                  {disableButton ? `重新发送(${countdown})` : '获取验证码'}
 | 
			
		||||
                </Button>
 | 
			
		||||
              </div>
 | 
			
		||||
              <div style={{marginTop: 10}}>
 | 
			
		||||
              <div style={{ marginTop: 10 }}>
 | 
			
		||||
                <Input
 | 
			
		||||
                    fluid
 | 
			
		||||
                    placeholder="验证码"
 | 
			
		||||
                    name="email_verification_code"
 | 
			
		||||
                    value={inputs.email_verification_code}
 | 
			
		||||
                    onChange={(value) => handleInputChange('email_verification_code', value)}
 | 
			
		||||
                  fluid
 | 
			
		||||
                  placeholder='验证码'
 | 
			
		||||
                  name='email_verification_code'
 | 
			
		||||
                  value={inputs.email_verification_code}
 | 
			
		||||
                  onChange={(value) =>
 | 
			
		||||
                    handleInputChange('email_verification_code', value)
 | 
			
		||||
                  }
 | 
			
		||||
                />
 | 
			
		||||
              </div>
 | 
			
		||||
              {turnstileEnabled ? (
 | 
			
		||||
@@ -599,17 +712,22 @@ const PersonalSetting = () => {
 | 
			
		||||
            >
 | 
			
		||||
              <div style={{ marginTop: 20 }}>
 | 
			
		||||
                <Banner
 | 
			
		||||
                  type="danger"
 | 
			
		||||
                  description="您正在删除自己的帐户,将清空所有数据且不可恢复"
 | 
			
		||||
                  type='danger'
 | 
			
		||||
                  description='您正在删除自己的帐户,将清空所有数据且不可恢复'
 | 
			
		||||
                  closeIcon={null}
 | 
			
		||||
                />
 | 
			
		||||
              </div>
 | 
			
		||||
              <div style={{ marginTop: 20 }}>
 | 
			
		||||
                <Input
 | 
			
		||||
                  placeholder={`输入你的账户名 ${userState?.user?.username} 以确认删除`}
 | 
			
		||||
                  name="self_account_deletion_confirmation"
 | 
			
		||||
                  name='self_account_deletion_confirmation'
 | 
			
		||||
                  value={inputs.self_account_deletion_confirmation}
 | 
			
		||||
                  onChange={(value) => handleInputChange('self_account_deletion_confirmation', value)}
 | 
			
		||||
                  onChange={(value) =>
 | 
			
		||||
                    handleInputChange(
 | 
			
		||||
                      'self_account_deletion_confirmation',
 | 
			
		||||
                      value,
 | 
			
		||||
                    )
 | 
			
		||||
                  }
 | 
			
		||||
                />
 | 
			
		||||
                {turnstileEnabled ? (
 | 
			
		||||
                  <Turnstile
 | 
			
		||||
@@ -632,17 +750,21 @@ const PersonalSetting = () => {
 | 
			
		||||
            >
 | 
			
		||||
              <div style={{ marginTop: 20 }}>
 | 
			
		||||
                <Input
 | 
			
		||||
                  name="set_new_password"
 | 
			
		||||
                  placeholder="新密码"
 | 
			
		||||
                  name='set_new_password'
 | 
			
		||||
                  placeholder='新密码'
 | 
			
		||||
                  value={inputs.set_new_password}
 | 
			
		||||
                  onChange={(value) => handleInputChange('set_new_password', value)}
 | 
			
		||||
                  onChange={(value) =>
 | 
			
		||||
                    handleInputChange('set_new_password', value)
 | 
			
		||||
                  }
 | 
			
		||||
                />
 | 
			
		||||
                <Input
 | 
			
		||||
                  style={{ marginTop: 20 }}
 | 
			
		||||
                  name="set_new_password_confirmation"
 | 
			
		||||
                  placeholder="确认新密码"
 | 
			
		||||
                  name='set_new_password_confirmation'
 | 
			
		||||
                  placeholder='确认新密码'
 | 
			
		||||
                  value={inputs.set_new_password_confirmation}
 | 
			
		||||
                  onChange={(value) => handleInputChange('set_new_password_confirmation', value)}
 | 
			
		||||
                  onChange={(value) =>
 | 
			
		||||
                    handleInputChange('set_new_password_confirmation', value)
 | 
			
		||||
                  }
 | 
			
		||||
                />
 | 
			
		||||
                {turnstileEnabled ? (
 | 
			
		||||
                  <Turnstile
 | 
			
		||||
@@ -657,7 +779,6 @@ const PersonalSetting = () => {
 | 
			
		||||
              </div>
 | 
			
		||||
            </Modal>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
        </Layout.Content>
 | 
			
		||||
      </Layout>
 | 
			
		||||
    </div>
 | 
			
		||||
 
 | 
			
		||||
@@ -2,12 +2,11 @@ import { Navigate } from 'react-router-dom';
 | 
			
		||||
 | 
			
		||||
import { history } from '../helpers';
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
function PrivateRoute({ children }) {
 | 
			
		||||
  if (!localStorage.getItem('user')) {
 | 
			
		||||
    return <Navigate to="/login" state={{ from: history.location }} />;
 | 
			
		||||
    return <Navigate to='/login' state={{ from: history.location }} />;
 | 
			
		||||
  }
 | 
			
		||||
  return children;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { PrivateRoute };
 | 
			
		||||
export { PrivateRoute };
 | 
			
		||||
 
 | 
			
		||||
@@ -1,29 +1,58 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { API, copy, showError, showSuccess, timestamp2string } from '../helpers';
 | 
			
		||||
import {
 | 
			
		||||
  API,
 | 
			
		||||
  copy,
 | 
			
		||||
  showError,
 | 
			
		||||
  showSuccess,
 | 
			
		||||
  timestamp2string,
 | 
			
		||||
} from '../helpers';
 | 
			
		||||
 | 
			
		||||
import { ITEMS_PER_PAGE } from '../constants';
 | 
			
		||||
import { renderQuota } from '../helpers/render';
 | 
			
		||||
import { Button, Form, Modal, Popconfirm, Popover, Table, Tag } from '@douyinfe/semi-ui';
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Form,
 | 
			
		||||
  Modal,
 | 
			
		||||
  Popconfirm,
 | 
			
		||||
  Popover,
 | 
			
		||||
  Table,
 | 
			
		||||
  Tag,
 | 
			
		||||
} from '@douyinfe/semi-ui';
 | 
			
		||||
import EditRedemption from '../pages/Redemption/EditRedemption';
 | 
			
		||||
 | 
			
		||||
function renderTimestamp(timestamp) {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {timestamp2string(timestamp)}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
  return <>{timestamp2string(timestamp)}</>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function renderStatus(status) {
 | 
			
		||||
  switch (status) {
 | 
			
		||||
    case 1:
 | 
			
		||||
      return <Tag color="green" size="large">未使用</Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='green' size='large'>
 | 
			
		||||
          未使用
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    case 2:
 | 
			
		||||
      return <Tag color="red" size="large"> 已禁用 </Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='red' size='large'>
 | 
			
		||||
          {' '}
 | 
			
		||||
          已禁用{' '}
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    case 3:
 | 
			
		||||
      return <Tag color="grey" size="large"> 已使用 </Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='grey' size='large'>
 | 
			
		||||
          {' '}
 | 
			
		||||
          已使用{' '}
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    default:
 | 
			
		||||
      return <Tag color="black" size="large"> 未知状态 </Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='black' size='large'>
 | 
			
		||||
          {' '}
 | 
			
		||||
          未知状态{' '}
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -31,121 +60,115 @@ const RedemptionsTable = () => {
 | 
			
		||||
  const columns = [
 | 
			
		||||
    {
 | 
			
		||||
      title: 'ID',
 | 
			
		||||
      dataIndex: 'id'
 | 
			
		||||
      dataIndex: 'id',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '名称',
 | 
			
		||||
      dataIndex: 'name'
 | 
			
		||||
      dataIndex: 'name',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '状态',
 | 
			
		||||
      dataIndex: 'status',
 | 
			
		||||
      key: 'status',
 | 
			
		||||
      render: (text, record, index) => {
 | 
			
		||||
        return (
 | 
			
		||||
          <div>
 | 
			
		||||
            {renderStatus(text)}
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
        return <div>{renderStatus(text)}</div>;
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '额度',
 | 
			
		||||
      dataIndex: 'quota',
 | 
			
		||||
      render: (text, record, index) => {
 | 
			
		||||
        return (
 | 
			
		||||
          <div>
 | 
			
		||||
            {renderQuota(parseInt(text))}
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
        return <div>{renderQuota(parseInt(text))}</div>;
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '创建时间',
 | 
			
		||||
      dataIndex: 'created_time',
 | 
			
		||||
      render: (text, record, index) => {
 | 
			
		||||
        return (
 | 
			
		||||
          <div>
 | 
			
		||||
            {renderTimestamp(text)}
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
        return <div>{renderTimestamp(text)}</div>;
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '兑换人ID',
 | 
			
		||||
      dataIndex: 'used_user_id',
 | 
			
		||||
      render: (text, record, index) => {
 | 
			
		||||
        return (
 | 
			
		||||
          <div>
 | 
			
		||||
            {text === 0 ? '无' : text}
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
        return <div>{text === 0 ? '无' : text}</div>;
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '',
 | 
			
		||||
      dataIndex: 'operate',
 | 
			
		||||
      render: (text, record, index) => (
 | 
			
		||||
        <div>
 | 
			
		||||
          <Popover
 | 
			
		||||
            content={
 | 
			
		||||
              record.key
 | 
			
		||||
            }
 | 
			
		||||
            style={{ padding: 20 }}
 | 
			
		||||
            position="top"
 | 
			
		||||
          >
 | 
			
		||||
            <Button theme="light" type="tertiary" style={{ marginRight: 1 }}>查看</Button>
 | 
			
		||||
          <Popover content={record.key} style={{ padding: 20 }} position='top'>
 | 
			
		||||
            <Button theme='light' type='tertiary' style={{ marginRight: 1 }}>
 | 
			
		||||
              查看
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Popover>
 | 
			
		||||
          <Button theme="light" type="secondary" style={{ marginRight: 1 }}
 | 
			
		||||
                  onClick={async (text) => {
 | 
			
		||||
                    await copyText(record.key);
 | 
			
		||||
                  }}
 | 
			
		||||
          >复制</Button>
 | 
			
		||||
          <Button
 | 
			
		||||
            theme='light'
 | 
			
		||||
            type='secondary'
 | 
			
		||||
            style={{ marginRight: 1 }}
 | 
			
		||||
            onClick={async (text) => {
 | 
			
		||||
              await copyText(record.key);
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            复制
 | 
			
		||||
          </Button>
 | 
			
		||||
          <Popconfirm
 | 
			
		||||
            title="确定是否要删除此兑换码?"
 | 
			
		||||
            content="此修改将不可逆"
 | 
			
		||||
            title='确定是否要删除此兑换码?'
 | 
			
		||||
            content='此修改将不可逆'
 | 
			
		||||
            okType={'danger'}
 | 
			
		||||
            position={'left'}
 | 
			
		||||
            onConfirm={() => {
 | 
			
		||||
              manageRedemption(record.id, 'delete', record).then(
 | 
			
		||||
                () => {
 | 
			
		||||
                  removeRecord(record.key);
 | 
			
		||||
                }
 | 
			
		||||
              );
 | 
			
		||||
              manageRedemption(record.id, 'delete', record).then(() => {
 | 
			
		||||
                removeRecord(record.key);
 | 
			
		||||
              });
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <Button theme="light" type="danger" style={{ marginRight: 1 }}>删除</Button>
 | 
			
		||||
            <Button theme='light' type='danger' style={{ marginRight: 1 }}>
 | 
			
		||||
              删除
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Popconfirm>
 | 
			
		||||
          {
 | 
			
		||||
            record.status === 1 ?
 | 
			
		||||
              <Button theme="light" type="warning" style={{ marginRight: 1 }} onClick={
 | 
			
		||||
                async () => {
 | 
			
		||||
                  manageRedemption(
 | 
			
		||||
                    record.id,
 | 
			
		||||
                    'disable',
 | 
			
		||||
                    record
 | 
			
		||||
                  );
 | 
			
		||||
                }
 | 
			
		||||
              }>禁用</Button> :
 | 
			
		||||
              <Button theme="light" type="secondary" style={{ marginRight: 1 }} onClick={
 | 
			
		||||
                async () => {
 | 
			
		||||
                  manageRedemption(
 | 
			
		||||
                    record.id,
 | 
			
		||||
                    'enable',
 | 
			
		||||
                    record
 | 
			
		||||
                  );
 | 
			
		||||
                }
 | 
			
		||||
              } disabled={record.status === 3}>启用</Button>
 | 
			
		||||
          }
 | 
			
		||||
          <Button theme="light" type="tertiary" style={{ marginRight: 1 }} onClick={
 | 
			
		||||
            () => {
 | 
			
		||||
          {record.status === 1 ? (
 | 
			
		||||
            <Button
 | 
			
		||||
              theme='light'
 | 
			
		||||
              type='warning'
 | 
			
		||||
              style={{ marginRight: 1 }}
 | 
			
		||||
              onClick={async () => {
 | 
			
		||||
                manageRedemption(record.id, 'disable', record);
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              禁用
 | 
			
		||||
            </Button>
 | 
			
		||||
          ) : (
 | 
			
		||||
            <Button
 | 
			
		||||
              theme='light'
 | 
			
		||||
              type='secondary'
 | 
			
		||||
              style={{ marginRight: 1 }}
 | 
			
		||||
              onClick={async () => {
 | 
			
		||||
                manageRedemption(record.id, 'enable', record);
 | 
			
		||||
              }}
 | 
			
		||||
              disabled={record.status === 3}
 | 
			
		||||
            >
 | 
			
		||||
              启用
 | 
			
		||||
            </Button>
 | 
			
		||||
          )}
 | 
			
		||||
          <Button
 | 
			
		||||
            theme='light'
 | 
			
		||||
            type='tertiary'
 | 
			
		||||
            style={{ marginRight: 1 }}
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              setEditingRedemption(record);
 | 
			
		||||
              setShowEdit(true);
 | 
			
		||||
            }
 | 
			
		||||
          } disabled={record.status !== 1}>编辑</Button>
 | 
			
		||||
            }}
 | 
			
		||||
            disabled={record.status !== 1}
 | 
			
		||||
          >
 | 
			
		||||
            编辑
 | 
			
		||||
          </Button>
 | 
			
		||||
        </div>
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
      ),
 | 
			
		||||
    },
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  const [redemptions, setRedemptions] = useState([]);
 | 
			
		||||
@@ -156,7 +179,7 @@ const RedemptionsTable = () => {
 | 
			
		||||
  const [tokenCount, setTokenCount] = useState(ITEMS_PER_PAGE);
 | 
			
		||||
  const [selectedKeys, setSelectedKeys] = useState([]);
 | 
			
		||||
  const [editingRedemption, setEditingRedemption] = useState({
 | 
			
		||||
    id: undefined
 | 
			
		||||
    id: undefined,
 | 
			
		||||
  });
 | 
			
		||||
  const [showEdit, setShowEdit] = useState(false);
 | 
			
		||||
 | 
			
		||||
@@ -178,7 +201,7 @@ const RedemptionsTable = () => {
 | 
			
		||||
    // }
 | 
			
		||||
    // data.key = '' + data.id
 | 
			
		||||
    setRedemptions(redeptions);
 | 
			
		||||
    if (redeptions.length >= (activePage) * ITEMS_PER_PAGE) {
 | 
			
		||||
    if (redeptions.length >= activePage * ITEMS_PER_PAGE) {
 | 
			
		||||
      setTokenCount(redeptions.length + 1);
 | 
			
		||||
    } else {
 | 
			
		||||
      setTokenCount(redeptions.length);
 | 
			
		||||
@@ -202,10 +225,10 @@ const RedemptionsTable = () => {
 | 
			
		||||
    setLoading(false);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const removeRecord = key => {
 | 
			
		||||
  const removeRecord = (key) => {
 | 
			
		||||
    let newDataSource = [...redemptions];
 | 
			
		||||
    if (key != null) {
 | 
			
		||||
      let idx = newDataSource.findIndex(data => data.key === key);
 | 
			
		||||
      let idx = newDataSource.findIndex((data) => data.key === key);
 | 
			
		||||
 | 
			
		||||
      if (idx > -1) {
 | 
			
		||||
        newDataSource.splice(idx, 1);
 | 
			
		||||
@@ -268,7 +291,6 @@ const RedemptionsTable = () => {
 | 
			
		||||
      let newRedemptions = [...redemptions];
 | 
			
		||||
      // let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
 | 
			
		||||
      if (action === 'delete') {
 | 
			
		||||
 | 
			
		||||
      } else {
 | 
			
		||||
        record.status = redemption.status;
 | 
			
		||||
      }
 | 
			
		||||
@@ -286,7 +308,9 @@ const RedemptionsTable = () => {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    setSearching(true);
 | 
			
		||||
    const res = await API.get(`/api/redemption/search?keyword=${searchKeyword}`);
 | 
			
		||||
    const res = await API.get(
 | 
			
		||||
      `/api/redemption/search?keyword=${searchKeyword}`,
 | 
			
		||||
    );
 | 
			
		||||
    const { success, message, data } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      setRedemptions(data);
 | 
			
		||||
@@ -315,32 +339,32 @@ const RedemptionsTable = () => {
 | 
			
		||||
    setLoading(false);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handlePageChange = page => {
 | 
			
		||||
  const handlePageChange = (page) => {
 | 
			
		||||
    setActivePage(page);
 | 
			
		||||
    if (page === Math.ceil(redemptions.length / ITEMS_PER_PAGE) + 1) {
 | 
			
		||||
      // In this case we have to load more data and then append them.
 | 
			
		||||
      loadRedemptions(page - 1).then(r => {
 | 
			
		||||
      });
 | 
			
		||||
      loadRedemptions(page - 1).then((r) => {});
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  let pageData = redemptions.slice((activePage - 1) * ITEMS_PER_PAGE, activePage * ITEMS_PER_PAGE);
 | 
			
		||||
  let pageData = redemptions.slice(
 | 
			
		||||
    (activePage - 1) * ITEMS_PER_PAGE,
 | 
			
		||||
    activePage * ITEMS_PER_PAGE,
 | 
			
		||||
  );
 | 
			
		||||
  const rowSelection = {
 | 
			
		||||
    onSelect: (record, selected) => {
 | 
			
		||||
    },
 | 
			
		||||
    onSelectAll: (selected, selectedRows) => {
 | 
			
		||||
    },
 | 
			
		||||
    onSelect: (record, selected) => {},
 | 
			
		||||
    onSelectAll: (selected, selectedRows) => {},
 | 
			
		||||
    onChange: (selectedRowKeys, selectedRows) => {
 | 
			
		||||
      setSelectedKeys(selectedRows);
 | 
			
		||||
    }
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleRow = (record, index) => {
 | 
			
		||||
    if (record.status !== 1) {
 | 
			
		||||
      return {
 | 
			
		||||
        style: {
 | 
			
		||||
          background: 'var(--semi-color-disabled-border)'
 | 
			
		||||
        }
 | 
			
		||||
          background: 'var(--semi-color-disabled-border)',
 | 
			
		||||
        },
 | 
			
		||||
      };
 | 
			
		||||
    } else {
 | 
			
		||||
      return {};
 | 
			
		||||
@@ -349,45 +373,64 @@ const RedemptionsTable = () => {
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <EditRedemption refresh={refresh} editingRedemption={editingRedemption} visiable={showEdit}
 | 
			
		||||
                      handleClose={closeEdit}></EditRedemption>
 | 
			
		||||
      <EditRedemption
 | 
			
		||||
        refresh={refresh}
 | 
			
		||||
        editingRedemption={editingRedemption}
 | 
			
		||||
        visiable={showEdit}
 | 
			
		||||
        handleClose={closeEdit}
 | 
			
		||||
      ></EditRedemption>
 | 
			
		||||
      <Form onSubmit={searchRedemptions}>
 | 
			
		||||
        <Form.Input
 | 
			
		||||
          label="搜索关键字"
 | 
			
		||||
          field="keyword"
 | 
			
		||||
          icon="search"
 | 
			
		||||
          iconPosition="left"
 | 
			
		||||
          placeholder="关键字(id或者名称)"
 | 
			
		||||
          label='搜索关键字'
 | 
			
		||||
          field='keyword'
 | 
			
		||||
          icon='search'
 | 
			
		||||
          iconPosition='left'
 | 
			
		||||
          placeholder='关键字(id或者名称)'
 | 
			
		||||
          value={searchKeyword}
 | 
			
		||||
          loading={searching}
 | 
			
		||||
          onChange={handleKeywordChange}
 | 
			
		||||
        />
 | 
			
		||||
      </Form>
 | 
			
		||||
 | 
			
		||||
      <Table style={{ marginTop: 20 }} columns={columns} dataSource={pageData} pagination={{
 | 
			
		||||
        currentPage: activePage,
 | 
			
		||||
        pageSize: ITEMS_PER_PAGE,
 | 
			
		||||
        total: tokenCount,
 | 
			
		||||
        // showSizeChanger: true,
 | 
			
		||||
        // pageSizeOptions: [10, 20, 50, 100],
 | 
			
		||||
        formatPageText: (page) => `第 ${page.currentStart} - ${page.currentEnd} 条,共 ${redemptions.length} 条`,
 | 
			
		||||
        // onPageSizeChange: (size) => {
 | 
			
		||||
        //   setPageSize(size);
 | 
			
		||||
        //   setActivePage(1);
 | 
			
		||||
        // },
 | 
			
		||||
        onPageChange: handlePageChange
 | 
			
		||||
      }} loading={loading} rowSelection={rowSelection} onRow={handleRow}>
 | 
			
		||||
      </Table>
 | 
			
		||||
      <Button theme="light" type="primary" style={{ marginRight: 8 }} onClick={
 | 
			
		||||
        () => {
 | 
			
		||||
      <Table
 | 
			
		||||
        style={{ marginTop: 20 }}
 | 
			
		||||
        columns={columns}
 | 
			
		||||
        dataSource={pageData}
 | 
			
		||||
        pagination={{
 | 
			
		||||
          currentPage: activePage,
 | 
			
		||||
          pageSize: ITEMS_PER_PAGE,
 | 
			
		||||
          total: tokenCount,
 | 
			
		||||
          // showSizeChanger: true,
 | 
			
		||||
          // pageSizeOptions: [10, 20, 50, 100],
 | 
			
		||||
          formatPageText: (page) =>
 | 
			
		||||
            `第 ${page.currentStart} - ${page.currentEnd} 条,共 ${redemptions.length} 条`,
 | 
			
		||||
          // onPageSizeChange: (size) => {
 | 
			
		||||
          //   setPageSize(size);
 | 
			
		||||
          //   setActivePage(1);
 | 
			
		||||
          // },
 | 
			
		||||
          onPageChange: handlePageChange,
 | 
			
		||||
        }}
 | 
			
		||||
        loading={loading}
 | 
			
		||||
        rowSelection={rowSelection}
 | 
			
		||||
        onRow={handleRow}
 | 
			
		||||
      ></Table>
 | 
			
		||||
      <Button
 | 
			
		||||
        theme='light'
 | 
			
		||||
        type='primary'
 | 
			
		||||
        style={{ marginRight: 8 }}
 | 
			
		||||
        onClick={() => {
 | 
			
		||||
          setEditingRedemption({
 | 
			
		||||
            id: undefined
 | 
			
		||||
            id: undefined,
 | 
			
		||||
          });
 | 
			
		||||
          setShowEdit(true);
 | 
			
		||||
        }
 | 
			
		||||
      }>添加兑换码</Button>
 | 
			
		||||
      <Button label="复制所选兑换码" type="warning" onClick={
 | 
			
		||||
        async () => {
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        添加兑换码
 | 
			
		||||
      </Button>
 | 
			
		||||
      <Button
 | 
			
		||||
        label='复制所选兑换码'
 | 
			
		||||
        type='warning'
 | 
			
		||||
        onClick={async () => {
 | 
			
		||||
          if (selectedKeys.length === 0) {
 | 
			
		||||
            showError('请至少选择一个兑换码!');
 | 
			
		||||
            return;
 | 
			
		||||
@@ -397,8 +440,10 @@ const RedemptionsTable = () => {
 | 
			
		||||
            keys += selectedKeys[i].name + '    ' + selectedKeys[i].key + '\n';
 | 
			
		||||
          }
 | 
			
		||||
          await copyText(keys);
 | 
			
		||||
        }
 | 
			
		||||
      }>复制所选兑换码到剪贴板</Button>
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        复制所选兑换码到剪贴板
 | 
			
		||||
      </Button>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,13 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { Button, Form, Grid, Header, Image, Message, Segment } from 'semantic-ui-react';
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Form,
 | 
			
		||||
  Grid,
 | 
			
		||||
  Header,
 | 
			
		||||
  Image,
 | 
			
		||||
  Message,
 | 
			
		||||
  Segment,
 | 
			
		||||
} from 'semantic-ui-react';
 | 
			
		||||
import { Link, useNavigate } from 'react-router-dom';
 | 
			
		||||
import { API, getLogo, showError, showInfo, showSuccess } from '../helpers';
 | 
			
		||||
import Turnstile from 'react-turnstile';
 | 
			
		||||
@@ -10,7 +18,7 @@ const RegisterForm = () => {
 | 
			
		||||
    password: '',
 | 
			
		||||
    password2: '',
 | 
			
		||||
    email: '',
 | 
			
		||||
    verification_code: ''
 | 
			
		||||
    verification_code: '',
 | 
			
		||||
  });
 | 
			
		||||
  const { username, password, password2 } = inputs;
 | 
			
		||||
  const [showEmailVerification, setShowEmailVerification] = useState(false);
 | 
			
		||||
@@ -65,7 +73,7 @@ const RegisterForm = () => {
 | 
			
		||||
      inputs.aff_code = affCode;
 | 
			
		||||
      const res = await API.post(
 | 
			
		||||
        `/api/user/register?turnstile=${turnstileToken}`,
 | 
			
		||||
        inputs
 | 
			
		||||
        inputs,
 | 
			
		||||
      );
 | 
			
		||||
      const { success, message } = res.data;
 | 
			
		||||
      if (success) {
 | 
			
		||||
@@ -88,7 +96,7 @@ const RegisterForm = () => {
 | 
			
		||||
    }
 | 
			
		||||
    setLoading(true);
 | 
			
		||||
    const res = await API.get(
 | 
			
		||||
      `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`
 | 
			
		||||
      `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`,
 | 
			
		||||
    );
 | 
			
		||||
    const { success, message } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
@@ -100,49 +108,49 @@ const RegisterForm = () => {
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <Grid textAlign="center" style={{ marginTop: '48px' }}>
 | 
			
		||||
    <Grid textAlign='center' style={{ marginTop: '48px' }}>
 | 
			
		||||
      <Grid.Column style={{ maxWidth: 450 }}>
 | 
			
		||||
        <Header as="h2" color="" textAlign="center">
 | 
			
		||||
        <Header as='h2' color='' textAlign='center'>
 | 
			
		||||
          <Image src={logo} /> 新用户注册
 | 
			
		||||
        </Header>
 | 
			
		||||
        <Form size="large">
 | 
			
		||||
        <Form size='large'>
 | 
			
		||||
          <Segment>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              fluid
 | 
			
		||||
              icon="user"
 | 
			
		||||
              iconPosition="left"
 | 
			
		||||
              placeholder="输入用户名,最长 12 位"
 | 
			
		||||
              icon='user'
 | 
			
		||||
              iconPosition='left'
 | 
			
		||||
              placeholder='输入用户名,最长 12 位'
 | 
			
		||||
              onChange={handleChange}
 | 
			
		||||
              name="username"
 | 
			
		||||
              name='username'
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              fluid
 | 
			
		||||
              icon="lock"
 | 
			
		||||
              iconPosition="left"
 | 
			
		||||
              placeholder="输入密码,最短 8 位,最长 20 位"
 | 
			
		||||
              icon='lock'
 | 
			
		||||
              iconPosition='left'
 | 
			
		||||
              placeholder='输入密码,最短 8 位,最长 20 位'
 | 
			
		||||
              onChange={handleChange}
 | 
			
		||||
              name="password"
 | 
			
		||||
              type="password"
 | 
			
		||||
              name='password'
 | 
			
		||||
              type='password'
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              fluid
 | 
			
		||||
              icon="lock"
 | 
			
		||||
              iconPosition="left"
 | 
			
		||||
              placeholder="输入密码,最短 8 位,最长 20 位"
 | 
			
		||||
              icon='lock'
 | 
			
		||||
              iconPosition='left'
 | 
			
		||||
              placeholder='输入密码,最短 8 位,最长 20 位'
 | 
			
		||||
              onChange={handleChange}
 | 
			
		||||
              name="password2"
 | 
			
		||||
              type="password"
 | 
			
		||||
              name='password2'
 | 
			
		||||
              type='password'
 | 
			
		||||
            />
 | 
			
		||||
            {showEmailVerification ? (
 | 
			
		||||
              <>
 | 
			
		||||
                <Form.Input
 | 
			
		||||
                  fluid
 | 
			
		||||
                  icon="mail"
 | 
			
		||||
                  iconPosition="left"
 | 
			
		||||
                  placeholder="输入邮箱地址"
 | 
			
		||||
                  icon='mail'
 | 
			
		||||
                  iconPosition='left'
 | 
			
		||||
                  placeholder='输入邮箱地址'
 | 
			
		||||
                  onChange={handleChange}
 | 
			
		||||
                  name="email"
 | 
			
		||||
                  type="email"
 | 
			
		||||
                  name='email'
 | 
			
		||||
                  type='email'
 | 
			
		||||
                  action={
 | 
			
		||||
                    <Button onClick={sendVerificationCode} disabled={loading}>
 | 
			
		||||
                      获取验证码
 | 
			
		||||
@@ -151,11 +159,11 @@ const RegisterForm = () => {
 | 
			
		||||
                />
 | 
			
		||||
                <Form.Input
 | 
			
		||||
                  fluid
 | 
			
		||||
                  icon="lock"
 | 
			
		||||
                  iconPosition="left"
 | 
			
		||||
                  placeholder="输入验证码"
 | 
			
		||||
                  icon='lock'
 | 
			
		||||
                  iconPosition='left'
 | 
			
		||||
                  placeholder='输入验证码'
 | 
			
		||||
                  onChange={handleChange}
 | 
			
		||||
                  name="verification_code"
 | 
			
		||||
                  name='verification_code'
 | 
			
		||||
                />
 | 
			
		||||
              </>
 | 
			
		||||
            ) : (
 | 
			
		||||
@@ -172,9 +180,9 @@ const RegisterForm = () => {
 | 
			
		||||
              <></>
 | 
			
		||||
            )}
 | 
			
		||||
            <Button
 | 
			
		||||
              color="green"
 | 
			
		||||
              color='green'
 | 
			
		||||
              fluid
 | 
			
		||||
              size="large"
 | 
			
		||||
              size='large'
 | 
			
		||||
              onClick={handleSubmit}
 | 
			
		||||
              loading={loading}
 | 
			
		||||
            >
 | 
			
		||||
@@ -184,7 +192,7 @@ const RegisterForm = () => {
 | 
			
		||||
        </Form>
 | 
			
		||||
        <Message>
 | 
			
		||||
          已有账户?
 | 
			
		||||
          <Link to="/login" className="btn btn-link">
 | 
			
		||||
          <Link to='/login' className='btn btn-link'>
 | 
			
		||||
            点击登录
 | 
			
		||||
          </Link>
 | 
			
		||||
        </Message>
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,14 @@ import { Link, useNavigate } from 'react-router-dom';
 | 
			
		||||
import { UserContext } from '../context/User';
 | 
			
		||||
import { StatusContext } from '../context/Status';
 | 
			
		||||
 | 
			
		||||
import { API, getLogo, getSystemName, isAdmin, isMobile, showError } from '../helpers';
 | 
			
		||||
import {
 | 
			
		||||
  API,
 | 
			
		||||
  getLogo,
 | 
			
		||||
  getSystemName,
 | 
			
		||||
  isAdmin,
 | 
			
		||||
  isMobile,
 | 
			
		||||
  showError,
 | 
			
		||||
} from '../helpers';
 | 
			
		||||
import '../index.css';
 | 
			
		||||
 | 
			
		||||
import {
 | 
			
		||||
@@ -17,7 +24,7 @@ import {
 | 
			
		||||
  IconKey,
 | 
			
		||||
  IconLayers,
 | 
			
		||||
  IconSetting,
 | 
			
		||||
  IconUser
 | 
			
		||||
  IconUser,
 | 
			
		||||
} from '@douyinfe/semi-icons';
 | 
			
		||||
import { Layout, Nav } from '@douyinfe/semi-ui';
 | 
			
		||||
 | 
			
		||||
@@ -26,7 +33,8 @@ import { Layout, Nav } from '@douyinfe/semi-ui';
 | 
			
		||||
const SiderBar = () => {
 | 
			
		||||
  const [userState, userDispatch] = useContext(UserContext);
 | 
			
		||||
  const [statusState, statusDispatch] = useContext(StatusContext);
 | 
			
		||||
  const defaultIsCollapsed = isMobile() || localStorage.getItem('default_collapse_sidebar') === 'true';
 | 
			
		||||
  const defaultIsCollapsed =
 | 
			
		||||
    isMobile() || localStorage.getItem('default_collapse_sidebar') === 'true';
 | 
			
		||||
 | 
			
		||||
  let navigate = useNavigate();
 | 
			
		||||
  const [selectedKeys, setSelectedKeys] = useState(['home']);
 | 
			
		||||
@@ -46,89 +54,105 @@ const SiderBar = () => {
 | 
			
		||||
    setting: '/setting',
 | 
			
		||||
    about: '/about',
 | 
			
		||||
    chat: '/chat',
 | 
			
		||||
    detail: '/detail'
 | 
			
		||||
    detail: '/detail',
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const headerButtons = useMemo(() => [
 | 
			
		||||
    {
 | 
			
		||||
      text: '首页',
 | 
			
		||||
      itemKey: 'home',
 | 
			
		||||
      to: '/',
 | 
			
		||||
      icon: <IconHome />
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      text: '渠道',
 | 
			
		||||
      itemKey: 'channel',
 | 
			
		||||
      to: '/channel',
 | 
			
		||||
      icon: <IconLayers />,
 | 
			
		||||
      className: isAdmin() ? 'semi-navigation-item-normal' : 'tableHiddle'
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      text: '聊天',
 | 
			
		||||
      itemKey: 'chat',
 | 
			
		||||
      to: '/chat',
 | 
			
		||||
      icon: <IconComment />,
 | 
			
		||||
      className: localStorage.getItem('chat_link') ? 'semi-navigation-item-normal' : 'tableHiddle'
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      text: '令牌',
 | 
			
		||||
      itemKey: 'token',
 | 
			
		||||
      to: '/token',
 | 
			
		||||
      icon: <IconKey />
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      text: '兑换码',
 | 
			
		||||
      itemKey: 'redemption',
 | 
			
		||||
      to: '/redemption',
 | 
			
		||||
      icon: <IconGift />,
 | 
			
		||||
      className: isAdmin() ? 'semi-navigation-item-normal' : 'tableHiddle'
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      text: '钱包',
 | 
			
		||||
      itemKey: 'topup',
 | 
			
		||||
      to: '/topup',
 | 
			
		||||
      icon: <IconCreditCard />
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      text: '用户管理',
 | 
			
		||||
      itemKey: 'user',
 | 
			
		||||
      to: '/user',
 | 
			
		||||
      icon: <IconUser />,
 | 
			
		||||
      className: isAdmin() ? 'semi-navigation-item-normal' : 'tableHiddle'
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      text: '日志',
 | 
			
		||||
      itemKey: 'log',
 | 
			
		||||
      to: '/log',
 | 
			
		||||
      icon: <IconHistogram />
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      text: '数据看板',
 | 
			
		||||
      itemKey: 'detail',
 | 
			
		||||
      to: '/detail',
 | 
			
		||||
      icon: <IconCalendarClock />,
 | 
			
		||||
      className: localStorage.getItem('enable_data_export') === 'true' ? 'semi-navigation-item-normal' : 'tableHiddle'
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      text: '绘图',
 | 
			
		||||
      itemKey: 'midjourney',
 | 
			
		||||
      to: '/midjourney',
 | 
			
		||||
      icon: <IconImage />,
 | 
			
		||||
      className: localStorage.getItem('enable_drawing') === 'true' ? 'semi-navigation-item-normal' : 'tableHiddle'
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      text: '设置',
 | 
			
		||||
      itemKey: 'setting',
 | 
			
		||||
      to: '/setting',
 | 
			
		||||
      icon: <IconSetting />
 | 
			
		||||
    }
 | 
			
		||||
    // {
 | 
			
		||||
    //     text: '关于',
 | 
			
		||||
    //     itemKey: 'about',
 | 
			
		||||
    //     to: '/about',
 | 
			
		||||
    //     icon: <IconAt/>
 | 
			
		||||
    // }
 | 
			
		||||
  ], [localStorage.getItem('enable_data_export'), localStorage.getItem('enable_drawing'), localStorage.getItem('chat_link'), isAdmin()]);
 | 
			
		||||
  const headerButtons = useMemo(
 | 
			
		||||
    () => [
 | 
			
		||||
      {
 | 
			
		||||
        text: '首页',
 | 
			
		||||
        itemKey: 'home',
 | 
			
		||||
        to: '/',
 | 
			
		||||
        icon: <IconHome />,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        text: '渠道',
 | 
			
		||||
        itemKey: 'channel',
 | 
			
		||||
        to: '/channel',
 | 
			
		||||
        icon: <IconLayers />,
 | 
			
		||||
        className: isAdmin() ? 'semi-navigation-item-normal' : 'tableHiddle',
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        text: '聊天',
 | 
			
		||||
        itemKey: 'chat',
 | 
			
		||||
        to: '/chat',
 | 
			
		||||
        icon: <IconComment />,
 | 
			
		||||
        className: localStorage.getItem('chat_link')
 | 
			
		||||
          ? 'semi-navigation-item-normal'
 | 
			
		||||
          : 'tableHiddle',
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        text: '令牌',
 | 
			
		||||
        itemKey: 'token',
 | 
			
		||||
        to: '/token',
 | 
			
		||||
        icon: <IconKey />,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        text: '兑换码',
 | 
			
		||||
        itemKey: 'redemption',
 | 
			
		||||
        to: '/redemption',
 | 
			
		||||
        icon: <IconGift />,
 | 
			
		||||
        className: isAdmin() ? 'semi-navigation-item-normal' : 'tableHiddle',
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        text: '钱包',
 | 
			
		||||
        itemKey: 'topup',
 | 
			
		||||
        to: '/topup',
 | 
			
		||||
        icon: <IconCreditCard />,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        text: '用户管理',
 | 
			
		||||
        itemKey: 'user',
 | 
			
		||||
        to: '/user',
 | 
			
		||||
        icon: <IconUser />,
 | 
			
		||||
        className: isAdmin() ? 'semi-navigation-item-normal' : 'tableHiddle',
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        text: '日志',
 | 
			
		||||
        itemKey: 'log',
 | 
			
		||||
        to: '/log',
 | 
			
		||||
        icon: <IconHistogram />,
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        text: '数据看板',
 | 
			
		||||
        itemKey: 'detail',
 | 
			
		||||
        to: '/detail',
 | 
			
		||||
        icon: <IconCalendarClock />,
 | 
			
		||||
        className:
 | 
			
		||||
          localStorage.getItem('enable_data_export') === 'true'
 | 
			
		||||
            ? 'semi-navigation-item-normal'
 | 
			
		||||
            : 'tableHiddle',
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        text: '绘图',
 | 
			
		||||
        itemKey: 'midjourney',
 | 
			
		||||
        to: '/midjourney',
 | 
			
		||||
        icon: <IconImage />,
 | 
			
		||||
        className:
 | 
			
		||||
          localStorage.getItem('enable_drawing') === 'true'
 | 
			
		||||
            ? 'semi-navigation-item-normal'
 | 
			
		||||
            : 'tableHiddle',
 | 
			
		||||
      },
 | 
			
		||||
      {
 | 
			
		||||
        text: '设置',
 | 
			
		||||
        itemKey: 'setting',
 | 
			
		||||
        to: '/setting',
 | 
			
		||||
        icon: <IconSetting />,
 | 
			
		||||
      },
 | 
			
		||||
      // {
 | 
			
		||||
      //     text: '关于',
 | 
			
		||||
      //     itemKey: 'about',
 | 
			
		||||
      //     to: '/about',
 | 
			
		||||
      //     icon: <IconAt/>
 | 
			
		||||
      // }
 | 
			
		||||
    ],
 | 
			
		||||
    [
 | 
			
		||||
      localStorage.getItem('enable_data_export'),
 | 
			
		||||
      localStorage.getItem('enable_drawing'),
 | 
			
		||||
      localStorage.getItem('chat_link'),
 | 
			
		||||
      isAdmin(),
 | 
			
		||||
    ],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const loadStatus = async () => {
 | 
			
		||||
    const res = await API.get('/api/status');
 | 
			
		||||
@@ -143,8 +167,14 @@ const SiderBar = () => {
 | 
			
		||||
      localStorage.setItem('display_in_currency', data.display_in_currency);
 | 
			
		||||
      localStorage.setItem('enable_drawing', data.enable_drawing);
 | 
			
		||||
      localStorage.setItem('enable_data_export', data.enable_data_export);
 | 
			
		||||
      localStorage.setItem('data_export_default_time', data.data_export_default_time);
 | 
			
		||||
      localStorage.setItem('default_collapse_sidebar', data.default_collapse_sidebar);
 | 
			
		||||
      localStorage.setItem(
 | 
			
		||||
        'data_export_default_time',
 | 
			
		||||
        data.data_export_default_time,
 | 
			
		||||
      );
 | 
			
		||||
      localStorage.setItem(
 | 
			
		||||
        'default_collapse_sidebar',
 | 
			
		||||
        data.default_collapse_sidebar,
 | 
			
		||||
      );
 | 
			
		||||
      localStorage.setItem('mj_notify_enabled', data.mj_notify_enabled);
 | 
			
		||||
      if (data.chat_link) {
 | 
			
		||||
        localStorage.setItem('chat_link', data.chat_link);
 | 
			
		||||
@@ -163,11 +193,14 @@ const SiderBar = () => {
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    loadStatus().then(() => {
 | 
			
		||||
      setIsCollapsed(isMobile() || localStorage.getItem('default_collapse_sidebar') === 'true');
 | 
			
		||||
      setIsCollapsed(
 | 
			
		||||
        isMobile() ||
 | 
			
		||||
          localStorage.getItem('default_collapse_sidebar') === 'true',
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
    let localKey = window.location.pathname.split('/')[1]
 | 
			
		||||
    let localKey = window.location.pathname.split('/')[1];
 | 
			
		||||
    if (localKey === '') {
 | 
			
		||||
      localKey = 'home'
 | 
			
		||||
      localKey = 'home';
 | 
			
		||||
    }
 | 
			
		||||
    setSelectedKeys([localKey]);
 | 
			
		||||
  }, []);
 | 
			
		||||
@@ -179,9 +212,12 @@ const SiderBar = () => {
 | 
			
		||||
          <Nav
 | 
			
		||||
            // bodyStyle={{ maxWidth: 200 }}
 | 
			
		||||
            style={{ maxWidth: 200 }}
 | 
			
		||||
            defaultIsCollapsed={isMobile() || localStorage.getItem('default_collapse_sidebar') === 'true'}
 | 
			
		||||
            defaultIsCollapsed={
 | 
			
		||||
              isMobile() ||
 | 
			
		||||
              localStorage.getItem('default_collapse_sidebar') === 'true'
 | 
			
		||||
            }
 | 
			
		||||
            isCollapsed={isCollapsed}
 | 
			
		||||
            onCollapseChange={collapsed => {
 | 
			
		||||
            onCollapseChange={(collapsed) => {
 | 
			
		||||
              setIsCollapsed(collapsed);
 | 
			
		||||
            }}
 | 
			
		||||
            selectedKeys={selectedKeys}
 | 
			
		||||
@@ -196,20 +232,20 @@ const SiderBar = () => {
 | 
			
		||||
              );
 | 
			
		||||
            }}
 | 
			
		||||
            items={headerButtons}
 | 
			
		||||
            onSelect={key => {
 | 
			
		||||
            onSelect={(key) => {
 | 
			
		||||
              setSelectedKeys([key.itemKey]);
 | 
			
		||||
            }}
 | 
			
		||||
            header={{
 | 
			
		||||
              logo: <img src={logo} alt="logo" style={{ marginRight: '0.75em' }} />,
 | 
			
		||||
              text: systemName
 | 
			
		||||
              logo: (
 | 
			
		||||
                <img src={logo} alt='logo' style={{ marginRight: '0.75em' }} />
 | 
			
		||||
              ),
 | 
			
		||||
              text: systemName,
 | 
			
		||||
            }}
 | 
			
		||||
            // footer={{
 | 
			
		||||
            //   text: '© 2021 NekoAPI',
 | 
			
		||||
            // }}
 | 
			
		||||
          >
 | 
			
		||||
 | 
			
		||||
            <Nav.Footer collapseButton={true}>
 | 
			
		||||
            </Nav.Footer>
 | 
			
		||||
            <Nav.Footer collapseButton={true}></Nav.Footer>
 | 
			
		||||
          </Nav>
 | 
			
		||||
        </div>
 | 
			
		||||
      </Layout>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,13 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { Button, Divider, Form, Grid, Header, Message, Modal } from 'semantic-ui-react';
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Divider,
 | 
			
		||||
  Form,
 | 
			
		||||
  Grid,
 | 
			
		||||
  Header,
 | 
			
		||||
  Message,
 | 
			
		||||
  Modal,
 | 
			
		||||
} from 'semantic-ui-react';
 | 
			
		||||
import { API, removeTrailingSlash, showError, verifyJSON } from '../helpers';
 | 
			
		||||
 | 
			
		||||
const SystemSetting = () => {
 | 
			
		||||
@@ -42,13 +50,14 @@ const SystemSetting = () => {
 | 
			
		||||
    // telegram login
 | 
			
		||||
    TelegramOAuthEnabled: '',
 | 
			
		||||
    TelegramBotToken: '',
 | 
			
		||||
    TelegramBotName: ''
 | 
			
		||||
    TelegramBotName: '',
 | 
			
		||||
  });
 | 
			
		||||
  const [originInputs, setOriginInputs] = useState({});
 | 
			
		||||
  let [loading, setLoading] = useState(false);
 | 
			
		||||
  const [EmailDomainWhitelist, setEmailDomainWhitelist] = useState([]);
 | 
			
		||||
  const [restrictedDomainInput, setRestrictedDomainInput] = useState('');
 | 
			
		||||
  const [showPasswordWarningModal, setShowPasswordWarningModal] = useState(false);
 | 
			
		||||
  const [showPasswordWarningModal, setShowPasswordWarningModal] =
 | 
			
		||||
    useState(false);
 | 
			
		||||
 | 
			
		||||
  const getOptions = async () => {
 | 
			
		||||
    const res = await API.get('/api/option/');
 | 
			
		||||
@@ -63,13 +72,15 @@ const SystemSetting = () => {
 | 
			
		||||
      });
 | 
			
		||||
      setInputs({
 | 
			
		||||
        ...newInputs,
 | 
			
		||||
        EmailDomainWhitelist: newInputs.EmailDomainWhitelist.split(',')
 | 
			
		||||
        EmailDomainWhitelist: newInputs.EmailDomainWhitelist.split(','),
 | 
			
		||||
      });
 | 
			
		||||
      setOriginInputs(newInputs);
 | 
			
		||||
 | 
			
		||||
      setEmailDomainWhitelist(newInputs.EmailDomainWhitelist.split(',').map((item) => {
 | 
			
		||||
        return { key: item, text: item, value: item };
 | 
			
		||||
      }));
 | 
			
		||||
      setEmailDomainWhitelist(
 | 
			
		||||
        newInputs.EmailDomainWhitelist.split(',').map((item) => {
 | 
			
		||||
          return { key: item, text: item, value: item };
 | 
			
		||||
        }),
 | 
			
		||||
      );
 | 
			
		||||
    } else {
 | 
			
		||||
      showError(message);
 | 
			
		||||
    }
 | 
			
		||||
@@ -100,7 +111,7 @@ const SystemSetting = () => {
 | 
			
		||||
    }
 | 
			
		||||
    const res = await API.put('/api/option/', {
 | 
			
		||||
      key,
 | 
			
		||||
      value
 | 
			
		||||
      value,
 | 
			
		||||
    });
 | 
			
		||||
    const { success, message } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
@@ -108,7 +119,8 @@ const SystemSetting = () => {
 | 
			
		||||
        value = value.split(',');
 | 
			
		||||
      }
 | 
			
		||||
      setInputs((inputs) => ({
 | 
			
		||||
        ...inputs, [key]: value
 | 
			
		||||
        ...inputs,
 | 
			
		||||
        [key]: value,
 | 
			
		||||
      }));
 | 
			
		||||
    } else {
 | 
			
		||||
      showError(message);
 | 
			
		||||
@@ -170,21 +182,22 @@ const SystemSetting = () => {
 | 
			
		||||
      await updateOption('TopupGroupRatio', inputs.TopupGroupRatio);
 | 
			
		||||
    }
 | 
			
		||||
    let stripeApiSecret = removeTrailingSlash(inputs.StripeApiSecret);
 | 
			
		||||
    if (stripeApiSecret && !stripeApiSecret.startsWith("sk_")) {
 | 
			
		||||
    if (stripeApiSecret && !stripeApiSecret.startsWith('sk_')) {
 | 
			
		||||
      showError('输入了无效的Stripe API密钥');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    stripeApiSecret && await updateOption('StripeApiSecret', stripeApiSecret);
 | 
			
		||||
    stripeApiSecret && (await updateOption('StripeApiSecret', stripeApiSecret));
 | 
			
		||||
 | 
			
		||||
    let stripeWebhookSecret = removeTrailingSlash(inputs.StripeWebhookSecret);
 | 
			
		||||
    if (stripeWebhookSecret && !stripeWebhookSecret.startsWith("whsec_")) {
 | 
			
		||||
    if (stripeWebhookSecret && !stripeWebhookSecret.startsWith('whsec_')) {
 | 
			
		||||
      showError('输入了无效的Stripe Webhook签名密钥');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    stripeWebhookSecret && await updateOption('StripeWebhookSecret', stripeWebhookSecret);
 | 
			
		||||
    stripeWebhookSecret &&
 | 
			
		||||
      (await updateOption('StripeWebhookSecret', stripeWebhookSecret));
 | 
			
		||||
 | 
			
		||||
    let stripePriceId = removeTrailingSlash(inputs.StripePriceId);
 | 
			
		||||
    if (stripePriceId && !stripePriceId.startsWith("price_")) {
 | 
			
		||||
    if (stripePriceId && !stripePriceId.startsWith('price_')) {
 | 
			
		||||
      showError('输入了无效的Stripe 物品价格ID');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
@@ -219,13 +232,16 @@ const SystemSetting = () => {
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  const submitEmailDomainWhitelist = async () => {
 | 
			
		||||
    if (
 | 
			
		||||
      originInputs['EmailDomainWhitelist'] !== inputs.EmailDomainWhitelist.join(',') &&
 | 
			
		||||
      originInputs['EmailDomainWhitelist'] !==
 | 
			
		||||
        inputs.EmailDomainWhitelist.join(',') &&
 | 
			
		||||
      inputs.SMTPToken !== ''
 | 
			
		||||
    ) {
 | 
			
		||||
      await updateOption('EmailDomainWhitelist', inputs.EmailDomainWhitelist.join(','));
 | 
			
		||||
      await updateOption(
 | 
			
		||||
        'EmailDomainWhitelist',
 | 
			
		||||
        inputs.EmailDomainWhitelist.join(','),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@@ -233,7 +249,7 @@ const SystemSetting = () => {
 | 
			
		||||
    if (originInputs['WeChatServerAddress'] !== inputs.WeChatServerAddress) {
 | 
			
		||||
      await updateOption(
 | 
			
		||||
        'WeChatServerAddress',
 | 
			
		||||
        removeTrailingSlash(inputs.WeChatServerAddress)
 | 
			
		||||
        removeTrailingSlash(inputs.WeChatServerAddress),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    if (
 | 
			
		||||
@@ -242,7 +258,7 @@ const SystemSetting = () => {
 | 
			
		||||
    ) {
 | 
			
		||||
      await updateOption(
 | 
			
		||||
        'WeChatAccountQRCodeImageURL',
 | 
			
		||||
        inputs.WeChatAccountQRCodeImageURL
 | 
			
		||||
        inputs.WeChatAccountQRCodeImageURL,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    if (
 | 
			
		||||
@@ -300,17 +316,23 @@ const SystemSetting = () => {
 | 
			
		||||
 | 
			
		||||
  const submitNewRestrictedDomain = () => {
 | 
			
		||||
    const localDomainList = inputs.EmailDomainWhitelist;
 | 
			
		||||
    if (restrictedDomainInput !== '' && !localDomainList.includes(restrictedDomainInput)) {
 | 
			
		||||
    if (
 | 
			
		||||
      restrictedDomainInput !== '' &&
 | 
			
		||||
      !localDomainList.includes(restrictedDomainInput)
 | 
			
		||||
    ) {
 | 
			
		||||
      setRestrictedDomainInput('');
 | 
			
		||||
      setInputs({
 | 
			
		||||
        ...inputs,
 | 
			
		||||
        EmailDomainWhitelist: [...localDomainList, restrictedDomainInput]
 | 
			
		||||
        EmailDomainWhitelist: [...localDomainList, restrictedDomainInput],
 | 
			
		||||
      });
 | 
			
		||||
      setEmailDomainWhitelist([...EmailDomainWhitelist, {
 | 
			
		||||
        key: restrictedDomainInput,
 | 
			
		||||
        text: restrictedDomainInput,
 | 
			
		||||
        value: restrictedDomainInput
 | 
			
		||||
      }]);
 | 
			
		||||
      setEmailDomainWhitelist([
 | 
			
		||||
        ...EmailDomainWhitelist,
 | 
			
		||||
        {
 | 
			
		||||
          key: restrictedDomainInput,
 | 
			
		||||
          text: restrictedDomainInput,
 | 
			
		||||
          value: restrictedDomainInput,
 | 
			
		||||
        },
 | 
			
		||||
      ]);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@@ -318,13 +340,13 @@ const SystemSetting = () => {
 | 
			
		||||
    <Grid columns={1}>
 | 
			
		||||
      <Grid.Column>
 | 
			
		||||
        <Form loading={loading}>
 | 
			
		||||
          <Header as="h3">通用设置</Header>
 | 
			
		||||
          <Form.Group widths="equal">
 | 
			
		||||
          <Header as='h3'>通用设置</Header>
 | 
			
		||||
          <Form.Group widths='equal'>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label="服务器地址"
 | 
			
		||||
              placeholder="例如:https://yourdomain.com"
 | 
			
		||||
              label='服务器地址'
 | 
			
		||||
              placeholder='例如:https://yourdomain.com'
 | 
			
		||||
              value={inputs.ServerAddress}
 | 
			
		||||
              name="ServerAddress"
 | 
			
		||||
              name='ServerAddress'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
@@ -332,15 +354,23 @@ const SystemSetting = () => {
 | 
			
		||||
            更新服务器地址
 | 
			
		||||
          </Form.Button>
 | 
			
		||||
          <Divider />
 | 
			
		||||
          <Header as="h3">
 | 
			
		||||
          <Header as='h3'>
 | 
			
		||||
            支付设置(当前仅支持Stripe Checkout)
 | 
			
		||||
            <Header.Subheader>
 | 
			
		||||
              密钥、Webhook 等设置请
 | 
			
		||||
              <a href="https://dashboard.stripe.com/developers" target="_blank" rel="noreferrer">
 | 
			
		||||
              <a
 | 
			
		||||
                href='https://dashboard.stripe.com/developers'
 | 
			
		||||
                target='_blank'
 | 
			
		||||
                rel='noreferrer'
 | 
			
		||||
              >
 | 
			
		||||
                点击此处
 | 
			
		||||
              </a>
 | 
			
		||||
              进行设置,最好先在
 | 
			
		||||
              <a href="https://dashboard.stripe.com/test/developers" target="_blank" rel="noreferrer">
 | 
			
		||||
              <a
 | 
			
		||||
                href='https://dashboard.stripe.com/test/developers'
 | 
			
		||||
                target='_blank'
 | 
			
		||||
                rel='noreferrer'
 | 
			
		||||
              >
 | 
			
		||||
                测试环境
 | 
			
		||||
              </a>
 | 
			
		||||
              进行测试
 | 
			
		||||
@@ -349,60 +379,61 @@ const SystemSetting = () => {
 | 
			
		||||
          <Message>
 | 
			
		||||
            Webhook 填:
 | 
			
		||||
            <code>{`${inputs.ServerAddress}/api/stripe/webhook`}</code>
 | 
			
		||||
            ,需要包含事件:<code>checkout.session.completed</code> 和 <code>checkout.session.expired</code>
 | 
			
		||||
            ,需要包含事件:<code>checkout.session.completed</code> 和{' '}
 | 
			
		||||
            <code>checkout.session.expired</code>
 | 
			
		||||
          </Message>
 | 
			
		||||
          <Form.Group widths="equal">
 | 
			
		||||
          <Form.Group widths='equal'>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label="API密钥"
 | 
			
		||||
              placeholder="sk_xxx的Stripe密钥,敏感信息不显示"
 | 
			
		||||
              label='API密钥'
 | 
			
		||||
              placeholder='sk_xxx的Stripe密钥,敏感信息不显示'
 | 
			
		||||
              value={inputs.StripeApiSecret}
 | 
			
		||||
              name="StripeApiSecret"
 | 
			
		||||
              name='StripeApiSecret'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label="Webhook签名密钥"
 | 
			
		||||
              placeholder="whsec_xxx的Webhook签名密钥,敏感信息不显示"
 | 
			
		||||
              label='Webhook签名密钥'
 | 
			
		||||
              placeholder='whsec_xxx的Webhook签名密钥,敏感信息不显示'
 | 
			
		||||
              value={inputs.StripeWebhookSecret}
 | 
			
		||||
              name="StripeWebhookSecret"
 | 
			
		||||
              name='StripeWebhookSecret'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label="商品价格ID"
 | 
			
		||||
              placeholder="price_xxx的商品价格ID,新建产品后可获得"
 | 
			
		||||
              label='商品价格ID'
 | 
			
		||||
              placeholder='price_xxx的商品价格ID,新建产品后可获得'
 | 
			
		||||
              value={inputs.StripePriceId}
 | 
			
		||||
              name="StripePriceId"
 | 
			
		||||
              name='StripePriceId'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Group widths="equal">
 | 
			
		||||
          <Form.Group widths='equal'>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
                label="商品单价(元)"
 | 
			
		||||
                placeholder="商品的人民币价格"
 | 
			
		||||
                value={inputs.StripeUnitPrice}
 | 
			
		||||
                name="StripeUnitPrice"
 | 
			
		||||
                type={"number"}
 | 
			
		||||
                min={0}
 | 
			
		||||
                onChange={handleInputChange}
 | 
			
		||||
              label='商品单价(元)'
 | 
			
		||||
              placeholder='商品的人民币价格'
 | 
			
		||||
              value={inputs.StripeUnitPrice}
 | 
			
		||||
              name='StripeUnitPrice'
 | 
			
		||||
              type={'number'}
 | 
			
		||||
              min={0}
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
                label="最低充值数量"
 | 
			
		||||
                placeholder="例如:2,就是最低充值2件商品"
 | 
			
		||||
                value={inputs.MinTopUp}
 | 
			
		||||
                name="MinTopUp"
 | 
			
		||||
                type={"number"}
 | 
			
		||||
                min={1}
 | 
			
		||||
                onChange={handleInputChange}
 | 
			
		||||
              label='最低充值数量'
 | 
			
		||||
              placeholder='例如:2,就是最低充值2件商品'
 | 
			
		||||
              value={inputs.MinTopUp}
 | 
			
		||||
              name='MinTopUp'
 | 
			
		||||
              type={'number'}
 | 
			
		||||
              min={1}
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Group widths="equal">
 | 
			
		||||
          <Form.Group widths='equal'>
 | 
			
		||||
            <Form.TextArea
 | 
			
		||||
              label="充值分组倍率"
 | 
			
		||||
              name="TopupGroupRatio"
 | 
			
		||||
              label='充值分组倍率'
 | 
			
		||||
              name='TopupGroupRatio'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              style={{ minHeight: 250, fontFamily: 'JetBrains Mono, Consolas' }}
 | 
			
		||||
              autoComplete="new-password"
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.TopupGroupRatio}
 | 
			
		||||
              placeholder="为一个 JSON 文本,键为组名称,值为倍率"
 | 
			
		||||
              placeholder='为一个 JSON 文本,键为组名称,值为倍率'
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Group inline>
 | 
			
		||||
@@ -411,22 +442,21 @@ const SystemSetting = () => {
 | 
			
		||||
            </Form.Button>
 | 
			
		||||
            <Form.Checkbox
 | 
			
		||||
              checked={inputs.PaymentEnabled === 'true'}
 | 
			
		||||
              label="开启在线支付"
 | 
			
		||||
              name="PaymentEnabled"
 | 
			
		||||
              label='开启在线支付'
 | 
			
		||||
              name='PaymentEnabled'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Divider />
 | 
			
		||||
          <Header as="h3">配置登录注册</Header>
 | 
			
		||||
          <Header as='h3'>配置登录注册</Header>
 | 
			
		||||
          <Form.Group inline>
 | 
			
		||||
            <Form.Checkbox
 | 
			
		||||
              checked={inputs.PasswordLoginEnabled === 'true'}
 | 
			
		||||
              label="允许通过密码进行登录"
 | 
			
		||||
              name="PasswordLoginEnabled"
 | 
			
		||||
              label='允许通过密码进行登录'
 | 
			
		||||
              name='PasswordLoginEnabled'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
            {
 | 
			
		||||
              showPasswordWarningModal &&
 | 
			
		||||
            {showPasswordWarningModal && (
 | 
			
		||||
              <Modal
 | 
			
		||||
                open={showPasswordWarningModal}
 | 
			
		||||
                onClose={() => setShowPasswordWarningModal(false)}
 | 
			
		||||
@@ -435,12 +465,16 @@ const SystemSetting = () => {
 | 
			
		||||
              >
 | 
			
		||||
                <Modal.Header>警告</Modal.Header>
 | 
			
		||||
                <Modal.Content>
 | 
			
		||||
                  <p>取消密码登录将导致所有未绑定其他登录方式的用户(包括管理员)无法通过密码登录,确认取消?</p>
 | 
			
		||||
                  <p>
 | 
			
		||||
                    取消密码登录将导致所有未绑定其他登录方式的用户(包括管理员)无法通过密码登录,确认取消?
 | 
			
		||||
                  </p>
 | 
			
		||||
                </Modal.Content>
 | 
			
		||||
                <Modal.Actions>
 | 
			
		||||
                  <Button onClick={() => setShowPasswordWarningModal(false)}>取消</Button>
 | 
			
		||||
                  <Button onClick={() => setShowPasswordWarningModal(false)}>
 | 
			
		||||
                    取消
 | 
			
		||||
                  </Button>
 | 
			
		||||
                  <Button
 | 
			
		||||
                    color="yellow"
 | 
			
		||||
                    color='yellow'
 | 
			
		||||
                    onClick={async () => {
 | 
			
		||||
                      setShowPasswordWarningModal(false);
 | 
			
		||||
                      await updateOption('PasswordLoginEnabled', 'false');
 | 
			
		||||
@@ -450,23 +484,23 @@ const SystemSetting = () => {
 | 
			
		||||
                  </Button>
 | 
			
		||||
                </Modal.Actions>
 | 
			
		||||
              </Modal>
 | 
			
		||||
            }
 | 
			
		||||
            )}
 | 
			
		||||
            <Form.Checkbox
 | 
			
		||||
              checked={inputs.PasswordRegisterEnabled === 'true'}
 | 
			
		||||
              label="允许通过密码进行注册"
 | 
			
		||||
              name="PasswordRegisterEnabled"
 | 
			
		||||
              label='允许通过密码进行注册'
 | 
			
		||||
              name='PasswordRegisterEnabled'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Checkbox
 | 
			
		||||
              checked={inputs.EmailVerificationEnabled === 'true'}
 | 
			
		||||
              label="通过密码注册时需要进行邮箱验证"
 | 
			
		||||
              name="EmailVerificationEnabled"
 | 
			
		||||
              label='通过密码注册时需要进行邮箱验证'
 | 
			
		||||
              name='EmailVerificationEnabled'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Checkbox
 | 
			
		||||
              checked={inputs.GitHubOAuthEnabled === 'true'}
 | 
			
		||||
              label="允许通过 GitHub 账户登录 & 注册"
 | 
			
		||||
              name="GitHubOAuthEnabled"
 | 
			
		||||
              label='允许通过 GitHub 账户登录 & 注册'
 | 
			
		||||
              name='GitHubOAuthEnabled'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Checkbox
 | 
			
		||||
@@ -477,136 +511,149 @@ const SystemSetting = () => {
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Checkbox
 | 
			
		||||
              checked={inputs.WeChatAuthEnabled === 'true'}
 | 
			
		||||
              label="允许通过微信登录 & 注册"
 | 
			
		||||
              name="WeChatAuthEnabled"
 | 
			
		||||
              label='允许通过微信登录 & 注册'
 | 
			
		||||
              name='WeChatAuthEnabled'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Checkbox
 | 
			
		||||
              checked={inputs.TelegramOAuthEnabled === 'true'}
 | 
			
		||||
              label="允许通过 Telegram 进行登录"
 | 
			
		||||
              name="TelegramOAuthEnabled"
 | 
			
		||||
              label='允许通过 Telegram 进行登录'
 | 
			
		||||
              name='TelegramOAuthEnabled'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Group inline>
 | 
			
		||||
            <Form.Checkbox
 | 
			
		||||
              checked={inputs.RegisterEnabled === 'true'}
 | 
			
		||||
              label="允许新用户注册(此项为否时,新用户将无法以任何方式进行注册)"
 | 
			
		||||
              name="RegisterEnabled"
 | 
			
		||||
              label='允许新用户注册(此项为否时,新用户将无法以任何方式进行注册)'
 | 
			
		||||
              name='RegisterEnabled'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Checkbox
 | 
			
		||||
              checked={inputs.TurnstileCheckEnabled === 'true'}
 | 
			
		||||
              label="启用 Turnstile 用户校验"
 | 
			
		||||
              name="TurnstileCheckEnabled"
 | 
			
		||||
              label='启用 Turnstile 用户校验'
 | 
			
		||||
              name='TurnstileCheckEnabled'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Divider />
 | 
			
		||||
          <Header as="h3">
 | 
			
		||||
          <Header as='h3'>
 | 
			
		||||
            配置邮箱域名白名单
 | 
			
		||||
            <Header.Subheader>用以防止恶意用户利用临时邮箱批量注册</Header.Subheader>
 | 
			
		||||
            <Header.Subheader>
 | 
			
		||||
              用以防止恶意用户利用临时邮箱批量注册
 | 
			
		||||
            </Header.Subheader>
 | 
			
		||||
          </Header>
 | 
			
		||||
          <Form.Group widths={3}>
 | 
			
		||||
            <Form.Checkbox
 | 
			
		||||
              label="启用邮箱域名白名单"
 | 
			
		||||
              name="EmailDomainRestrictionEnabled"
 | 
			
		||||
              label='启用邮箱域名白名单'
 | 
			
		||||
              name='EmailDomainRestrictionEnabled'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              checked={inputs.EmailDomainRestrictionEnabled === 'true'}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Group widths={2}>
 | 
			
		||||
            <Form.Dropdown
 | 
			
		||||
              label="允许的邮箱域名"
 | 
			
		||||
              placeholder="允许的邮箱域名"
 | 
			
		||||
              name="EmailDomainWhitelist"
 | 
			
		||||
              label='允许的邮箱域名'
 | 
			
		||||
              placeholder='允许的邮箱域名'
 | 
			
		||||
              name='EmailDomainWhitelist'
 | 
			
		||||
              required
 | 
			
		||||
              fluid
 | 
			
		||||
              multiple
 | 
			
		||||
              selection
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              value={inputs.EmailDomainWhitelist}
 | 
			
		||||
              autoComplete="new-password"
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              options={EmailDomainWhitelist}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label="添加新的允许的邮箱域名"
 | 
			
		||||
              label='添加新的允许的邮箱域名'
 | 
			
		||||
              action={
 | 
			
		||||
                <Button type="button" onClick={() => {
 | 
			
		||||
                  submitNewRestrictedDomain();
 | 
			
		||||
                }}>填入</Button>
 | 
			
		||||
                <Button
 | 
			
		||||
                  type='button'
 | 
			
		||||
                  onClick={() => {
 | 
			
		||||
                    submitNewRestrictedDomain();
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  填入
 | 
			
		||||
                </Button>
 | 
			
		||||
              }
 | 
			
		||||
              onKeyDown={(e) => {
 | 
			
		||||
                if (e.key === 'Enter') {
 | 
			
		||||
                  submitNewRestrictedDomain();
 | 
			
		||||
                }
 | 
			
		||||
              }}
 | 
			
		||||
              autoComplete="new-password"
 | 
			
		||||
              placeholder="输入新的允许的邮箱域名"
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              placeholder='输入新的允许的邮箱域名'
 | 
			
		||||
              value={restrictedDomainInput}
 | 
			
		||||
              onChange={(e, { value }) => {
 | 
			
		||||
                setRestrictedDomainInput(value);
 | 
			
		||||
              }}
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Button onClick={submitEmailDomainWhitelist}>保存邮箱域名白名单设置</Form.Button>
 | 
			
		||||
          <Form.Button onClick={submitEmailDomainWhitelist}>
 | 
			
		||||
            保存邮箱域名白名单设置
 | 
			
		||||
          </Form.Button>
 | 
			
		||||
          <Divider />
 | 
			
		||||
          <Header as="h3">
 | 
			
		||||
          <Header as='h3'>
 | 
			
		||||
            配置 SMTP
 | 
			
		||||
            <Header.Subheader>用以支持系统的邮件发送</Header.Subheader>
 | 
			
		||||
          </Header>
 | 
			
		||||
          <Form.Group widths={3}>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label="SMTP 服务器地址"
 | 
			
		||||
              name="SMTPServer"
 | 
			
		||||
              label='SMTP 服务器地址'
 | 
			
		||||
              name='SMTPServer'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              autoComplete="new-password"
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.SMTPServer}
 | 
			
		||||
              placeholder="例如:smtp.qq.com"
 | 
			
		||||
              placeholder='例如:smtp.qq.com'
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label="SMTP 端口"
 | 
			
		||||
              name="SMTPPort"
 | 
			
		||||
              label='SMTP 端口'
 | 
			
		||||
              name='SMTPPort'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              autoComplete="new-password"
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.SMTPPort}
 | 
			
		||||
              placeholder="默认: 587"
 | 
			
		||||
              placeholder='默认: 587'
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label="SMTP 账户"
 | 
			
		||||
              name="SMTPAccount"
 | 
			
		||||
              label='SMTP 账户'
 | 
			
		||||
              name='SMTPAccount'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              autoComplete="new-password"
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.SMTPAccount}
 | 
			
		||||
              placeholder="通常是邮箱地址"
 | 
			
		||||
              placeholder='通常是邮箱地址'
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Group widths={3}>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label="SMTP 发送者邮箱"
 | 
			
		||||
              name="SMTPFrom"
 | 
			
		||||
              label='SMTP 发送者邮箱'
 | 
			
		||||
              name='SMTPFrom'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              autoComplete="new-password"
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.SMTPFrom}
 | 
			
		||||
              placeholder="通常和邮箱地址保持一致"
 | 
			
		||||
              placeholder='通常和邮箱地址保持一致'
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label="SMTP 访问凭证"
 | 
			
		||||
              name="SMTPToken"
 | 
			
		||||
              label='SMTP 访问凭证'
 | 
			
		||||
              name='SMTPToken'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              type="password"
 | 
			
		||||
              autoComplete="new-password"
 | 
			
		||||
              type='password'
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              checked={inputs.RegisterEnabled === 'true'}
 | 
			
		||||
              placeholder="敏感信息不会发送到前端显示"
 | 
			
		||||
              placeholder='敏感信息不会发送到前端显示'
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Button onClick={submitSMTP}>保存 SMTP 设置</Form.Button>
 | 
			
		||||
          <Divider />
 | 
			
		||||
          <Header as="h3">
 | 
			
		||||
          <Header as='h3'>
 | 
			
		||||
            配置 GitHub OAuth App
 | 
			
		||||
            <Header.Subheader>
 | 
			
		||||
              用以支持通过 GitHub 进行登录注册,
 | 
			
		||||
              <a href="https://github.com/settings/developers" target="_blank" rel="noreferrer">
 | 
			
		||||
              <a
 | 
			
		||||
                href='https://github.com/settings/developers'
 | 
			
		||||
                target='_blank'
 | 
			
		||||
                rel='noreferrer'
 | 
			
		||||
              >
 | 
			
		||||
                点击此处
 | 
			
		||||
              </a>
 | 
			
		||||
              管理你的 GitHub OAuth App
 | 
			
		||||
@@ -619,21 +666,21 @@ const SystemSetting = () => {
 | 
			
		||||
          </Message>
 | 
			
		||||
          <Form.Group widths={3}>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label="GitHub Client ID"
 | 
			
		||||
              name="GitHubClientId"
 | 
			
		||||
              label='GitHub Client ID'
 | 
			
		||||
              name='GitHubClientId'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              autoComplete="new-password"
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.GitHubClientId}
 | 
			
		||||
              placeholder="输入你注册的 GitHub OAuth APP 的 ID"
 | 
			
		||||
              placeholder='输入你注册的 GitHub OAuth APP 的 ID'
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label="GitHub Client Secret"
 | 
			
		||||
              name="GitHubClientSecret"
 | 
			
		||||
              label='GitHub Client Secret'
 | 
			
		||||
              name='GitHubClientSecret'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              type="password"
 | 
			
		||||
              autoComplete="new-password"
 | 
			
		||||
              type='password'
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.GitHubClientSecret}
 | 
			
		||||
              placeholder="敏感信息不会发送到前端显示"
 | 
			
		||||
              placeholder='敏感信息不会发送到前端显示'
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Button onClick={submitGitHubOAuth}>
 | 
			
		||||
@@ -644,7 +691,11 @@ const SystemSetting = () => {
 | 
			
		||||
            配置 LINUX DO Oauth
 | 
			
		||||
            <Header.Subheader>
 | 
			
		||||
              用以支持通过 LINUX DO 进行登录注册,
 | 
			
		||||
              <a href='https://connect.linux.do' target='_blank' rel="noreferrer">
 | 
			
		||||
              <a
 | 
			
		||||
                href='https://connect.linux.do'
 | 
			
		||||
                target='_blank'
 | 
			
		||||
                rel='noreferrer'
 | 
			
		||||
              >
 | 
			
		||||
                点击此处
 | 
			
		||||
              </a>
 | 
			
		||||
              管理你的 LINUX DO OAuth
 | 
			
		||||
@@ -674,27 +725,28 @@ const SystemSetting = () => {
 | 
			
		||||
              placeholder='敏感信息不会发送到前端显示'
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
                label='限制最低信任等级'
 | 
			
		||||
                name='LinuxDoMinLevel'
 | 
			
		||||
                onChange={handleInputChange}
 | 
			
		||||
                type='number'
 | 
			
		||||
                min={0}
 | 
			
		||||
                max={4}
 | 
			
		||||
                value={inputs.LinuxDoMinLevel}
 | 
			
		||||
                placeholder='输入允许使用的最低 LINUX DO 信任等级'
 | 
			
		||||
              label='限制最低信任等级'
 | 
			
		||||
              name='LinuxDoMinLevel'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              type='number'
 | 
			
		||||
              min={0}
 | 
			
		||||
              max={4}
 | 
			
		||||
              value={inputs.LinuxDoMinLevel}
 | 
			
		||||
              placeholder='输入允许使用的最低 LINUX DO 信任等级'
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Button onClick={submitLinuxDoOAuth}>
 | 
			
		||||
            保存 LINUX DO OAuth 设置
 | 
			
		||||
          </Form.Button>
 | 
			
		||||
          <Divider />
 | 
			
		||||
          <Header as="h3">
 | 
			
		||||
          <Header as='h3'>
 | 
			
		||||
            配置 WeChat Server
 | 
			
		||||
            <Header.Subheader>
 | 
			
		||||
              用以支持通过微信进行登录注册,
 | 
			
		||||
              <a
 | 
			
		||||
                href="https://github.com/songquanpeng/wechat-server"
 | 
			
		||||
                target="_blank" rel="noreferrer"
 | 
			
		||||
                href='https://github.com/songquanpeng/wechat-server'
 | 
			
		||||
                target='_blank'
 | 
			
		||||
                rel='noreferrer'
 | 
			
		||||
              >
 | 
			
		||||
                点击此处
 | 
			
		||||
              </a>
 | 
			
		||||
@@ -703,61 +755,65 @@ const SystemSetting = () => {
 | 
			
		||||
          </Header>
 | 
			
		||||
          <Form.Group widths={3}>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label="WeChat Server 服务器地址"
 | 
			
		||||
              name="WeChatServerAddress"
 | 
			
		||||
              placeholder="例如:https://yourdomain.com"
 | 
			
		||||
              label='WeChat Server 服务器地址'
 | 
			
		||||
              name='WeChatServerAddress'
 | 
			
		||||
              placeholder='例如:https://yourdomain.com'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              autoComplete="new-password"
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.WeChatServerAddress}
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label="WeChat Server 访问凭证"
 | 
			
		||||
              name="WeChatServerToken"
 | 
			
		||||
              type="password"
 | 
			
		||||
              label='WeChat Server 访问凭证'
 | 
			
		||||
              name='WeChatServerToken'
 | 
			
		||||
              type='password'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              autoComplete="new-password"
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.WeChatServerToken}
 | 
			
		||||
              placeholder="敏感信息不会发送到前端显示"
 | 
			
		||||
              placeholder='敏感信息不会发送到前端显示'
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label="微信公众号二维码图片链接"
 | 
			
		||||
              name="WeChatAccountQRCodeImageURL"
 | 
			
		||||
              label='微信公众号二维码图片链接'
 | 
			
		||||
              name='WeChatAccountQRCodeImageURL'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              autoComplete="new-password"
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.WeChatAccountQRCodeImageURL}
 | 
			
		||||
              placeholder="输入一个图片链接"
 | 
			
		||||
              placeholder='输入一个图片链接'
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Button onClick={submitWeChat}>
 | 
			
		||||
            保存 WeChat Server 设置
 | 
			
		||||
          </Form.Button>
 | 
			
		||||
          <Divider />
 | 
			
		||||
          <Header as="h3">配置 Telegram 登录</Header>
 | 
			
		||||
          <Header as='h3'>配置 Telegram 登录</Header>
 | 
			
		||||
          <Form.Group inline>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label="Telegram Bot Token"
 | 
			
		||||
              name="TelegramBotToken"
 | 
			
		||||
              label='Telegram Bot Token'
 | 
			
		||||
              name='TelegramBotToken'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              value={inputs.TelegramBotToken}
 | 
			
		||||
              placeholder="输入你的 Telegram Bot Token"
 | 
			
		||||
              placeholder='输入你的 Telegram Bot Token'
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label="Telegram Bot 名称"
 | 
			
		||||
              name="TelegramBotName"
 | 
			
		||||
              label='Telegram Bot 名称'
 | 
			
		||||
              name='TelegramBotName'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              value={inputs.TelegramBotName}
 | 
			
		||||
              placeholder="输入你的 Telegram Bot 名称"
 | 
			
		||||
              placeholder='输入你的 Telegram Bot 名称'
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Button onClick={submitTelegramSettings}>
 | 
			
		||||
            保存 Telegram 登录设置
 | 
			
		||||
          </Form.Button>
 | 
			
		||||
          <Divider />
 | 
			
		||||
          <Header as="h3">
 | 
			
		||||
          <Header as='h3'>
 | 
			
		||||
            配置 Turnstile
 | 
			
		||||
            <Header.Subheader>
 | 
			
		||||
              用以支持用户校验,
 | 
			
		||||
              <a href="https://dash.cloudflare.com/" target="_blank" rel="noreferrer">
 | 
			
		||||
              <a
 | 
			
		||||
                href='https://dash.cloudflare.com/'
 | 
			
		||||
                target='_blank'
 | 
			
		||||
                rel='noreferrer'
 | 
			
		||||
              >
 | 
			
		||||
                点击此处
 | 
			
		||||
              </a>
 | 
			
		||||
              管理你的 Turnstile Sites,推荐选择 Invisible Widget Type
 | 
			
		||||
@@ -765,21 +821,21 @@ const SystemSetting = () => {
 | 
			
		||||
          </Header>
 | 
			
		||||
          <Form.Group widths={3}>
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label="Turnstile Site Key"
 | 
			
		||||
              name="TurnstileSiteKey"
 | 
			
		||||
              label='Turnstile Site Key'
 | 
			
		||||
              name='TurnstileSiteKey'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              autoComplete="new-password"
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.TurnstileSiteKey}
 | 
			
		||||
              placeholder="输入你注册的 Turnstile Site Key"
 | 
			
		||||
              placeholder='输入你注册的 Turnstile Site Key'
 | 
			
		||||
            />
 | 
			
		||||
            <Form.Input
 | 
			
		||||
              label="Turnstile Secret Key"
 | 
			
		||||
              name="TurnstileSecretKey"
 | 
			
		||||
              label='Turnstile Secret Key'
 | 
			
		||||
              name='TurnstileSecretKey'
 | 
			
		||||
              onChange={handleInputChange}
 | 
			
		||||
              type="password"
 | 
			
		||||
              autoComplete="new-password"
 | 
			
		||||
              type='password'
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              value={inputs.TurnstileSecretKey}
 | 
			
		||||
              placeholder="敏感信息不会发送到前端显示"
 | 
			
		||||
              placeholder='敏感信息不会发送到前端显示'
 | 
			
		||||
            />
 | 
			
		||||
          </Form.Group>
 | 
			
		||||
          <Form.Button onClick={submitTurnstile}>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,25 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { API, copy, showError, showSuccess, timestamp2string } from '../helpers';
 | 
			
		||||
import {
 | 
			
		||||
  API,
 | 
			
		||||
  copy,
 | 
			
		||||
  showError,
 | 
			
		||||
  showSuccess,
 | 
			
		||||
  timestamp2string,
 | 
			
		||||
} from '../helpers';
 | 
			
		||||
 | 
			
		||||
import { ITEMS_PER_PAGE } from '../constants';
 | 
			
		||||
import { renderQuota } from '../helpers/render';
 | 
			
		||||
import { Button, Dropdown, Form, Modal, Popconfirm, Popover, SplitButtonGroup, Table, Tag } from '@douyinfe/semi-ui';
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Dropdown,
 | 
			
		||||
  Form,
 | 
			
		||||
  Modal,
 | 
			
		||||
  Popconfirm,
 | 
			
		||||
  Popover,
 | 
			
		||||
  SplitButtonGroup,
 | 
			
		||||
  Table,
 | 
			
		||||
  Tag,
 | 
			
		||||
} from '@douyinfe/semi-ui';
 | 
			
		||||
 | 
			
		||||
import { IconTreeTriangleDown } from '@douyinfe/semi-icons';
 | 
			
		||||
import EditToken from '../pages/Token/EditToken';
 | 
			
		||||
@@ -11,85 +27,107 @@ import EditToken from '../pages/Token/EditToken';
 | 
			
		||||
const COPY_OPTIONS = [
 | 
			
		||||
  { key: 'next', text: 'ChatGPT Next Web', value: 'next' },
 | 
			
		||||
  { key: 'ama', text: 'ChatGPT Web & Midjourney', value: 'ama' },
 | 
			
		||||
  { key: 'opencat', text: 'OpenCat', value: 'opencat' }
 | 
			
		||||
  { key: 'opencat', text: 'OpenCat', value: 'opencat' },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const OPEN_LINK_OPTIONS = [
 | 
			
		||||
  { key: 'ama', text: 'ChatGPT Web & Midjourney', value: 'ama' },
 | 
			
		||||
  { key: 'opencat', text: 'OpenCat', value: 'opencat' }
 | 
			
		||||
  { key: 'opencat', text: 'OpenCat', value: 'opencat' },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
function renderTimestamp(timestamp) {
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {timestamp2string(timestamp)}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
  return <>{timestamp2string(timestamp)}</>;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function renderStatus(status, model_limits_enabled = false) {
 | 
			
		||||
  switch (status) {
 | 
			
		||||
    case 1:
 | 
			
		||||
      if (model_limits_enabled) {
 | 
			
		||||
        return <Tag color="green" size="large">已启用:限制模型</Tag>;
 | 
			
		||||
        return (
 | 
			
		||||
          <Tag color='green' size='large'>
 | 
			
		||||
            已启用:限制模型
 | 
			
		||||
          </Tag>
 | 
			
		||||
        );
 | 
			
		||||
      } else {
 | 
			
		||||
        return <Tag color="green" size="large">已启用</Tag>;
 | 
			
		||||
        return (
 | 
			
		||||
          <Tag color='green' size='large'>
 | 
			
		||||
            已启用
 | 
			
		||||
          </Tag>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    case 2:
 | 
			
		||||
      return <Tag color="red" size="large"> 已禁用 </Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='red' size='large'>
 | 
			
		||||
          {' '}
 | 
			
		||||
          已禁用{' '}
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    case 3:
 | 
			
		||||
      return <Tag color="yellow" size="large"> 已过期 </Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='yellow' size='large'>
 | 
			
		||||
          {' '}
 | 
			
		||||
          已过期{' '}
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    case 4:
 | 
			
		||||
      return <Tag color="grey" size="large"> 已耗尽 </Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='grey' size='large'>
 | 
			
		||||
          {' '}
 | 
			
		||||
          已耗尽{' '}
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    default:
 | 
			
		||||
      return <Tag color="black" size="large"> 未知状态 </Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='black' size='large'>
 | 
			
		||||
          {' '}
 | 
			
		||||
          未知状态{' '}
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const TokensTable = () => {
 | 
			
		||||
 | 
			
		||||
  const link_menu = [
 | 
			
		||||
    {
 | 
			
		||||
      node: 'item', key: 'next', name: 'ChatGPT Next Web', onClick: () => {
 | 
			
		||||
      node: 'item',
 | 
			
		||||
      key: 'next',
 | 
			
		||||
      name: 'ChatGPT Next Web',
 | 
			
		||||
      onClick: () => {
 | 
			
		||||
        onOpenLink('next');
 | 
			
		||||
      }
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    { node: 'item', key: 'ama', name: 'AMA 问天', value: 'ama' },
 | 
			
		||||
    {
 | 
			
		||||
      node: 'item', key: 'next-mj', name: 'ChatGPT Web & Midjourney', value: 'next-mj', onClick: () => {
 | 
			
		||||
      node: 'item',
 | 
			
		||||
      key: 'next-mj',
 | 
			
		||||
      name: 'ChatGPT Web & Midjourney',
 | 
			
		||||
      value: 'next-mj',
 | 
			
		||||
      onClick: () => {
 | 
			
		||||
        onOpenLink('next-mj');
 | 
			
		||||
      }
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    { node: 'item', key: 'opencat', name: 'OpenCat', value: 'opencat' }
 | 
			
		||||
    { node: 'item', key: 'opencat', name: 'OpenCat', value: 'opencat' },
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  const columns = [
 | 
			
		||||
    {
 | 
			
		||||
      title: '名称',
 | 
			
		||||
      dataIndex: 'name'
 | 
			
		||||
      dataIndex: 'name',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '状态',
 | 
			
		||||
      dataIndex: 'status',
 | 
			
		||||
      key: 'status',
 | 
			
		||||
      render: (text, record, index) => {
 | 
			
		||||
        return (
 | 
			
		||||
          <div>
 | 
			
		||||
            {renderStatus(text, record.model_limits_enabled)}
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
        return <div>{renderStatus(text, record.model_limits_enabled)}</div>;
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '已用额度',
 | 
			
		||||
      dataIndex: 'used_quota',
 | 
			
		||||
      render: (text, record, index) => {
 | 
			
		||||
        return (
 | 
			
		||||
          <div>
 | 
			
		||||
            {renderQuota(parseInt(text))}
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
        return <div>{renderQuota(parseInt(text))}</div>;
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '剩余额度',
 | 
			
		||||
@@ -97,22 +135,25 @@ const TokensTable = () => {
 | 
			
		||||
      render: (text, record, index) => {
 | 
			
		||||
        return (
 | 
			
		||||
          <div>
 | 
			
		||||
            {record.unlimited_quota ? <Tag size={'large'} color={'white'}>无限制</Tag> :
 | 
			
		||||
              <Tag size={'large'} color={'light-blue'}>{renderQuota(parseInt(text))}</Tag>}
 | 
			
		||||
            {record.unlimited_quota ? (
 | 
			
		||||
              <Tag size={'large'} color={'white'}>
 | 
			
		||||
                无限制
 | 
			
		||||
              </Tag>
 | 
			
		||||
            ) : (
 | 
			
		||||
              <Tag size={'large'} color={'light-blue'}>
 | 
			
		||||
                {renderQuota(parseInt(text))}
 | 
			
		||||
              </Tag>
 | 
			
		||||
            )}
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '创建时间',
 | 
			
		||||
      dataIndex: 'created_time',
 | 
			
		||||
      render: (text, record, index) => {
 | 
			
		||||
        return (
 | 
			
		||||
          <div>
 | 
			
		||||
            {renderTimestamp(text)}
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
        return <div>{renderTimestamp(text)}</div>;
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '过期时间',
 | 
			
		||||
@@ -123,7 +164,7 @@ const TokensTable = () => {
 | 
			
		||||
            {record.expired_time === -1 ? '永不过期' : renderTimestamp(text)}
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '',
 | 
			
		||||
@@ -131,25 +172,41 @@ const TokensTable = () => {
 | 
			
		||||
      render: (text, record, index) => (
 | 
			
		||||
        <div>
 | 
			
		||||
          <Popover
 | 
			
		||||
            content={
 | 
			
		||||
              'sk-' + record.key
 | 
			
		||||
            }
 | 
			
		||||
            content={'sk-' + record.key}
 | 
			
		||||
            style={{ padding: 20 }}
 | 
			
		||||
            position="top"
 | 
			
		||||
            position='top'
 | 
			
		||||
          >
 | 
			
		||||
            <Button theme="light" type="tertiary" style={{ marginRight: 1 }}>查看</Button>
 | 
			
		||||
            <Button theme='light' type='tertiary' style={{ marginRight: 1 }}>
 | 
			
		||||
              查看
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Popover>
 | 
			
		||||
          <Button theme="light" type="secondary" style={{ marginRight: 1 }}
 | 
			
		||||
                  onClick={async (text) => {
 | 
			
		||||
                    await copyText('sk-' + record.key);
 | 
			
		||||
                  }}
 | 
			
		||||
          >复制</Button>
 | 
			
		||||
          <SplitButtonGroup style={{ marginRight: 1 }} aria-label="项目操作按钮组">
 | 
			
		||||
            <Button theme="light" style={{ color: 'rgba(var(--semi-teal-7), 1)' }} onClick={() => {
 | 
			
		||||
              onOpenLink('next', record.key);
 | 
			
		||||
            }}>聊天</Button>
 | 
			
		||||
            <Dropdown trigger="click" position="bottomRight" menu={
 | 
			
		||||
              [
 | 
			
		||||
          <Button
 | 
			
		||||
            theme='light'
 | 
			
		||||
            type='secondary'
 | 
			
		||||
            style={{ marginRight: 1 }}
 | 
			
		||||
            onClick={async (text) => {
 | 
			
		||||
              await copyText('sk-' + record.key);
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            复制
 | 
			
		||||
          </Button>
 | 
			
		||||
          <SplitButtonGroup
 | 
			
		||||
            style={{ marginRight: 1 }}
 | 
			
		||||
            aria-label='项目操作按钮组'
 | 
			
		||||
          >
 | 
			
		||||
            <Button
 | 
			
		||||
              theme='light'
 | 
			
		||||
              style={{ color: 'rgba(var(--semi-teal-7), 1)' }}
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                onOpenLink('next', record.key);
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              聊天
 | 
			
		||||
            </Button>
 | 
			
		||||
            <Dropdown
 | 
			
		||||
              trigger='click'
 | 
			
		||||
              position='bottomRight'
 | 
			
		||||
              menu={[
 | 
			
		||||
                {
 | 
			
		||||
                  node: 'item',
 | 
			
		||||
                  key: 'next',
 | 
			
		||||
@@ -157,7 +214,7 @@ const TokensTable = () => {
 | 
			
		||||
                  name: 'ChatGPT Next Web',
 | 
			
		||||
                  onClick: () => {
 | 
			
		||||
                    onOpenLink('next', record.key);
 | 
			
		||||
                  }
 | 
			
		||||
                  },
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                  node: 'item',
 | 
			
		||||
@@ -166,70 +223,88 @@ const TokensTable = () => {
 | 
			
		||||
                  name: 'ChatGPT Web & Midjourney',
 | 
			
		||||
                  onClick: () => {
 | 
			
		||||
                    onOpenLink('next-mj', record.key);
 | 
			
		||||
                  }
 | 
			
		||||
                  },
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                  node: 'item', key: 'ama', name: 'AMA 问天(BotGem)', onClick: () => {
 | 
			
		||||
                  node: 'item',
 | 
			
		||||
                  key: 'ama',
 | 
			
		||||
                  name: 'AMA 问天(BotGem)',
 | 
			
		||||
                  onClick: () => {
 | 
			
		||||
                    onOpenLink('ama', record.key);
 | 
			
		||||
                  }
 | 
			
		||||
                  },
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                  node: 'item', key: 'opencat', name: 'OpenCat', onClick: () => {
 | 
			
		||||
                  node: 'item',
 | 
			
		||||
                  key: 'opencat',
 | 
			
		||||
                  name: 'OpenCat',
 | 
			
		||||
                  onClick: () => {
 | 
			
		||||
                    onOpenLink('opencat', record.key);
 | 
			
		||||
                  }
 | 
			
		||||
                }
 | 
			
		||||
              ]
 | 
			
		||||
            }
 | 
			
		||||
                  },
 | 
			
		||||
                },
 | 
			
		||||
              ]}
 | 
			
		||||
            >
 | 
			
		||||
              <Button style={{ padding: '8px 4px', color: 'rgba(var(--semi-teal-7), 1)' }} type="primary"
 | 
			
		||||
                      icon={<IconTreeTriangleDown />}></Button>
 | 
			
		||||
              <Button
 | 
			
		||||
                style={{
 | 
			
		||||
                  padding: '8px 4px',
 | 
			
		||||
                  color: 'rgba(var(--semi-teal-7), 1)',
 | 
			
		||||
                }}
 | 
			
		||||
                type='primary'
 | 
			
		||||
                icon={<IconTreeTriangleDown />}
 | 
			
		||||
              ></Button>
 | 
			
		||||
            </Dropdown>
 | 
			
		||||
          </SplitButtonGroup>
 | 
			
		||||
          <Popconfirm
 | 
			
		||||
            title="确定是否要删除此令牌?"
 | 
			
		||||
            content="此修改将不可逆"
 | 
			
		||||
            title='确定是否要删除此令牌?'
 | 
			
		||||
            content='此修改将不可逆'
 | 
			
		||||
            okType={'danger'}
 | 
			
		||||
            position={'left'}
 | 
			
		||||
            onConfirm={() => {
 | 
			
		||||
              manageToken(record.id, 'delete', record).then(
 | 
			
		||||
                () => {
 | 
			
		||||
                  removeRecord(record.key);
 | 
			
		||||
                }
 | 
			
		||||
              );
 | 
			
		||||
              manageToken(record.id, 'delete', record).then(() => {
 | 
			
		||||
                removeRecord(record.key);
 | 
			
		||||
              });
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            <Button theme="light" type="danger" style={{ marginRight: 1 }}>删除</Button>
 | 
			
		||||
            <Button theme='light' type='danger' style={{ marginRight: 1 }}>
 | 
			
		||||
              删除
 | 
			
		||||
            </Button>
 | 
			
		||||
          </Popconfirm>
 | 
			
		||||
          {
 | 
			
		||||
            record.status === 1 ?
 | 
			
		||||
              <Button theme="light" type="warning" style={{ marginRight: 1 }} onClick={
 | 
			
		||||
                async () => {
 | 
			
		||||
                  manageToken(
 | 
			
		||||
                    record.id,
 | 
			
		||||
                    'disable',
 | 
			
		||||
                    record
 | 
			
		||||
                  );
 | 
			
		||||
                }
 | 
			
		||||
              }>禁用</Button> :
 | 
			
		||||
              <Button theme="light" type="secondary" style={{ marginRight: 1 }} onClick={
 | 
			
		||||
                async () => {
 | 
			
		||||
                  manageToken(
 | 
			
		||||
                    record.id,
 | 
			
		||||
                    'enable',
 | 
			
		||||
                    record
 | 
			
		||||
                  );
 | 
			
		||||
                }
 | 
			
		||||
              }>启用</Button>
 | 
			
		||||
          }
 | 
			
		||||
          <Button theme="light" type="tertiary" style={{ marginRight: 1 }} onClick={
 | 
			
		||||
            () => {
 | 
			
		||||
          {record.status === 1 ? (
 | 
			
		||||
            <Button
 | 
			
		||||
              theme='light'
 | 
			
		||||
              type='warning'
 | 
			
		||||
              style={{ marginRight: 1 }}
 | 
			
		||||
              onClick={async () => {
 | 
			
		||||
                manageToken(record.id, 'disable', record);
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              禁用
 | 
			
		||||
            </Button>
 | 
			
		||||
          ) : (
 | 
			
		||||
            <Button
 | 
			
		||||
              theme='light'
 | 
			
		||||
              type='secondary'
 | 
			
		||||
              style={{ marginRight: 1 }}
 | 
			
		||||
              onClick={async () => {
 | 
			
		||||
                manageToken(record.id, 'enable', record);
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              启用
 | 
			
		||||
            </Button>
 | 
			
		||||
          )}
 | 
			
		||||
          <Button
 | 
			
		||||
            theme='light'
 | 
			
		||||
            type='tertiary'
 | 
			
		||||
            style={{ marginRight: 1 }}
 | 
			
		||||
            onClick={() => {
 | 
			
		||||
              setEditingToken(record);
 | 
			
		||||
              setShowEdit(true);
 | 
			
		||||
            }
 | 
			
		||||
          }>编辑</Button>
 | 
			
		||||
            }}
 | 
			
		||||
          >
 | 
			
		||||
            编辑
 | 
			
		||||
          </Button>
 | 
			
		||||
        </div>
 | 
			
		||||
      )
 | 
			
		||||
    }
 | 
			
		||||
      ),
 | 
			
		||||
    },
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE);
 | 
			
		||||
@@ -245,14 +320,14 @@ const TokensTable = () => {
 | 
			
		||||
  const [showTopUpModal, setShowTopUpModal] = useState(false);
 | 
			
		||||
  const [targetTokenIdx, setTargetTokenIdx] = useState(0);
 | 
			
		||||
  const [editingToken, setEditingToken] = useState({
 | 
			
		||||
    id: undefined
 | 
			
		||||
    id: undefined,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const closeEdit = () => {
 | 
			
		||||
    setShowEdit(false);
 | 
			
		||||
    setTimeout(() => {
 | 
			
		||||
      setEditingToken({
 | 
			
		||||
        id: undefined
 | 
			
		||||
        id: undefined,
 | 
			
		||||
      });
 | 
			
		||||
    }, 500);
 | 
			
		||||
  };
 | 
			
		||||
@@ -266,7 +341,10 @@ const TokensTable = () => {
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  let pageData = tokens.slice((activePage - 1) * pageSize, activePage * pageSize);
 | 
			
		||||
  let pageData = tokens.slice(
 | 
			
		||||
    (activePage - 1) * pageSize,
 | 
			
		||||
    activePage * pageSize,
 | 
			
		||||
  );
 | 
			
		||||
  const loadTokens = async (startIdx) => {
 | 
			
		||||
    setLoading(true);
 | 
			
		||||
    const res = await API.get(`/api/token/?p=${startIdx}&size=${pageSize}`);
 | 
			
		||||
@@ -315,7 +393,8 @@ const TokensTable = () => {
 | 
			
		||||
    let nextUrl;
 | 
			
		||||
 | 
			
		||||
    if (nextLink) {
 | 
			
		||||
      nextUrl = nextLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
 | 
			
		||||
      nextUrl =
 | 
			
		||||
        nextLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
 | 
			
		||||
    } else {
 | 
			
		||||
      nextUrl = `https://chat.oneapi.pro/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
 | 
			
		||||
    }
 | 
			
		||||
@@ -323,7 +402,8 @@ const TokensTable = () => {
 | 
			
		||||
    let url;
 | 
			
		||||
    switch (type) {
 | 
			
		||||
      case 'ama':
 | 
			
		||||
        url = mjLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
 | 
			
		||||
        url =
 | 
			
		||||
          mjLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
 | 
			
		||||
        break;
 | 
			
		||||
      case 'opencat':
 | 
			
		||||
        url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`;
 | 
			
		||||
@@ -367,7 +447,8 @@ const TokensTable = () => {
 | 
			
		||||
    let defaultUrl;
 | 
			
		||||
 | 
			
		||||
    if (chatLink) {
 | 
			
		||||
      defaultUrl = chatLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
 | 
			
		||||
      defaultUrl =
 | 
			
		||||
        chatLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
 | 
			
		||||
    }
 | 
			
		||||
    let url;
 | 
			
		||||
    switch (type) {
 | 
			
		||||
@@ -378,7 +459,8 @@ const TokensTable = () => {
 | 
			
		||||
        url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`;
 | 
			
		||||
        break;
 | 
			
		||||
      case 'next-mj':
 | 
			
		||||
        url = mjLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
 | 
			
		||||
        url =
 | 
			
		||||
          mjLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`;
 | 
			
		||||
        break;
 | 
			
		||||
      default:
 | 
			
		||||
        if (!chatLink) {
 | 
			
		||||
@@ -399,10 +481,10 @@ const TokensTable = () => {
 | 
			
		||||
      });
 | 
			
		||||
  }, [pageSize]);
 | 
			
		||||
 | 
			
		||||
  const removeRecord = key => {
 | 
			
		||||
  const removeRecord = (key) => {
 | 
			
		||||
    let newDataSource = [...tokens];
 | 
			
		||||
    if (key != null) {
 | 
			
		||||
      let idx = newDataSource.findIndex(data => data.key === key);
 | 
			
		||||
      let idx = newDataSource.findIndex((data) => data.key === key);
 | 
			
		||||
 | 
			
		||||
      if (idx > -1) {
 | 
			
		||||
        newDataSource.splice(idx, 1);
 | 
			
		||||
@@ -435,7 +517,6 @@ const TokensTable = () => {
 | 
			
		||||
      let newTokens = [...tokens];
 | 
			
		||||
      // let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx;
 | 
			
		||||
      if (action === 'delete') {
 | 
			
		||||
 | 
			
		||||
      } else {
 | 
			
		||||
        record.status = token.status;
 | 
			
		||||
        // newTokens[realIdx].status = token.status;
 | 
			
		||||
@@ -455,7 +536,9 @@ const TokensTable = () => {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    setSearching(true);
 | 
			
		||||
    const res = await API.get(`/api/token/search?keyword=${searchKeyword}&token=${searchToken}`);
 | 
			
		||||
    const res = await API.get(
 | 
			
		||||
      `/api/token/search?keyword=${searchKeyword}&token=${searchToken}`,
 | 
			
		||||
    );
 | 
			
		||||
    const { success, message, data } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      setTokensFormat(data);
 | 
			
		||||
@@ -488,32 +571,28 @@ const TokensTable = () => {
 | 
			
		||||
    setLoading(false);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  const handlePageChange = page => {
 | 
			
		||||
  const handlePageChange = (page) => {
 | 
			
		||||
    setActivePage(page);
 | 
			
		||||
    if (page === Math.ceil(tokens.length / pageSize) + 1) {
 | 
			
		||||
      // In this case we have to load more data and then append them.
 | 
			
		||||
      loadTokens(page - 1).then(r => {
 | 
			
		||||
      });
 | 
			
		||||
      loadTokens(page - 1).then((r) => {});
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const rowSelection = {
 | 
			
		||||
    onSelect: (record, selected) => {
 | 
			
		||||
    },
 | 
			
		||||
    onSelectAll: (selected, selectedRows) => {
 | 
			
		||||
    },
 | 
			
		||||
    onSelect: (record, selected) => {},
 | 
			
		||||
    onSelectAll: (selected, selectedRows) => {},
 | 
			
		||||
    onChange: (selectedRowKeys, selectedRows) => {
 | 
			
		||||
      setSelectedKeys(selectedRows);
 | 
			
		||||
    }
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handleRow = (record, index) => {
 | 
			
		||||
    if (record.status !== 1) {
 | 
			
		||||
      return {
 | 
			
		||||
        style: {
 | 
			
		||||
          background: 'var(--semi-color-disabled-border)'
 | 
			
		||||
        }
 | 
			
		||||
          background: 'var(--semi-color-disabled-border)',
 | 
			
		||||
        },
 | 
			
		||||
      };
 | 
			
		||||
    } else {
 | 
			
		||||
      return {};
 | 
			
		||||
@@ -522,63 +601,98 @@ const TokensTable = () => {
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <EditToken refresh={refresh} editingToken={editingToken} visiable={showEdit} handleClose={closeEdit}></EditToken>
 | 
			
		||||
      <Form layout="horizontal" style={{ marginTop: 10 }} labelPosition={'left'}>
 | 
			
		||||
      <EditToken
 | 
			
		||||
        refresh={refresh}
 | 
			
		||||
        editingToken={editingToken}
 | 
			
		||||
        visiable={showEdit}
 | 
			
		||||
        handleClose={closeEdit}
 | 
			
		||||
      ></EditToken>
 | 
			
		||||
      <Form
 | 
			
		||||
        layout='horizontal'
 | 
			
		||||
        style={{ marginTop: 10 }}
 | 
			
		||||
        labelPosition={'left'}
 | 
			
		||||
      >
 | 
			
		||||
        <Form.Input
 | 
			
		||||
          field="keyword"
 | 
			
		||||
          label="搜索关键字"
 | 
			
		||||
          placeholder="令牌名称"
 | 
			
		||||
          field='keyword'
 | 
			
		||||
          label='搜索关键字'
 | 
			
		||||
          placeholder='令牌名称'
 | 
			
		||||
          value={searchKeyword}
 | 
			
		||||
          loading={searching}
 | 
			
		||||
          onChange={handleKeywordChange}
 | 
			
		||||
        />
 | 
			
		||||
        <Form.Input
 | 
			
		||||
          field="token"
 | 
			
		||||
          label="Key"
 | 
			
		||||
          placeholder="密钥"
 | 
			
		||||
          field='token'
 | 
			
		||||
          label='Key'
 | 
			
		||||
          placeholder='密钥'
 | 
			
		||||
          value={searchToken}
 | 
			
		||||
          loading={searching}
 | 
			
		||||
          onChange={handleSearchTokenChange}
 | 
			
		||||
        />
 | 
			
		||||
        <Button label="查询" type="primary" htmlType="submit" className="btn-margin-right"
 | 
			
		||||
                onClick={searchTokens} style={{ marginRight: 8 }}>查询</Button>
 | 
			
		||||
        <Button
 | 
			
		||||
          label='查询'
 | 
			
		||||
          type='primary'
 | 
			
		||||
          htmlType='submit'
 | 
			
		||||
          className='btn-margin-right'
 | 
			
		||||
          onClick={searchTokens}
 | 
			
		||||
          style={{ marginRight: 8 }}
 | 
			
		||||
        >
 | 
			
		||||
          查询
 | 
			
		||||
        </Button>
 | 
			
		||||
      </Form>
 | 
			
		||||
 | 
			
		||||
      <Table style={{ marginTop: 20 }} columns={columns} dataSource={pageData} pagination={{
 | 
			
		||||
        currentPage: activePage,
 | 
			
		||||
        pageSize: pageSize,
 | 
			
		||||
        total: tokenCount,
 | 
			
		||||
        showSizeChanger: true,
 | 
			
		||||
        pageSizeOptions: [10, 20, 50, 100],
 | 
			
		||||
        formatPageText: (page) => `第 ${page.currentStart} - ${page.currentEnd} 条,共 ${tokens.length} 条`,
 | 
			
		||||
        onPageSizeChange: (size) => {
 | 
			
		||||
          setPageSize(size);
 | 
			
		||||
          setActivePage(1);
 | 
			
		||||
        },
 | 
			
		||||
        onPageChange: handlePageChange
 | 
			
		||||
      }} loading={loading} rowSelection={rowSelection} onRow={handleRow}>
 | 
			
		||||
      </Table>
 | 
			
		||||
      <Button theme="light" type="primary" style={{ marginRight: 8 }} onClick={
 | 
			
		||||
        () => {
 | 
			
		||||
      <Table
 | 
			
		||||
        style={{ marginTop: 20 }}
 | 
			
		||||
        columns={columns}
 | 
			
		||||
        dataSource={pageData}
 | 
			
		||||
        pagination={{
 | 
			
		||||
          currentPage: activePage,
 | 
			
		||||
          pageSize: pageSize,
 | 
			
		||||
          total: tokenCount,
 | 
			
		||||
          showSizeChanger: true,
 | 
			
		||||
          pageSizeOptions: [10, 20, 50, 100],
 | 
			
		||||
          formatPageText: (page) =>
 | 
			
		||||
            `第 ${page.currentStart} - ${page.currentEnd} 条,共 ${tokens.length} 条`,
 | 
			
		||||
          onPageSizeChange: (size) => {
 | 
			
		||||
            setPageSize(size);
 | 
			
		||||
            setActivePage(1);
 | 
			
		||||
          },
 | 
			
		||||
          onPageChange: handlePageChange,
 | 
			
		||||
        }}
 | 
			
		||||
        loading={loading}
 | 
			
		||||
        rowSelection={rowSelection}
 | 
			
		||||
        onRow={handleRow}
 | 
			
		||||
      ></Table>
 | 
			
		||||
      <Button
 | 
			
		||||
        theme='light'
 | 
			
		||||
        type='primary'
 | 
			
		||||
        style={{ marginRight: 8 }}
 | 
			
		||||
        onClick={() => {
 | 
			
		||||
          setEditingToken({
 | 
			
		||||
            id: undefined
 | 
			
		||||
            id: undefined,
 | 
			
		||||
          });
 | 
			
		||||
          setShowEdit(true);
 | 
			
		||||
        }
 | 
			
		||||
      }>添加令牌</Button>
 | 
			
		||||
      <Button label="复制所选令牌" type="warning" onClick={
 | 
			
		||||
        async () => {
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        添加令牌
 | 
			
		||||
      </Button>
 | 
			
		||||
      <Button
 | 
			
		||||
        label='复制所选令牌'
 | 
			
		||||
        type='warning'
 | 
			
		||||
        onClick={async () => {
 | 
			
		||||
          if (selectedKeys.length === 0) {
 | 
			
		||||
            showError('请至少选择一个令牌!');
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
          let keys = '';
 | 
			
		||||
          for (let i = 0; i < selectedKeys.length; i++) {
 | 
			
		||||
            keys += selectedKeys[i].name + '    sk-' + selectedKeys[i].key + '\n';
 | 
			
		||||
            keys +=
 | 
			
		||||
              selectedKeys[i].name + '    sk-' + selectedKeys[i].key + '\n';
 | 
			
		||||
          }
 | 
			
		||||
          await copyText(keys);
 | 
			
		||||
        }
 | 
			
		||||
      }>复制所选令牌到剪贴板</Button>
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        复制所选令牌到剪贴板
 | 
			
		||||
      </Button>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,14 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { API, showError, showSuccess } from '../helpers';
 | 
			
		||||
import { Button, Form, Popconfirm, Space, Table, Tag, Tooltip } from '@douyinfe/semi-ui';
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Form,
 | 
			
		||||
  Popconfirm,
 | 
			
		||||
  Space,
 | 
			
		||||
  Table,
 | 
			
		||||
  Tag,
 | 
			
		||||
  Tooltip,
 | 
			
		||||
} from '@douyinfe/semi-ui';
 | 
			
		||||
import { ITEMS_PER_PAGE } from '../constants';
 | 
			
		||||
import { renderGroup, renderNumber, renderQuota } from '../helpers/render';
 | 
			
		||||
import AddUser from '../pages/User/AddUser';
 | 
			
		||||
@@ -9,138 +17,236 @@ import EditUser from '../pages/User/EditUser';
 | 
			
		||||
function renderRole(role) {
 | 
			
		||||
  switch (role) {
 | 
			
		||||
    case 1:
 | 
			
		||||
      return <Tag size="large">普通用户</Tag>;
 | 
			
		||||
      return <Tag size='large'>普通用户</Tag>;
 | 
			
		||||
    case 10:
 | 
			
		||||
      return <Tag color="yellow" size="large">管理员</Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='yellow' size='large'>
 | 
			
		||||
          管理员
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    case 100:
 | 
			
		||||
      return <Tag color="orange" size="large">超级管理员</Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='orange' size='large'>
 | 
			
		||||
          超级管理员
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
    default:
 | 
			
		||||
      return <Tag color="red" size="large">未知身份</Tag>;
 | 
			
		||||
      return (
 | 
			
		||||
        <Tag color='red' size='large'>
 | 
			
		||||
          未知身份
 | 
			
		||||
        </Tag>
 | 
			
		||||
      );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const UsersTable = () => {
 | 
			
		||||
  const columns = [{
 | 
			
		||||
    title: 'ID', dataIndex: 'id'
 | 
			
		||||
  }, {
 | 
			
		||||
    title: '用户名', dataIndex: 'username'
 | 
			
		||||
  }, {
 | 
			
		||||
    title: '分组', dataIndex: 'group', render: (text, record, index) => {
 | 
			
		||||
      return (<div>
 | 
			
		||||
        {renderGroup(text)}
 | 
			
		||||
      </div>);
 | 
			
		||||
    }
 | 
			
		||||
  }, {
 | 
			
		||||
    title: '统计信息', dataIndex: 'info', render: (text, record, index) => {
 | 
			
		||||
      return (<div>
 | 
			
		||||
        <Space spacing={1}>
 | 
			
		||||
          <Tooltip content={'剩余额度'}>
 | 
			
		||||
            <Tag color="white" size="large">{renderQuota(record.quota)}</Tag>
 | 
			
		||||
          </Tooltip>
 | 
			
		||||
          <Tooltip content={'已用额度'}>
 | 
			
		||||
            <Tag color="white" size="large">{renderQuota(record.used_quota)}</Tag>
 | 
			
		||||
          </Tooltip>
 | 
			
		||||
          <Tooltip content={'调用次数'}>
 | 
			
		||||
            <Tag color="white" size="large">{renderNumber(record.request_count)}</Tag>
 | 
			
		||||
          </Tooltip>
 | 
			
		||||
        </Space>
 | 
			
		||||
      </div>);
 | 
			
		||||
    }
 | 
			
		||||
  }, {
 | 
			
		||||
    title: '邀请信息', dataIndex: 'invite', render: (text, record, index) => {
 | 
			
		||||
      return (<div>
 | 
			
		||||
        <Space spacing={1}>
 | 
			
		||||
          <Tooltip content={'邀请人数'}>
 | 
			
		||||
            <Tag color="white" size="large">{renderNumber(record.aff_count)}</Tag>
 | 
			
		||||
          </Tooltip>
 | 
			
		||||
          <Tooltip content={'邀请总收益'}>
 | 
			
		||||
            <Tag color="white" size="large">{renderQuota(record.aff_history_quota)}</Tag>
 | 
			
		||||
          </Tooltip>
 | 
			
		||||
          <Tooltip content={'邀请人ID'}>
 | 
			
		||||
            {record.inviter_id === 0 ? <Tag color="white" size="large">无</Tag> :
 | 
			
		||||
              <Tag color="white" size="large">{record.inviter_id}</Tag>}
 | 
			
		||||
          </Tooltip>
 | 
			
		||||
        </Space>
 | 
			
		||||
      </div>);
 | 
			
		||||
    }
 | 
			
		||||
  }, {
 | 
			
		||||
    title: '角色', dataIndex: 'role', render: (text, record, index) => {
 | 
			
		||||
      return (<div>
 | 
			
		||||
        {renderRole(text)}
 | 
			
		||||
      </div>);
 | 
			
		||||
    }
 | 
			
		||||
  }, {
 | 
			
		||||
    title: '状态', dataIndex: 'status', render: (text, record, index) => {
 | 
			
		||||
      return (<div>
 | 
			
		||||
        {record.DeletedAt !== null ? <Tag color="red">已注销</Tag> : renderStatus(text)}
 | 
			
		||||
      </div>);
 | 
			
		||||
    }
 | 
			
		||||
  }, {
 | 
			
		||||
    title: '', dataIndex: 'operate', render: (text, record, index) => (<div>
 | 
			
		||||
      {
 | 
			
		||||
        record.DeletedAt !== null ? <></> :
 | 
			
		||||
          <>
 | 
			
		||||
  const columns = [
 | 
			
		||||
    {
 | 
			
		||||
      title: 'ID',
 | 
			
		||||
      dataIndex: 'id',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '用户名',
 | 
			
		||||
      dataIndex: 'username',
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '分组',
 | 
			
		||||
      dataIndex: 'group',
 | 
			
		||||
      render: (text, record, index) => {
 | 
			
		||||
        return <div>{renderGroup(text)}</div>;
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '统计信息',
 | 
			
		||||
      dataIndex: 'info',
 | 
			
		||||
      render: (text, record, index) => {
 | 
			
		||||
        return (
 | 
			
		||||
          <div>
 | 
			
		||||
            <Space spacing={1}>
 | 
			
		||||
              <Tooltip content={'剩余额度'}>
 | 
			
		||||
                <Tag color='white' size='large'>
 | 
			
		||||
                  {renderQuota(record.quota)}
 | 
			
		||||
                </Tag>
 | 
			
		||||
              </Tooltip>
 | 
			
		||||
              <Tooltip content={'已用额度'}>
 | 
			
		||||
                <Tag color='white' size='large'>
 | 
			
		||||
                  {renderQuota(record.used_quota)}
 | 
			
		||||
                </Tag>
 | 
			
		||||
              </Tooltip>
 | 
			
		||||
              <Tooltip content={'调用次数'}>
 | 
			
		||||
                <Tag color='white' size='large'>
 | 
			
		||||
                  {renderNumber(record.request_count)}
 | 
			
		||||
                </Tag>
 | 
			
		||||
              </Tooltip>
 | 
			
		||||
            </Space>
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '邀请信息',
 | 
			
		||||
      dataIndex: 'invite',
 | 
			
		||||
      render: (text, record, index) => {
 | 
			
		||||
        return (
 | 
			
		||||
          <div>
 | 
			
		||||
            <Space spacing={1}>
 | 
			
		||||
              <Tooltip content={'邀请人数'}>
 | 
			
		||||
                <Tag color='white' size='large'>
 | 
			
		||||
                  {renderNumber(record.aff_count)}
 | 
			
		||||
                </Tag>
 | 
			
		||||
              </Tooltip>
 | 
			
		||||
              <Tooltip content={'邀请总收益'}>
 | 
			
		||||
                <Tag color='white' size='large'>
 | 
			
		||||
                  {renderQuota(record.aff_history_quota)}
 | 
			
		||||
                </Tag>
 | 
			
		||||
              </Tooltip>
 | 
			
		||||
              <Tooltip content={'邀请人ID'}>
 | 
			
		||||
                {record.inviter_id === 0 ? (
 | 
			
		||||
                  <Tag color='white' size='large'>
 | 
			
		||||
                    无
 | 
			
		||||
                  </Tag>
 | 
			
		||||
                ) : (
 | 
			
		||||
                  <Tag color='white' size='large'>
 | 
			
		||||
                    {record.inviter_id}
 | 
			
		||||
                  </Tag>
 | 
			
		||||
                )}
 | 
			
		||||
              </Tooltip>
 | 
			
		||||
            </Space>
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '角色',
 | 
			
		||||
      dataIndex: 'role',
 | 
			
		||||
      render: (text, record, index) => {
 | 
			
		||||
        return <div>{renderRole(text)}</div>;
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '状态',
 | 
			
		||||
      dataIndex: 'status',
 | 
			
		||||
      render: (text, record, index) => {
 | 
			
		||||
        return (
 | 
			
		||||
          <div>
 | 
			
		||||
            {record.DeletedAt !== null ? (
 | 
			
		||||
              <Tag color='red'>已注销</Tag>
 | 
			
		||||
            ) : (
 | 
			
		||||
              renderStatus(text)
 | 
			
		||||
            )}
 | 
			
		||||
          </div>
 | 
			
		||||
        );
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    {
 | 
			
		||||
      title: '',
 | 
			
		||||
      dataIndex: 'operate',
 | 
			
		||||
      render: (text, record, index) => (
 | 
			
		||||
        <div>
 | 
			
		||||
          {record.DeletedAt !== null ? (
 | 
			
		||||
            <></>
 | 
			
		||||
          ) : (
 | 
			
		||||
            <>
 | 
			
		||||
              <Popconfirm
 | 
			
		||||
                title='确定?'
 | 
			
		||||
                okType={'warning'}
 | 
			
		||||
                onConfirm={() => {
 | 
			
		||||
                  manageUser(record.username, 'promote', record);
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <Button theme='light' type='warning' style={{ marginRight: 1 }}>
 | 
			
		||||
                  提升
 | 
			
		||||
                </Button>
 | 
			
		||||
              </Popconfirm>
 | 
			
		||||
              <Popconfirm
 | 
			
		||||
                title='确定?'
 | 
			
		||||
                okType={'warning'}
 | 
			
		||||
                onConfirm={() => {
 | 
			
		||||
                  manageUser(record.username, 'demote', record);
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <Button
 | 
			
		||||
                  theme='light'
 | 
			
		||||
                  type='secondary'
 | 
			
		||||
                  style={{ marginRight: 1 }}
 | 
			
		||||
                >
 | 
			
		||||
                  降级
 | 
			
		||||
                </Button>
 | 
			
		||||
              </Popconfirm>
 | 
			
		||||
              {record.status === 1 ? (
 | 
			
		||||
                <Button
 | 
			
		||||
                  theme='light'
 | 
			
		||||
                  type='warning'
 | 
			
		||||
                  style={{ marginRight: 1 }}
 | 
			
		||||
                  onClick={async () => {
 | 
			
		||||
                    manageUser(record.username, 'disable', record);
 | 
			
		||||
                  }}
 | 
			
		||||
                >
 | 
			
		||||
                  禁用
 | 
			
		||||
                </Button>
 | 
			
		||||
              ) : (
 | 
			
		||||
                <Button
 | 
			
		||||
                  theme='light'
 | 
			
		||||
                  type='secondary'
 | 
			
		||||
                  style={{ marginRight: 1 }}
 | 
			
		||||
                  onClick={async () => {
 | 
			
		||||
                    manageUser(record.username, 'enable', record);
 | 
			
		||||
                  }}
 | 
			
		||||
                  disabled={record.status === 3}
 | 
			
		||||
                >
 | 
			
		||||
                  启用
 | 
			
		||||
                </Button>
 | 
			
		||||
              )}
 | 
			
		||||
              <Button
 | 
			
		||||
                theme='light'
 | 
			
		||||
                type='tertiary'
 | 
			
		||||
                style={{ marginRight: 1 }}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  setEditingUser(record);
 | 
			
		||||
                  setShowEditUser(true);
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                编辑
 | 
			
		||||
              </Button>
 | 
			
		||||
            </>
 | 
			
		||||
          )}
 | 
			
		||||
          {record.DeletedAt !== null ? (
 | 
			
		||||
            <Popconfirm
 | 
			
		||||
              title="确定?"
 | 
			
		||||
              okType={'warning'}
 | 
			
		||||
              title='确定是否要删除此用户?'
 | 
			
		||||
              content='硬删除,此修改将不可逆'
 | 
			
		||||
              okType={'danger'}
 | 
			
		||||
              position={'left'}
 | 
			
		||||
              onConfirm={() => {
 | 
			
		||||
                manageUser(record.username, 'promote', record);
 | 
			
		||||
                hardDeleteUser(record.id).then(() => {
 | 
			
		||||
                  removeRecord(record.id);
 | 
			
		||||
                });
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <Button theme="light" type="warning" style={{ marginRight: 1 }}>提升</Button>
 | 
			
		||||
              <Button theme='light' type='danger' style={{ marginRight: 1 }}>
 | 
			
		||||
                永久删除
 | 
			
		||||
              </Button>
 | 
			
		||||
            </Popconfirm>
 | 
			
		||||
          ) : (
 | 
			
		||||
            <Popconfirm
 | 
			
		||||
              title="确定?"
 | 
			
		||||
              okType={'warning'}
 | 
			
		||||
              title='确定是否要删除此用户?'
 | 
			
		||||
              content='软删除,数据依然留底'
 | 
			
		||||
              okType={'danger'}
 | 
			
		||||
              position={'left'}
 | 
			
		||||
              onConfirm={() => {
 | 
			
		||||
                manageUser(record.username, 'demote', record);
 | 
			
		||||
                manageUser(record.username, 'delete', record).then(() => {
 | 
			
		||||
                  record.DeletedAt = new Date();
 | 
			
		||||
                });
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              <Button theme="light" type="secondary" style={{ marginRight: 1 }}>降级</Button>
 | 
			
		||||
              <Button theme='light' type='danger' style={{ marginRight: 1 }}>
 | 
			
		||||
                删除
 | 
			
		||||
              </Button>
 | 
			
		||||
            </Popconfirm>
 | 
			
		||||
            {record.status === 1 ?
 | 
			
		||||
              <Button theme="light" type="warning" style={{ marginRight: 1 }} onClick={async () => {
 | 
			
		||||
                manageUser(record.username, 'disable', record);
 | 
			
		||||
              }}>禁用</Button> :
 | 
			
		||||
              <Button theme="light" type="secondary" style={{ marginRight: 1 }} onClick={async () => {
 | 
			
		||||
                manageUser(record.username, 'enable', record);
 | 
			
		||||
              }} disabled={record.status === 3}>启用</Button>}
 | 
			
		||||
            <Button theme="light" type="tertiary" style={{ marginRight: 1 }} onClick={() => {
 | 
			
		||||
              setEditingUser(record);
 | 
			
		||||
              setShowEditUser(true);
 | 
			
		||||
            }}>编辑</Button>
 | 
			
		||||
          </>
 | 
			
		||||
 | 
			
		||||
      }
 | 
			
		||||
      {
 | 
			
		||||
        record.DeletedAt !== null ? <Popconfirm
 | 
			
		||||
            title="确定是否要删除此用户?"
 | 
			
		||||
            content="硬删除,此修改将不可逆"
 | 
			
		||||
            okType={'danger'}
 | 
			
		||||
            position={'left'}
 | 
			
		||||
            onConfirm={() => {
 | 
			
		||||
              hardDeleteUser(record.id).then(() => {
 | 
			
		||||
                removeRecord(record.id);
 | 
			
		||||
              });
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
          <Button theme="light" type="danger" style={{ marginRight: 1 }}>永久删除</Button>
 | 
			
		||||
        </Popconfirm> : <Popconfirm
 | 
			
		||||
            title="确定是否要删除此用户?"
 | 
			
		||||
            content="软删除,数据依然留底"
 | 
			
		||||
            okType={'danger'}
 | 
			
		||||
            position={'left'}
 | 
			
		||||
            onConfirm={() => {
 | 
			
		||||
              manageUser(record.username, 'delete', record).then(() => {
 | 
			
		||||
                record.DeletedAt = new Date();
 | 
			
		||||
              });
 | 
			
		||||
            }}
 | 
			
		||||
        >
 | 
			
		||||
          <Button theme="light" type="danger" style={{ marginRight: 1 }}>删除</Button>
 | 
			
		||||
        </Popconfirm>
 | 
			
		||||
      }
 | 
			
		||||
    </div>)
 | 
			
		||||
  }];
 | 
			
		||||
          )}
 | 
			
		||||
        </div>
 | 
			
		||||
      ),
 | 
			
		||||
    },
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
  const [users, setUsers] = useState([]);
 | 
			
		||||
  const [loading, setLoading] = useState(true);
 | 
			
		||||
@@ -151,22 +257,22 @@ const UsersTable = () => {
 | 
			
		||||
  const [showAddUser, setShowAddUser] = useState(false);
 | 
			
		||||
  const [showEditUser, setShowEditUser] = useState(false);
 | 
			
		||||
  const [editingUser, setEditingUser] = useState({
 | 
			
		||||
    id: undefined
 | 
			
		||||
    id: undefined,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  const setCount = (data) => {
 | 
			
		||||
    if (data.length >= (activePage) * ITEMS_PER_PAGE) {
 | 
			
		||||
    if (data.length >= activePage * ITEMS_PER_PAGE) {
 | 
			
		||||
      setUserCount(data.length + 1);
 | 
			
		||||
    } else {
 | 
			
		||||
      setUserCount(data.length);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const removeRecord = key => {
 | 
			
		||||
  const removeRecord = (key) => {
 | 
			
		||||
    console.log(key);
 | 
			
		||||
    let newDataSource = [...users];
 | 
			
		||||
    if (key != null) {
 | 
			
		||||
      let idx = newDataSource.findIndex(data => data.id === key);
 | 
			
		||||
      let idx = newDataSource.findIndex((data) => data.id === key);
 | 
			
		||||
 | 
			
		||||
      if (idx > -1) {
 | 
			
		||||
        newDataSource.splice(idx, 1);
 | 
			
		||||
@@ -214,7 +320,8 @@ const UsersTable = () => {
 | 
			
		||||
 | 
			
		||||
  const manageUser = async (username, action, record) => {
 | 
			
		||||
    const res = await API.post('/api/user/manage', {
 | 
			
		||||
      username, action
 | 
			
		||||
      username,
 | 
			
		||||
      action,
 | 
			
		||||
    });
 | 
			
		||||
    const { success, message } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
@@ -222,7 +329,6 @@ const UsersTable = () => {
 | 
			
		||||
      let user = res.data.data;
 | 
			
		||||
      let newUsers = [...users];
 | 
			
		||||
      if (action === 'delete') {
 | 
			
		||||
 | 
			
		||||
      } else {
 | 
			
		||||
        record.status = user.status;
 | 
			
		||||
        record.role = user.role;
 | 
			
		||||
@@ -243,20 +349,24 @@ const UsersTable = () => {
 | 
			
		||||
      showError(message);
 | 
			
		||||
      throw new Error(message);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const renderStatus = (status) => {
 | 
			
		||||
    switch (status) {
 | 
			
		||||
      case 1:
 | 
			
		||||
        return <Tag size="large">已激活</Tag>;
 | 
			
		||||
        return <Tag size='large'>已激活</Tag>;
 | 
			
		||||
      case 2:
 | 
			
		||||
        return (<Tag size="large" color="red">
 | 
			
		||||
          已封禁
 | 
			
		||||
        </Tag>);
 | 
			
		||||
        return (
 | 
			
		||||
          <Tag size='large' color='red'>
 | 
			
		||||
            已封禁
 | 
			
		||||
          </Tag>
 | 
			
		||||
        );
 | 
			
		||||
      default:
 | 
			
		||||
        return (<Tag size="large" color="grey">
 | 
			
		||||
          未知状态
 | 
			
		||||
        </Tag>);
 | 
			
		||||
        return (
 | 
			
		||||
          <Tag size='large' color='grey'>
 | 
			
		||||
            未知状态
 | 
			
		||||
          </Tag>
 | 
			
		||||
        );
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@@ -297,16 +407,18 @@ const UsersTable = () => {
 | 
			
		||||
    setLoading(false);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const handlePageChange = page => {
 | 
			
		||||
  const handlePageChange = (page) => {
 | 
			
		||||
    setActivePage(page);
 | 
			
		||||
    if (page === Math.ceil(users.length / ITEMS_PER_PAGE) + 1) {
 | 
			
		||||
      // In this case we have to load more data and then append them.
 | 
			
		||||
      loadUsers(page - 1).then(r => {
 | 
			
		||||
      });
 | 
			
		||||
      loadUsers(page - 1).then((r) => {});
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const pageData = users.slice((activePage - 1) * ITEMS_PER_PAGE, activePage * ITEMS_PER_PAGE);
 | 
			
		||||
  const pageData = users.slice(
 | 
			
		||||
    (activePage - 1) * ITEMS_PER_PAGE,
 | 
			
		||||
    activePage * ITEMS_PER_PAGE,
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const closeAddUser = () => {
 | 
			
		||||
    setShowAddUser(false);
 | 
			
		||||
@@ -315,7 +427,7 @@ const UsersTable = () => {
 | 
			
		||||
  const closeEditUser = () => {
 | 
			
		||||
    setShowEditUser(false);
 | 
			
		||||
    setEditingUser({
 | 
			
		||||
      id: undefined
 | 
			
		||||
      id: undefined,
 | 
			
		||||
    });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
@@ -329,34 +441,52 @@ const UsersTable = () => {
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <AddUser refresh={refresh} visible={showAddUser} handleClose={closeAddUser}></AddUser>
 | 
			
		||||
      <EditUser refresh={refresh} visible={showEditUser} handleClose={closeEditUser}
 | 
			
		||||
                editingUser={editingUser}></EditUser>
 | 
			
		||||
      <AddUser
 | 
			
		||||
        refresh={refresh}
 | 
			
		||||
        visible={showAddUser}
 | 
			
		||||
        handleClose={closeAddUser}
 | 
			
		||||
      ></AddUser>
 | 
			
		||||
      <EditUser
 | 
			
		||||
        refresh={refresh}
 | 
			
		||||
        visible={showEditUser}
 | 
			
		||||
        handleClose={closeEditUser}
 | 
			
		||||
        editingUser={editingUser}
 | 
			
		||||
      ></EditUser>
 | 
			
		||||
      <Form onSubmit={searchUsers}>
 | 
			
		||||
        <Form.Input
 | 
			
		||||
          label="搜索关键字"
 | 
			
		||||
          icon="search"
 | 
			
		||||
          field="keyword"
 | 
			
		||||
          iconPosition="left"
 | 
			
		||||
          placeholder="搜索用户的 ID,用户名,显示名称,以及邮箱地址 ..."
 | 
			
		||||
          label='搜索关键字'
 | 
			
		||||
          icon='search'
 | 
			
		||||
          field='keyword'
 | 
			
		||||
          iconPosition='left'
 | 
			
		||||
          placeholder='搜索用户的 ID,用户名,显示名称,以及邮箱地址 ...'
 | 
			
		||||
          value={searchKeyword}
 | 
			
		||||
          loading={searching}
 | 
			
		||||
          onChange={value => handleKeywordChange(value)}
 | 
			
		||||
          onChange={(value) => handleKeywordChange(value)}
 | 
			
		||||
        />
 | 
			
		||||
      </Form>
 | 
			
		||||
 | 
			
		||||
      <Table columns={columns} dataSource={pageData} pagination={{
 | 
			
		||||
        currentPage: activePage,
 | 
			
		||||
        pageSize: ITEMS_PER_PAGE,
 | 
			
		||||
        total: userCount,
 | 
			
		||||
        pageSizeOpts: [10, 20, 50, 100],
 | 
			
		||||
        onPageChange: handlePageChange
 | 
			
		||||
      }} loading={loading} />
 | 
			
		||||
      <Button theme="light" type="primary" style={{ marginRight: 8 }} onClick={
 | 
			
		||||
        () => {
 | 
			
		||||
      <Table
 | 
			
		||||
        columns={columns}
 | 
			
		||||
        dataSource={pageData}
 | 
			
		||||
        pagination={{
 | 
			
		||||
          currentPage: activePage,
 | 
			
		||||
          pageSize: ITEMS_PER_PAGE,
 | 
			
		||||
          total: userCount,
 | 
			
		||||
          pageSizeOpts: [10, 20, 50, 100],
 | 
			
		||||
          onPageChange: handlePageChange,
 | 
			
		||||
        }}
 | 
			
		||||
        loading={loading}
 | 
			
		||||
      />
 | 
			
		||||
      <Button
 | 
			
		||||
        theme='light'
 | 
			
		||||
        type='primary'
 | 
			
		||||
        style={{ marginRight: 8 }}
 | 
			
		||||
        onClick={() => {
 | 
			
		||||
          setShowAddUser(true);
 | 
			
		||||
        }
 | 
			
		||||
      }>添加用户</Button>
 | 
			
		||||
        }}
 | 
			
		||||
      >
 | 
			
		||||
        添加用户
 | 
			
		||||
      </Button>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -3,15 +3,27 @@ import { Icon } from '@douyinfe/semi-ui';
 | 
			
		||||
 | 
			
		||||
const WeChatIcon = () => {
 | 
			
		||||
  function CustomIcon() {
 | 
			
		||||
    return <svg t="1709714447384" className="icon" viewBox="0 0 1024 1024" version="1.1"
 | 
			
		||||
                xmlns="http://www.w3.org/2000/svg" p-id="5091" width="16" height="16">
 | 
			
		||||
      <path
 | 
			
		||||
        d="M690.1 377.4c5.9 0 11.8 0.2 17.6 0.5-24.4-128.7-158.3-227.1-319.9-227.1C209 150.8 64 271.4 64 420.2c0 81.1 43.6 154.2 111.9 203.6 5.5 3.9 9.1 10.3 9.1 17.6 0 2.4-0.5 4.6-1.1 6.9-5.5 20.3-14.2 52.8-14.6 54.3-0.7 2.6-1.7 5.2-1.7 7.9 0 5.9 4.8 10.8 10.8 10.8 2.3 0 4.2-0.9 6.2-2l70.9-40.9c5.3-3.1 11-5 17.2-5 3.2 0 6.4 0.5 9.5 1.4 33.1 9.5 68.8 14.8 105.7 14.8 6 0 11.9-0.1 17.8-0.4-7.1-21-10.9-43.1-10.9-66 0-135.8 132.2-245.8 295.3-245.8z m-194.3-86.5c23.8 0 43.2 19.3 43.2 43.1s-19.3 43.1-43.2 43.1c-23.8 0-43.2-19.3-43.2-43.1s19.4-43.1 43.2-43.1z m-215.9 86.2c-23.8 0-43.2-19.3-43.2-43.1s19.3-43.1 43.2-43.1 43.2 19.3 43.2 43.1-19.4 43.1-43.2 43.1z"
 | 
			
		||||
        p-id="5092"></path>
 | 
			
		||||
      <path
 | 
			
		||||
        d="M866.7 792.7c56.9-41.2 93.2-102 93.2-169.7 0-124-120.8-224.5-269.9-224.5-149 0-269.9 100.5-269.9 224.5S540.9 847.5 690 847.5c30.8 0 60.6-4.4 88.1-12.3 2.6-0.8 5.2-1.2 7.9-1.2 5.2 0 9.9 1.6 14.3 4.1l59.1 34c1.7 1 3.3 1.7 5.2 1.7 2.4 0 4.7-0.9 6.4-2.6 1.7-1.7 2.6-4 2.6-6.4 0-2.2-0.9-4.4-1.4-6.6-0.3-1.2-7.6-28.3-12.2-45.3-0.5-1.9-0.9-3.8-0.9-5.7 0.1-5.9 3.1-11.2 7.6-14.5zM600.2 587.2c-19.9 0-36-16.1-36-35.9 0-19.8 16.1-35.9 36-35.9s36 16.1 36 35.9c0 19.8-16.2 35.9-36 35.9z m179.9 0c-19.9 0-36-16.1-36-35.9 0-19.8 16.1-35.9 36-35.9s36 16.1 36 35.9c-0.1 19.8-16.2 35.9-36 35.9z"
 | 
			
		||||
        p-id="5093"></path>
 | 
			
		||||
    </svg>;
 | 
			
		||||
    return (
 | 
			
		||||
      <svg
 | 
			
		||||
        t='1709714447384'
 | 
			
		||||
        className='icon'
 | 
			
		||||
        viewBox='0 0 1024 1024'
 | 
			
		||||
        version='1.1'
 | 
			
		||||
        xmlns='http://www.w3.org/2000/svg'
 | 
			
		||||
        p-id='5091'
 | 
			
		||||
        width='16'
 | 
			
		||||
        height='16'
 | 
			
		||||
      >
 | 
			
		||||
        <path
 | 
			
		||||
          d='M690.1 377.4c5.9 0 11.8 0.2 17.6 0.5-24.4-128.7-158.3-227.1-319.9-227.1C209 150.8 64 271.4 64 420.2c0 81.1 43.6 154.2 111.9 203.6 5.5 3.9 9.1 10.3 9.1 17.6 0 2.4-0.5 4.6-1.1 6.9-5.5 20.3-14.2 52.8-14.6 54.3-0.7 2.6-1.7 5.2-1.7 7.9 0 5.9 4.8 10.8 10.8 10.8 2.3 0 4.2-0.9 6.2-2l70.9-40.9c5.3-3.1 11-5 17.2-5 3.2 0 6.4 0.5 9.5 1.4 33.1 9.5 68.8 14.8 105.7 14.8 6 0 11.9-0.1 17.8-0.4-7.1-21-10.9-43.1-10.9-66 0-135.8 132.2-245.8 295.3-245.8z m-194.3-86.5c23.8 0 43.2 19.3 43.2 43.1s-19.3 43.1-43.2 43.1c-23.8 0-43.2-19.3-43.2-43.1s19.4-43.1 43.2-43.1z m-215.9 86.2c-23.8 0-43.2-19.3-43.2-43.1s19.3-43.1 43.2-43.1 43.2 19.3 43.2 43.1-19.4 43.1-43.2 43.1z'
 | 
			
		||||
          p-id='5092'
 | 
			
		||||
        ></path>
 | 
			
		||||
        <path
 | 
			
		||||
          d='M866.7 792.7c56.9-41.2 93.2-102 93.2-169.7 0-124-120.8-224.5-269.9-224.5-149 0-269.9 100.5-269.9 224.5S540.9 847.5 690 847.5c30.8 0 60.6-4.4 88.1-12.3 2.6-0.8 5.2-1.2 7.9-1.2 5.2 0 9.9 1.6 14.3 4.1l59.1 34c1.7 1 3.3 1.7 5.2 1.7 2.4 0 4.7-0.9 6.4-2.6 1.7-1.7 2.6-4 2.6-6.4 0-2.2-0.9-4.4-1.4-6.6-0.3-1.2-7.6-28.3-12.2-45.3-0.5-1.9-0.9-3.8-0.9-5.7 0.1-5.9 3.1-11.2 7.6-14.5zM600.2 587.2c-19.9 0-36-16.1-36-35.9 0-19.8 16.1-35.9 36-35.9s36 16.1 36 35.9c0 19.8-16.2 35.9-36 35.9z m179.9 0c-19.9 0-36-16.1-36-35.9 0-19.8 16.1-35.9 36-35.9s36 16.1 36 35.9c-0.1 19.8-16.2 35.9-36 35.9z'
 | 
			
		||||
          p-id='5093'
 | 
			
		||||
        ></path>
 | 
			
		||||
      </svg>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
 
 | 
			
		||||
@@ -14,11 +14,11 @@ export async function getOAuthState() {
 | 
			
		||||
export async function onGitHubOAuthClicked(github_client_id) {
 | 
			
		||||
  const state = await getOAuthState();
 | 
			
		||||
  if (!state) return;
 | 
			
		||||
  location.href = `https://github.com/login/oauth/authorize?client_id=${github_client_id}&state=${state}&scope=user:email`
 | 
			
		||||
  location.href = `https://github.com/login/oauth/authorize?client_id=${github_client_id}&state=${state}&scope=user:email`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function onLinuxDoOAuthClicked(linuxdo_client_id) {
 | 
			
		||||
  const state = await getOAuthState();
 | 
			
		||||
  if (!state) return;
 | 
			
		||||
  location.href = `https://connect.linux.do/oauth2/authorize?client_id=${linuxdo_client_id}&response_type=code&state=${state}&scope=user:profile`;
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,21 +1,100 @@
 | 
			
		||||
export const CHANNEL_OPTIONS = [
 | 
			
		||||
    {key: 1, text: 'OpenAI', value: 1, color: 'green', label: 'OpenAI'},
 | 
			
		||||
    {key: 2, text: 'Midjourney Proxy', value: 2, color: 'light-blue', label: 'Midjourney Proxy'},
 | 
			
		||||
    {key: 5, text: 'Midjourney Proxy Plus', value: 5, color: 'blue', label: 'Midjourney Proxy Plus'},
 | 
			
		||||
    {key: 4, text: 'Ollama', value: 4, color: 'grey', label: 'Ollama'},
 | 
			
		||||
    {key: 14, text: 'Anthropic Claude', value: 14, color: 'indigo', label: 'Anthropic Claude'},
 | 
			
		||||
    {key: 3, text: 'Azure OpenAI', value: 3, color: 'teal', label: 'Azure OpenAI'},
 | 
			
		||||
    {key: 11, text: 'Google PaLM2', value: 11, color: 'orange', label: 'Google PaLM2'},
 | 
			
		||||
    {key: 24, text: 'Google Gemini', value: 24, color: 'orange', label: 'Google Gemini'},
 | 
			
		||||
    {key: 15, text: '百度文心千帆', value: 15, color: 'blue', label: '百度文心千帆'},
 | 
			
		||||
    {key: 17, text: '阿里通义千问', value: 17, color: 'orange', label: '阿里通义千问'},
 | 
			
		||||
    {key: 18, text: '讯飞星火认知', value: 18, color: 'blue', label: '讯飞星火认知'},
 | 
			
		||||
    {key: 16, text: '智谱 ChatGLM', value: 16, color: 'violet', label: '智谱 ChatGLM'},
 | 
			
		||||
    {key: 16, text: '智谱 GLM-4V', value: 26, color: 'purple', label: '智谱 GLM-4V'},
 | 
			
		||||
    {key: 16, text: 'Moonshot', value: 25, color: 'green', label: 'Moonshot'},
 | 
			
		||||
    {key: 19, text: '360 智脑', value: 19, color: 'blue', label: '360 智脑'},
 | 
			
		||||
    {key: 23, text: '腾讯混元', value: 23, color: 'teal', label: '腾讯混元'},
 | 
			
		||||
    {key: 8, text: '自定义渠道', value: 8, color: 'pink', label: '自定义渠道'},
 | 
			
		||||
    {key: 22, text: '知识库:FastGPT', value: 22, color: 'blue', label: '知识库:FastGPT'},
 | 
			
		||||
    {key: 21, text: '知识库:AI Proxy', value: 21, color: 'purple', label: '知识库:AI Proxy'},
 | 
			
		||||
  { key: 1, text: 'OpenAI', value: 1, color: 'green', label: 'OpenAI' },
 | 
			
		||||
  {
 | 
			
		||||
    key: 2,
 | 
			
		||||
    text: 'Midjourney Proxy',
 | 
			
		||||
    value: 2,
 | 
			
		||||
    color: 'light-blue',
 | 
			
		||||
    label: 'Midjourney Proxy',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    key: 5,
 | 
			
		||||
    text: 'Midjourney Proxy Plus',
 | 
			
		||||
    value: 5,
 | 
			
		||||
    color: 'blue',
 | 
			
		||||
    label: 'Midjourney Proxy Plus',
 | 
			
		||||
  },
 | 
			
		||||
  { key: 4, text: 'Ollama', value: 4, color: 'grey', label: 'Ollama' },
 | 
			
		||||
  {
 | 
			
		||||
    key: 14,
 | 
			
		||||
    text: 'Anthropic Claude',
 | 
			
		||||
    value: 14,
 | 
			
		||||
    color: 'indigo',
 | 
			
		||||
    label: 'Anthropic Claude',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    key: 3,
 | 
			
		||||
    text: 'Azure OpenAI',
 | 
			
		||||
    value: 3,
 | 
			
		||||
    color: 'teal',
 | 
			
		||||
    label: 'Azure OpenAI',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    key: 11,
 | 
			
		||||
    text: 'Google PaLM2',
 | 
			
		||||
    value: 11,
 | 
			
		||||
    color: 'orange',
 | 
			
		||||
    label: 'Google PaLM2',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    key: 24,
 | 
			
		||||
    text: 'Google Gemini',
 | 
			
		||||
    value: 24,
 | 
			
		||||
    color: 'orange',
 | 
			
		||||
    label: 'Google Gemini',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    key: 15,
 | 
			
		||||
    text: '百度文心千帆',
 | 
			
		||||
    value: 15,
 | 
			
		||||
    color: 'blue',
 | 
			
		||||
    label: '百度文心千帆',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    key: 17,
 | 
			
		||||
    text: '阿里通义千问',
 | 
			
		||||
    value: 17,
 | 
			
		||||
    color: 'orange',
 | 
			
		||||
    label: '阿里通义千问',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    key: 18,
 | 
			
		||||
    text: '讯飞星火认知',
 | 
			
		||||
    value: 18,
 | 
			
		||||
    color: 'blue',
 | 
			
		||||
    label: '讯飞星火认知',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    key: 16,
 | 
			
		||||
    text: '智谱 ChatGLM',
 | 
			
		||||
    value: 16,
 | 
			
		||||
    color: 'violet',
 | 
			
		||||
    label: '智谱 ChatGLM',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    key: 16,
 | 
			
		||||
    text: '智谱 GLM-4V',
 | 
			
		||||
    value: 26,
 | 
			
		||||
    color: 'purple',
 | 
			
		||||
    label: '智谱 GLM-4V',
 | 
			
		||||
  },
 | 
			
		||||
  { key: 16, text: 'Moonshot', value: 25, color: 'green', label: 'Moonshot' },
 | 
			
		||||
  { key: 19, text: '360 智脑', value: 19, color: 'blue', label: '360 智脑' },
 | 
			
		||||
  { key: 23, text: '腾讯混元', value: 23, color: 'teal', label: '腾讯混元' },
 | 
			
		||||
  { key: 31, text: '零一万物', value: 31, color: 'green', label: '零一万物' },
 | 
			
		||||
  { key: 8, text: '自定义渠道', value: 8, color: 'pink', label: '自定义渠道' },
 | 
			
		||||
  {
 | 
			
		||||
    key: 22,
 | 
			
		||||
    text: '知识库:FastGPT',
 | 
			
		||||
    value: 22,
 | 
			
		||||
    color: 'blue',
 | 
			
		||||
    label: '知识库:FastGPT',
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    key: 21,
 | 
			
		||||
    text: '知识库:AI Proxy',
 | 
			
		||||
    value: 21,
 | 
			
		||||
    color: 'purple',
 | 
			
		||||
    label: '知识库:AI Proxy',
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
export * from './toast.constants';
 | 
			
		||||
export * from './user.constants';
 | 
			
		||||
export * from './common.constant';
 | 
			
		||||
export * from './channel.constants';
 | 
			
		||||
export * from './channel.constants';
 | 
			
		||||
 
 | 
			
		||||
@@ -3,5 +3,5 @@ export const toastConstants = {
 | 
			
		||||
  INFO_TIMEOUT: 3000,
 | 
			
		||||
  ERROR_TIMEOUT: 5000,
 | 
			
		||||
  WARNING_TIMEOUT: 10000,
 | 
			
		||||
  NOTICE_TIMEOUT: 20000
 | 
			
		||||
  NOTICE_TIMEOUT: 20000,
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,19 @@
 | 
			
		||||
export const userConstants = {
 | 
			
		||||
    REGISTER_REQUEST: 'USERS_REGISTER_REQUEST',
 | 
			
		||||
    REGISTER_SUCCESS: 'USERS_REGISTER_SUCCESS',
 | 
			
		||||
    REGISTER_FAILURE: 'USERS_REGISTER_FAILURE',
 | 
			
		||||
  REGISTER_REQUEST: 'USERS_REGISTER_REQUEST',
 | 
			
		||||
  REGISTER_SUCCESS: 'USERS_REGISTER_SUCCESS',
 | 
			
		||||
  REGISTER_FAILURE: 'USERS_REGISTER_FAILURE',
 | 
			
		||||
 | 
			
		||||
    LOGIN_REQUEST: 'USERS_LOGIN_REQUEST',
 | 
			
		||||
    LOGIN_SUCCESS: 'USERS_LOGIN_SUCCESS',
 | 
			
		||||
    LOGIN_FAILURE: 'USERS_LOGIN_FAILURE',
 | 
			
		||||
    
 | 
			
		||||
    LOGOUT: 'USERS_LOGOUT',
 | 
			
		||||
  LOGIN_REQUEST: 'USERS_LOGIN_REQUEST',
 | 
			
		||||
  LOGIN_SUCCESS: 'USERS_LOGIN_SUCCESS',
 | 
			
		||||
  LOGIN_FAILURE: 'USERS_LOGIN_FAILURE',
 | 
			
		||||
 | 
			
		||||
    GETALL_REQUEST: 'USERS_GETALL_REQUEST',
 | 
			
		||||
    GETALL_SUCCESS: 'USERS_GETALL_SUCCESS',
 | 
			
		||||
    GETALL_FAILURE: 'USERS_GETALL_FAILURE',
 | 
			
		||||
  LOGOUT: 'USERS_LOGOUT',
 | 
			
		||||
 | 
			
		||||
    DELETE_REQUEST: 'USERS_DELETE_REQUEST',
 | 
			
		||||
    DELETE_SUCCESS: 'USERS_DELETE_SUCCESS',
 | 
			
		||||
    DELETE_FAILURE: 'USERS_DELETE_FAILURE'    
 | 
			
		||||
  GETALL_REQUEST: 'USERS_GETALL_REQUEST',
 | 
			
		||||
  GETALL_SUCCESS: 'USERS_GETALL_SUCCESS',
 | 
			
		||||
  GETALL_FAILURE: 'USERS_GETALL_FAILURE',
 | 
			
		||||
 | 
			
		||||
  DELETE_REQUEST: 'USERS_DELETE_REQUEST',
 | 
			
		||||
  DELETE_SUCCESS: 'USERS_DELETE_SUCCESS',
 | 
			
		||||
  DELETE_FAILURE: 'USERS_DELETE_FAILURE',
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -16,4 +16,4 @@ export const StatusProvider = ({ children }) => {
 | 
			
		||||
      {children}
 | 
			
		||||
    </StatusContext.Provider>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,19 @@
 | 
			
		||||
// contexts/User/index.jsx
 | 
			
		||||
 | 
			
		||||
import React from "react"
 | 
			
		||||
import { reducer, initialState } from "./reducer"
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import { reducer, initialState } from './reducer';
 | 
			
		||||
 | 
			
		||||
export const UserContext = React.createContext({
 | 
			
		||||
  state: initialState,
 | 
			
		||||
  dispatch: () => null
 | 
			
		||||
})
 | 
			
		||||
  dispatch: () => null,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
export const UserProvider = ({ children }) => {
 | 
			
		||||
  const [state, dispatch] = React.useReducer(reducer, initialState)
 | 
			
		||||
  const [state, dispatch] = React.useReducer(reducer, initialState);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <UserContext.Provider value={[ state, dispatch ]}>
 | 
			
		||||
      { children }
 | 
			
		||||
    <UserContext.Provider value={[state, dispatch]}>
 | 
			
		||||
      {children}
 | 
			
		||||
    </UserContext.Provider>
 | 
			
		||||
  )
 | 
			
		||||
}
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -3,12 +3,12 @@ export const reducer = (state, action) => {
 | 
			
		||||
    case 'login':
 | 
			
		||||
      return {
 | 
			
		||||
        ...state,
 | 
			
		||||
        user: action.payload
 | 
			
		||||
        user: action.payload,
 | 
			
		||||
      };
 | 
			
		||||
    case 'logout':
 | 
			
		||||
      return {
 | 
			
		||||
        ...state,
 | 
			
		||||
        user: undefined
 | 
			
		||||
        user: undefined,
 | 
			
		||||
      };
 | 
			
		||||
 | 
			
		||||
    default:
 | 
			
		||||
@@ -17,5 +17,5 @@ export const reducer = (state, action) => {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const initialState = {
 | 
			
		||||
  user: undefined
 | 
			
		||||
};
 | 
			
		||||
  user: undefined,
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -2,12 +2,14 @@ import { showError } from './utils';
 | 
			
		||||
import axios from 'axios';
 | 
			
		||||
 | 
			
		||||
export const API = axios.create({
 | 
			
		||||
  baseURL: process.env.REACT_APP_SERVER ? process.env.REACT_APP_SERVER : '',
 | 
			
		||||
  baseURL: import.meta.env.VITE_REACT_APP_SERVER_URL
 | 
			
		||||
    ? import.meta.env.VITE_REACT_APP_SERVER_URL
 | 
			
		||||
    : '',
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
API.interceptors.response.use(
 | 
			
		||||
  (response) => response,
 | 
			
		||||
  (error) => {
 | 
			
		||||
    showError(error);
 | 
			
		||||
  }
 | 
			
		||||
  },
 | 
			
		||||
);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,10 @@
 | 
			
		||||
export function authHeader() {
 | 
			
		||||
    // return authorization header with jwt token
 | 
			
		||||
    let user = JSON.parse(localStorage.getItem('user'));
 | 
			
		||||
  // return authorization header with jwt token
 | 
			
		||||
  let user = JSON.parse(localStorage.getItem('user'));
 | 
			
		||||
 | 
			
		||||
    if (user && user.token) {
 | 
			
		||||
        return { 'Authorization': 'Bearer ' + user.token };
 | 
			
		||||
    } else {
 | 
			
		||||
        return {};
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
  if (user && user.token) {
 | 
			
		||||
    return { Authorization: 'Bearer ' + user.token };
 | 
			
		||||
  } else {
 | 
			
		||||
    return {};
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,3 @@
 | 
			
		||||
import { createBrowserHistory } from 'history';
 | 
			
		||||
 | 
			
		||||
export const history = createBrowserHistory();
 | 
			
		||||
export const history = createBrowserHistory();
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
export * from './history';
 | 
			
		||||
export * from './auth-header';
 | 
			
		||||
export * from './utils';
 | 
			
		||||
export * from './api';
 | 
			
		||||
export * from './api';
 | 
			
		||||
 
 | 
			
		||||
@@ -1,170 +1,197 @@
 | 
			
		||||
import {Label} from 'semantic-ui-react';
 | 
			
		||||
import {Tag} from "@douyinfe/semi-ui";
 | 
			
		||||
import { Label } from 'semantic-ui-react';
 | 
			
		||||
import { Tag } from '@douyinfe/semi-ui';
 | 
			
		||||
 | 
			
		||||
export function renderText(text, limit) {
 | 
			
		||||
    if (text.length > limit) {
 | 
			
		||||
        return text.slice(0, limit - 3) + '...';
 | 
			
		||||
    }
 | 
			
		||||
    return text;
 | 
			
		||||
  if (text.length > limit) {
 | 
			
		||||
    return text.slice(0, limit - 3) + '...';
 | 
			
		||||
  }
 | 
			
		||||
  return text;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function renderGroup(group) {
 | 
			
		||||
    if (group === '') {
 | 
			
		||||
        return <Tag size='large'>default</Tag>;
 | 
			
		||||
    }
 | 
			
		||||
    let groups = group.split(',');
 | 
			
		||||
    groups.sort();
 | 
			
		||||
    return <>
 | 
			
		||||
        {groups.map((group) => {
 | 
			
		||||
            if (group === 'vip' || group === 'pro') {
 | 
			
		||||
                return <Tag size='large' color='yellow'>{group}</Tag>;
 | 
			
		||||
            } else if (group === 'svip' || group === 'premium') {
 | 
			
		||||
                return <Tag size='large' color='red'>{group}</Tag>;
 | 
			
		||||
            }
 | 
			
		||||
            if (group === 'default') {
 | 
			
		||||
                return <Tag size='large'>{group}</Tag>;
 | 
			
		||||
            } else {
 | 
			
		||||
                return <Tag size='large' color={stringToColor(group)}>{group}</Tag>;
 | 
			
		||||
            }
 | 
			
		||||
        })}
 | 
			
		||||
    </>;
 | 
			
		||||
  if (group === '') {
 | 
			
		||||
    return <Tag size='large'>default</Tag>;
 | 
			
		||||
  }
 | 
			
		||||
  let groups = group.split(',');
 | 
			
		||||
  groups.sort();
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {groups.map((group) => {
 | 
			
		||||
        if (group === 'vip' || group === 'pro') {
 | 
			
		||||
          return (
 | 
			
		||||
            <Tag size='large' color='yellow'>
 | 
			
		||||
              {group}
 | 
			
		||||
            </Tag>
 | 
			
		||||
          );
 | 
			
		||||
        } else if (group === 'svip' || group === 'premium') {
 | 
			
		||||
          return (
 | 
			
		||||
            <Tag size='large' color='red'>
 | 
			
		||||
              {group}
 | 
			
		||||
            </Tag>
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
        if (group === 'default') {
 | 
			
		||||
          return <Tag size='large'>{group}</Tag>;
 | 
			
		||||
        } else {
 | 
			
		||||
          return (
 | 
			
		||||
            <Tag size='large' color={stringToColor(group)}>
 | 
			
		||||
              {group}
 | 
			
		||||
            </Tag>
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
      })}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function renderNumber(num) {
 | 
			
		||||
    if (num >= 1000000000) {
 | 
			
		||||
        return (num / 1000000000).toFixed(1) + 'B';
 | 
			
		||||
    } else if (num >= 1000000) {
 | 
			
		||||
        return (num / 1000000).toFixed(1) + 'M';
 | 
			
		||||
    } else if (num >= 10000) {
 | 
			
		||||
        return (num / 1000).toFixed(1) + 'k';
 | 
			
		||||
    } else {
 | 
			
		||||
        return num;
 | 
			
		||||
    }
 | 
			
		||||
  if (num >= 1000000000) {
 | 
			
		||||
    return (num / 1000000000).toFixed(1) + 'B';
 | 
			
		||||
  } else if (num >= 1000000) {
 | 
			
		||||
    return (num / 1000000).toFixed(1) + 'M';
 | 
			
		||||
  } else if (num >= 10000) {
 | 
			
		||||
    return (num / 1000).toFixed(1) + 'k';
 | 
			
		||||
  } else {
 | 
			
		||||
    return num;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function renderQuotaNumberWithDigit(num, digits = 2) {
 | 
			
		||||
    let displayInCurrency = localStorage.getItem('display_in_currency');
 | 
			
		||||
    num = num.toFixed(digits);
 | 
			
		||||
    if (displayInCurrency) {
 | 
			
		||||
        return '$' + num;
 | 
			
		||||
    }
 | 
			
		||||
    return num;
 | 
			
		||||
  let displayInCurrency = localStorage.getItem('display_in_currency');
 | 
			
		||||
  num = num.toFixed(digits);
 | 
			
		||||
  if (displayInCurrency) {
 | 
			
		||||
    return '$' + num;
 | 
			
		||||
  }
 | 
			
		||||
  return num;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function renderNumberWithPoint(num) {
 | 
			
		||||
    num = num.toFixed(2);
 | 
			
		||||
    if (num >= 100000) {
 | 
			
		||||
        // Convert number to string to manipulate it
 | 
			
		||||
        let numStr = num.toString();
 | 
			
		||||
        // Find the position of the decimal point
 | 
			
		||||
        let decimalPointIndex = numStr.indexOf('.');
 | 
			
		||||
  num = num.toFixed(2);
 | 
			
		||||
  if (num >= 100000) {
 | 
			
		||||
    // Convert number to string to manipulate it
 | 
			
		||||
    let numStr = num.toString();
 | 
			
		||||
    // Find the position of the decimal point
 | 
			
		||||
    let decimalPointIndex = numStr.indexOf('.');
 | 
			
		||||
 | 
			
		||||
        let wholePart = numStr;
 | 
			
		||||
        let decimalPart = '';
 | 
			
		||||
    let wholePart = numStr;
 | 
			
		||||
    let decimalPart = '';
 | 
			
		||||
 | 
			
		||||
        // If there is a decimal point, split the number into whole and decimal parts
 | 
			
		||||
        if (decimalPointIndex !== -1) {
 | 
			
		||||
            wholePart = numStr.slice(0, decimalPointIndex);
 | 
			
		||||
            decimalPart = numStr.slice(decimalPointIndex);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Take the first two and last two digits of the whole number part
 | 
			
		||||
        let shortenedWholePart = wholePart.slice(0, 2) + '..' + wholePart.slice(-2);
 | 
			
		||||
 | 
			
		||||
        // Return the formatted number
 | 
			
		||||
        return shortenedWholePart + decimalPart;
 | 
			
		||||
    // If there is a decimal point, split the number into whole and decimal parts
 | 
			
		||||
    if (decimalPointIndex !== -1) {
 | 
			
		||||
      wholePart = numStr.slice(0, decimalPointIndex);
 | 
			
		||||
      decimalPart = numStr.slice(decimalPointIndex);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // If the number is less than 100,000, return it unmodified
 | 
			
		||||
    return num;
 | 
			
		||||
    // Take the first two and last two digits of the whole number part
 | 
			
		||||
    let shortenedWholePart = wholePart.slice(0, 2) + '..' + wholePart.slice(-2);
 | 
			
		||||
 | 
			
		||||
    // Return the formatted number
 | 
			
		||||
    return shortenedWholePart + decimalPart;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // If the number is less than 100,000, return it unmodified
 | 
			
		||||
  return num;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getQuotaPerUnit() {
 | 
			
		||||
    let quotaPerUnit = localStorage.getItem('quota_per_unit');
 | 
			
		||||
    quotaPerUnit = parseFloat(quotaPerUnit);
 | 
			
		||||
    return quotaPerUnit;
 | 
			
		||||
  let quotaPerUnit = localStorage.getItem('quota_per_unit');
 | 
			
		||||
  quotaPerUnit = parseFloat(quotaPerUnit);
 | 
			
		||||
  return quotaPerUnit;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getQuotaWithUnit(quota, digits = 6) {
 | 
			
		||||
    let quotaPerUnit = localStorage.getItem('quota_per_unit');
 | 
			
		||||
    quotaPerUnit = parseFloat(quotaPerUnit);
 | 
			
		||||
    return (quota / quotaPerUnit).toFixed(digits);
 | 
			
		||||
  let quotaPerUnit = localStorage.getItem('quota_per_unit');
 | 
			
		||||
  quotaPerUnit = parseFloat(quotaPerUnit);
 | 
			
		||||
  return (quota / quotaPerUnit).toFixed(digits);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function renderQuota(quota, digits = 2) {
 | 
			
		||||
    let quotaPerUnit = localStorage.getItem('quota_per_unit');
 | 
			
		||||
    let displayInCurrency = localStorage.getItem('display_in_currency');
 | 
			
		||||
    quotaPerUnit = parseFloat(quotaPerUnit);
 | 
			
		||||
    displayInCurrency = displayInCurrency === 'true';
 | 
			
		||||
    if (displayInCurrency) {
 | 
			
		||||
        return '$' + (quota / quotaPerUnit).toFixed(digits);
 | 
			
		||||
    }
 | 
			
		||||
    return renderNumber(quota);
 | 
			
		||||
  let quotaPerUnit = localStorage.getItem('quota_per_unit');
 | 
			
		||||
  let displayInCurrency = localStorage.getItem('display_in_currency');
 | 
			
		||||
  quotaPerUnit = parseFloat(quotaPerUnit);
 | 
			
		||||
  displayInCurrency = displayInCurrency === 'true';
 | 
			
		||||
  if (displayInCurrency) {
 | 
			
		||||
    return '$' + (quota / quotaPerUnit).toFixed(digits);
 | 
			
		||||
  }
 | 
			
		||||
  return renderNumber(quota);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function renderQuotaWithPrompt(quota, digits) {
 | 
			
		||||
    let displayInCurrency = localStorage.getItem('display_in_currency');
 | 
			
		||||
    displayInCurrency = displayInCurrency === 'true';
 | 
			
		||||
    if (displayInCurrency) {
 | 
			
		||||
        return `(等价金额:${renderQuota(quota, digits)})`;
 | 
			
		||||
    }
 | 
			
		||||
    return '';
 | 
			
		||||
  let displayInCurrency = localStorage.getItem('display_in_currency');
 | 
			
		||||
  displayInCurrency = displayInCurrency === 'true';
 | 
			
		||||
  if (displayInCurrency) {
 | 
			
		||||
    return `(等价金额:${renderQuota(quota, digits)})`;
 | 
			
		||||
  }
 | 
			
		||||
  return '';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const colors = ['amber', 'blue', 'cyan', 'green', 'grey', 'indigo',
 | 
			
		||||
    'light-blue', 'lime', 'orange', 'pink',
 | 
			
		||||
    'purple', 'red', 'teal', 'violet', 'yellow'
 | 
			
		||||
]
 | 
			
		||||
const colors = [
 | 
			
		||||
  'amber',
 | 
			
		||||
  'blue',
 | 
			
		||||
  'cyan',
 | 
			
		||||
  'green',
 | 
			
		||||
  'grey',
 | 
			
		||||
  'indigo',
 | 
			
		||||
  'light-blue',
 | 
			
		||||
  'lime',
 | 
			
		||||
  'orange',
 | 
			
		||||
  'pink',
 | 
			
		||||
  'purple',
 | 
			
		||||
  'red',
 | 
			
		||||
  'teal',
 | 
			
		||||
  'violet',
 | 
			
		||||
  'yellow',
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export const modelColorMap = {
 | 
			
		||||
    'dall-e': 'rgb(147,112,219)',  // 深紫色
 | 
			
		||||
    'dall-e-2': 'rgb(147,112,219)',  // 介于紫色和蓝色之间的色调
 | 
			
		||||
    'dall-e-3': 'rgb(153,50,204)',  // 介于紫罗兰和洋红之间的色调
 | 
			
		||||
    'midjourney': 'rgb(136,43,180)',  // 介于紫罗兰和洋红之间的色调
 | 
			
		||||
    'gpt-3.5-turbo': 'rgb(184,227,167)',  // 浅绿色
 | 
			
		||||
    'gpt-3.5-turbo-0301': 'rgb(131,220,131)',  // 亮绿色
 | 
			
		||||
    'gpt-3.5-turbo-0613': 'rgb(60,179,113)',  // 海洋绿
 | 
			
		||||
    'gpt-3.5-turbo-1106': 'rgb(32,178,170)',  // 浅海洋绿
 | 
			
		||||
    'gpt-3.5-turbo-16k': 'rgb(252,200,149)',  // 淡橙色
 | 
			
		||||
    'gpt-3.5-turbo-16k-0613': 'rgb(255,181,119)',  // 淡桃色
 | 
			
		||||
    'gpt-3.5-turbo-instruct': 'rgb(175,238,238)',  // 粉蓝色
 | 
			
		||||
    'gpt-4': 'rgb(135,206,235)',  // 天蓝色
 | 
			
		||||
    'gpt-4-0314': 'rgb(70,130,180)',  // 钢蓝色
 | 
			
		||||
    'gpt-4-0613': 'rgb(100,149,237)',  // 矢车菊蓝
 | 
			
		||||
    'gpt-4-1106-preview': 'rgb(30,144,255)',  // 道奇蓝
 | 
			
		||||
    'gpt-4-0125-preview': 'rgb(2,177,236)',  // 深天蓝
 | 
			
		||||
    'gpt-4-turbo-preview': 'rgb(2,177,255)',  // 深天蓝
 | 
			
		||||
    'gpt-4-32k': 'rgb(104,111,238)',  // 中紫色
 | 
			
		||||
    'gpt-4-32k-0314': 'rgb(90,105,205)',  // 暗灰蓝色
 | 
			
		||||
    'gpt-4-32k-0613': 'rgb(61,71,139)',  // 暗蓝灰色
 | 
			
		||||
    'gpt-4-all': 'rgb(65,105,225)',  // 皇家蓝
 | 
			
		||||
    'gpt-4-gizmo-*': 'rgb(0,0,255)',  // 纯蓝色
 | 
			
		||||
    'gpt-4-vision-preview': 'rgb(25,25,112)',  // 午夜蓝
 | 
			
		||||
    'text-ada-001': 'rgb(255,192,203)',  // 粉红色
 | 
			
		||||
    'text-babbage-001': 'rgb(255,160,122)',  // 浅珊瑚色
 | 
			
		||||
    'text-curie-001': 'rgb(219,112,147)',  // 苍紫罗兰色
 | 
			
		||||
    'text-davinci-002': 'rgb(199,21,133)',  // 中紫罗兰红色
 | 
			
		||||
    'text-davinci-003': 'rgb(219,112,147)',  // 苍紫罗兰色(与Curie相同,表示同一个系列)
 | 
			
		||||
    'text-davinci-edit-001': 'rgb(255,105,180)',  // 热粉色
 | 
			
		||||
    'text-embedding-ada-002': 'rgb(255,182,193)',  // 浅粉红
 | 
			
		||||
    'text-embedding-v1': 'rgb(255,174,185)',  // 浅粉红色(略有区别)
 | 
			
		||||
    'text-moderation-latest': 'rgb(255,130,171)',  // 强粉色
 | 
			
		||||
    'text-moderation-stable': 'rgb(255,160,122)',  // 浅珊瑚色(与Babbage相同,表示同一类功能)
 | 
			
		||||
    'tts-1': 'rgb(255,140,0)',  // 深橙色
 | 
			
		||||
    'tts-1-1106': 'rgb(255,165,0)',  // 橙色
 | 
			
		||||
    'tts-1-hd': 'rgb(255,215,0)',  // 金色
 | 
			
		||||
    'tts-1-hd-1106': 'rgb(255,223,0)',  // 金黄色(略有区别)
 | 
			
		||||
    'whisper-1': 'rgb(245,245,220)'  // 米色
 | 
			
		||||
}
 | 
			
		||||
  'dall-e': 'rgb(147,112,219)', // 深紫色
 | 
			
		||||
  'dall-e-2': 'rgb(147,112,219)', // 介于紫色和蓝色之间的色调
 | 
			
		||||
  'dall-e-3': 'rgb(153,50,204)', // 介于紫罗兰和洋红之间的色调
 | 
			
		||||
  midjourney: 'rgb(136,43,180)', // 介于紫罗兰和洋红之间的色调
 | 
			
		||||
  'gpt-3.5-turbo': 'rgb(184,227,167)', // 浅绿色
 | 
			
		||||
  'gpt-3.5-turbo-0301': 'rgb(131,220,131)', // 亮绿色
 | 
			
		||||
  'gpt-3.5-turbo-0613': 'rgb(60,179,113)', // 海洋绿
 | 
			
		||||
  'gpt-3.5-turbo-1106': 'rgb(32,178,170)', // 浅海洋绿
 | 
			
		||||
  'gpt-3.5-turbo-16k': 'rgb(252,200,149)', // 淡橙色
 | 
			
		||||
  'gpt-3.5-turbo-16k-0613': 'rgb(255,181,119)', // 淡桃色
 | 
			
		||||
  'gpt-3.5-turbo-instruct': 'rgb(175,238,238)', // 粉蓝色
 | 
			
		||||
  'gpt-4': 'rgb(135,206,235)', // 天蓝色
 | 
			
		||||
  'gpt-4-0314': 'rgb(70,130,180)', // 钢蓝色
 | 
			
		||||
  'gpt-4-0613': 'rgb(100,149,237)', // 矢车菊蓝
 | 
			
		||||
  'gpt-4-1106-preview': 'rgb(30,144,255)', // 道奇蓝
 | 
			
		||||
  'gpt-4-0125-preview': 'rgb(2,177,236)', // 深天蓝
 | 
			
		||||
  'gpt-4-turbo-preview': 'rgb(2,177,255)', // 深天蓝
 | 
			
		||||
  'gpt-4-32k': 'rgb(104,111,238)', // 中紫色
 | 
			
		||||
  'gpt-4-32k-0314': 'rgb(90,105,205)', // 暗灰蓝色
 | 
			
		||||
  'gpt-4-32k-0613': 'rgb(61,71,139)', // 暗蓝灰色
 | 
			
		||||
  'gpt-4-all': 'rgb(65,105,225)', // 皇家蓝
 | 
			
		||||
  'gpt-4-gizmo-*': 'rgb(0,0,255)', // 纯蓝色
 | 
			
		||||
  'gpt-4-vision-preview': 'rgb(25,25,112)', // 午夜蓝
 | 
			
		||||
  'text-ada-001': 'rgb(255,192,203)', // 粉红色
 | 
			
		||||
  'text-babbage-001': 'rgb(255,160,122)', // 浅珊瑚色
 | 
			
		||||
  'text-curie-001': 'rgb(219,112,147)', // 苍紫罗兰色
 | 
			
		||||
  'text-davinci-002': 'rgb(199,21,133)', // 中紫罗兰红色
 | 
			
		||||
  'text-davinci-003': 'rgb(219,112,147)', // 苍紫罗兰色(与Curie相同,表示同一个系列)
 | 
			
		||||
  'text-davinci-edit-001': 'rgb(255,105,180)', // 热粉色
 | 
			
		||||
  'text-embedding-ada-002': 'rgb(255,182,193)', // 浅粉红
 | 
			
		||||
  'text-embedding-v1': 'rgb(255,174,185)', // 浅粉红色(略有区别)
 | 
			
		||||
  'text-moderation-latest': 'rgb(255,130,171)', // 强粉色
 | 
			
		||||
  'text-moderation-stable': 'rgb(255,160,122)', // 浅珊瑚色(与Babbage相同,表示同一类功能)
 | 
			
		||||
  'tts-1': 'rgb(255,140,0)', // 深橙色
 | 
			
		||||
  'tts-1-1106': 'rgb(255,165,0)', // 橙色
 | 
			
		||||
  'tts-1-hd': 'rgb(255,215,0)', // 金色
 | 
			
		||||
  'tts-1-hd-1106': 'rgb(255,223,0)', // 金黄色(略有区别)
 | 
			
		||||
  'whisper-1': 'rgb(245,245,220)', // 米色
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export function stringToColor(str) {
 | 
			
		||||
    let sum = 0;
 | 
			
		||||
    // 对字符串中的每个字符进行操作
 | 
			
		||||
    for (let i = 0; i < str.length; i++) {
 | 
			
		||||
        // 将字符的ASCII值加到sum中
 | 
			
		||||
        sum += str.charCodeAt(i);
 | 
			
		||||
    }
 | 
			
		||||
    // 使用模运算得到个位数
 | 
			
		||||
    let i = sum % colors.length;
 | 
			
		||||
    return colors[i];
 | 
			
		||||
}
 | 
			
		||||
  let sum = 0;
 | 
			
		||||
  // 对字符串中的每个字符进行操作
 | 
			
		||||
  for (let i = 0; i < str.length; i++) {
 | 
			
		||||
    // 将字符的ASCII值加到sum中
 | 
			
		||||
    sum += str.charCodeAt(i);
 | 
			
		||||
  }
 | 
			
		||||
  // 使用模运算得到个位数
 | 
			
		||||
  let i = sum % colors.length;
 | 
			
		||||
  return colors[i];
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import { Toast } from '@douyinfe/semi-ui';
 | 
			
		||||
import { toastConstants } from '../constants';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import {toast} from "react-toastify";
 | 
			
		||||
import { toast } from 'react-toastify';
 | 
			
		||||
 | 
			
		||||
const HTMLToastContent = ({ htmlContent }) => {
 | 
			
		||||
  return <div dangerouslySetInnerHTML={{ __html: htmlContent }} />;
 | 
			
		||||
@@ -30,7 +30,7 @@ export function getSystemName() {
 | 
			
		||||
export function getLogo() {
 | 
			
		||||
  let logo = localStorage.getItem('logo');
 | 
			
		||||
  if (!logo) return '/logo.png';
 | 
			
		||||
  return logo
 | 
			
		||||
  return logo;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getFooterHTML() {
 | 
			
		||||
@@ -79,6 +79,7 @@ export function showError(error) {
 | 
			
		||||
      switch (error.response.status) {
 | 
			
		||||
        case 401:
 | 
			
		||||
          // toast.error('错误:未登录或登录已过期,请重新登录!', showErrorOptions);
 | 
			
		||||
          localStorage.removeItem('user');
 | 
			
		||||
          window.location.href = '/login?expired=true';
 | 
			
		||||
          break;
 | 
			
		||||
        case 429:
 | 
			
		||||
@@ -127,7 +128,7 @@ export function openPage(url) {
 | 
			
		||||
 | 
			
		||||
export function removeTrailingSlash(url) {
 | 
			
		||||
  if (!url) {
 | 
			
		||||
    return "";
 | 
			
		||||
    return '';
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (url.endsWith('/')) {
 | 
			
		||||
@@ -161,17 +162,7 @@ export function timestamp2string(timestamp) {
 | 
			
		||||
    second = '0' + second;
 | 
			
		||||
  }
 | 
			
		||||
  return (
 | 
			
		||||
    year +
 | 
			
		||||
    '-' +
 | 
			
		||||
    month +
 | 
			
		||||
    '-' +
 | 
			
		||||
    day +
 | 
			
		||||
    ' ' +
 | 
			
		||||
    hour +
 | 
			
		||||
    ':' +
 | 
			
		||||
    minute +
 | 
			
		||||
    ':' +
 | 
			
		||||
    second
 | 
			
		||||
    year + '-' + month + '-' + day + ' ' + hour + ':' + minute + ':' + second
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -190,20 +181,20 @@ export function timestamp2string1(timestamp, dataExportDefaultTime = 'hour') {
 | 
			
		||||
  if (hour.length === 1) {
 | 
			
		||||
    hour = '0' + hour;
 | 
			
		||||
  }
 | 
			
		||||
  let str = month + '-' + day
 | 
			
		||||
  let str = month + '-' + day;
 | 
			
		||||
  if (dataExportDefaultTime === 'hour') {
 | 
			
		||||
    str += ' ' + hour + ":00"
 | 
			
		||||
    str += ' ' + hour + ':00';
 | 
			
		||||
  } else if (dataExportDefaultTime === 'week') {
 | 
			
		||||
    let nextWeek = new Date(timestamp * 1000 + 6 * 24 * 60 * 60 * 1000);
 | 
			
		||||
    let nextMonth = (nextWeek.getMonth() + 1).toString();
 | 
			
		||||
    let nextDay = nextWeek.getDate().toString();
 | 
			
		||||
    if (nextMonth.length === 1) {
 | 
			
		||||
        nextMonth = '0' + nextMonth;
 | 
			
		||||
      nextMonth = '0' + nextMonth;
 | 
			
		||||
    }
 | 
			
		||||
    if (nextDay.length === 1) {
 | 
			
		||||
        nextDay = '0' + nextDay;
 | 
			
		||||
      nextDay = '0' + nextDay;
 | 
			
		||||
    }
 | 
			
		||||
    str += ' - ' + nextMonth + '-' + nextDay
 | 
			
		||||
    str += ' - ' + nextMonth + '-' + nextDay;
 | 
			
		||||
  }
 | 
			
		||||
  return str;
 | 
			
		||||
}
 | 
			
		||||
@@ -229,9 +220,8 @@ export const verifyJSON = (str) => {
 | 
			
		||||
export function shouldShowPrompt(id) {
 | 
			
		||||
  let prompt = localStorage.getItem(`prompt-${id}`);
 | 
			
		||||
  return !prompt;
 | 
			
		||||
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function setPromptShown(id) {
 | 
			
		||||
  localStorage.setItem(`prompt-${id}`, 'true');
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,105 +1,109 @@
 | 
			
		||||
body {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    padding-top: 55px;
 | 
			
		||||
    overflow-y: scroll;
 | 
			
		||||
    font-family: Lato, 'Helvetica Neue', Arial, Helvetica, "Microsoft YaHei", sans-serif;
 | 
			
		||||
    -webkit-font-smoothing: antialiased;
 | 
			
		||||
    -moz-osx-font-smoothing: grayscale;
 | 
			
		||||
    scrollbar-width: none;
 | 
			
		||||
    color: var(--semi-color-text-0) !important;
 | 
			
		||||
    background-color: var( --semi-color-bg-0) !important;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
  margin: 0;
 | 
			
		||||
  padding-top: 55px;
 | 
			
		||||
  overflow-y: scroll;
 | 
			
		||||
  font-family: Lato, 'Helvetica Neue', Arial, Helvetica, 'Microsoft YaHei',
 | 
			
		||||
    sans-serif;
 | 
			
		||||
  -webkit-font-smoothing: antialiased;
 | 
			
		||||
  -moz-osx-font-smoothing: grayscale;
 | 
			
		||||
  scrollbar-width: none;
 | 
			
		||||
  color: var(--semi-color-text-0) !important;
 | 
			
		||||
  background-color: var(--semi-color-bg-0) !important;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#root {
 | 
			
		||||
    height: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media only screen and (max-width: 767px) {
 | 
			
		||||
    .semi-table-tbody, .semi-table-row, .semi-table-row-cell {
 | 
			
		||||
        display: block!important;
 | 
			
		||||
        width: auto!important;
 | 
			
		||||
        padding: 2px!important;
 | 
			
		||||
    }
 | 
			
		||||
    .semi-table-row-cell {
 | 
			
		||||
        border-bottom: 0!important;
 | 
			
		||||
    }
 | 
			
		||||
    .semi-table-tbody>.semi-table-row {
 | 
			
		||||
        border-bottom: 1px solid rgba(0,0,0,.1);
 | 
			
		||||
    }
 | 
			
		||||
    .semi-space {
 | 
			
		||||
        /*display: block!important;*/
 | 
			
		||||
        display: flex;
 | 
			
		||||
        flex-direction: row;
 | 
			
		||||
        flex-wrap: wrap;
 | 
			
		||||
        row-gap: 3px;
 | 
			
		||||
        column-gap: 10px;
 | 
			
		||||
    }
 | 
			
		||||
  .semi-table-tbody,
 | 
			
		||||
  .semi-table-row,
 | 
			
		||||
  .semi-table-row-cell {
 | 
			
		||||
    display: block !important;
 | 
			
		||||
    width: auto !important;
 | 
			
		||||
    padding: 2px !important;
 | 
			
		||||
  }
 | 
			
		||||
  .semi-table-row-cell {
 | 
			
		||||
    border-bottom: 0 !important;
 | 
			
		||||
  }
 | 
			
		||||
  .semi-table-tbody > .semi-table-row {
 | 
			
		||||
    border-bottom: 1px solid rgba(0, 0, 0, 0.1);
 | 
			
		||||
  }
 | 
			
		||||
  .semi-space {
 | 
			
		||||
    /*display: block!important;*/
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: row;
 | 
			
		||||
    flex-wrap: wrap;
 | 
			
		||||
    row-gap: 3px;
 | 
			
		||||
    column-gap: 10px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.semi-table-tbody > .semi-table-row > .semi-table-row-cell {
 | 
			
		||||
    padding: 16px 14px;
 | 
			
		||||
  padding: 16px 14px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.channel-table {
 | 
			
		||||
    .semi-table-tbody > .semi-table-row > .semi-table-row-cell {
 | 
			
		||||
        padding: 16px 8px;
 | 
			
		||||
    }
 | 
			
		||||
  .semi-table-tbody > .semi-table-row > .semi-table-row-cell {
 | 
			
		||||
    padding: 16px 8px;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.semi-layout {
 | 
			
		||||
    height: 100%;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tableShow {
 | 
			
		||||
    display: revert;
 | 
			
		||||
  display: revert;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.tableHiddle {
 | 
			
		||||
    display: none !important;
 | 
			
		||||
  display: none !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body::-webkit-scrollbar {
 | 
			
		||||
    display: none;
 | 
			
		||||
  display: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
code {
 | 
			
		||||
    font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
 | 
			
		||||
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
 | 
			
		||||
    monospace;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.semi-navigation-vertical {
 | 
			
		||||
    /*display: flex;*/
 | 
			
		||||
    /*flex-direction: column;*/
 | 
			
		||||
  /*display: flex;*/
 | 
			
		||||
  /*flex-direction: column;*/
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.semi-navigation-item {
 | 
			
		||||
    margin-bottom: 0;
 | 
			
		||||
  margin-bottom: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.semi-navigation-vertical {
 | 
			
		||||
    /*flex: 0 0 auto;*/
 | 
			
		||||
    /*display: flex;*/
 | 
			
		||||
    /*flex-direction: column;*/
 | 
			
		||||
    /*width: 100%;*/
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
  /*flex: 0 0 auto;*/
 | 
			
		||||
  /*display: flex;*/
 | 
			
		||||
  /*flex-direction: column;*/
 | 
			
		||||
  /*width: 100%;*/
 | 
			
		||||
  height: 100%;
 | 
			
		||||
  overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.main-content {
 | 
			
		||||
    padding: 4px;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
  padding: 4px;
 | 
			
		||||
  height: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.small-icon .icon {
 | 
			
		||||
    font-size: 1em !important;
 | 
			
		||||
  font-size: 1em !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.custom-footer {
 | 
			
		||||
    font-size: 1.1em;
 | 
			
		||||
  font-size: 1.1em;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@media only screen and (max-width: 600px) {
 | 
			
		||||
    .hide-on-mobile {
 | 
			
		||||
        display: none !important;
 | 
			
		||||
    }
 | 
			
		||||
  .hide-on-mobile {
 | 
			
		||||
    display: none !important;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,54 +1,50 @@
 | 
			
		||||
import { initVChartSemiTheme } from '@visactor/vchart-semi-theme';
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import ReactDOM from 'react-dom/client';
 | 
			
		||||
import {BrowserRouter} from 'react-router-dom';
 | 
			
		||||
import { BrowserRouter } from 'react-router-dom';
 | 
			
		||||
import App from './App';
 | 
			
		||||
import HeaderBar from './components/HeaderBar';
 | 
			
		||||
import Footer from './components/Footer';
 | 
			
		||||
import 'semantic-ui-css/semantic.min.css';
 | 
			
		||||
import 'semantic-ui-offline/semantic.min.css';
 | 
			
		||||
import './index.css';
 | 
			
		||||
import {UserProvider} from './context/User';
 | 
			
		||||
import {ToastContainer} from 'react-toastify';
 | 
			
		||||
import { UserProvider } from './context/User';
 | 
			
		||||
import { ToastContainer } from 'react-toastify';
 | 
			
		||||
import 'react-toastify/dist/ReactToastify.css';
 | 
			
		||||
import {StatusProvider} from './context/Status';
 | 
			
		||||
import {Layout} from "@douyinfe/semi-ui";
 | 
			
		||||
import SiderBar from "./components/SiderBar";
 | 
			
		||||
import { StatusProvider } from './context/Status';
 | 
			
		||||
import { Layout } from '@douyinfe/semi-ui';
 | 
			
		||||
import SiderBar from './components/SiderBar';
 | 
			
		||||
 | 
			
		||||
// initialization
 | 
			
		||||
initVChartSemiTheme({
 | 
			
		||||
    isWatchingThemeSwitch: true,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const root = ReactDOM.createRoot(document.getElementById('root'));
 | 
			
		||||
const {Sider, Content, Header} = Layout;
 | 
			
		||||
const { Sider, Content, Header } = Layout;
 | 
			
		||||
root.render(
 | 
			
		||||
    <React.StrictMode>
 | 
			
		||||
        <StatusProvider>
 | 
			
		||||
            <UserProvider>
 | 
			
		||||
                <BrowserRouter>
 | 
			
		||||
                    <Layout>
 | 
			
		||||
                        <Sider>
 | 
			
		||||
                            <SiderBar/>
 | 
			
		||||
                        </Sider>
 | 
			
		||||
                        <Layout>
 | 
			
		||||
                            <Header>
 | 
			
		||||
                                <HeaderBar/>
 | 
			
		||||
                            </Header>
 | 
			
		||||
                            <Content
 | 
			
		||||
                                style={{
 | 
			
		||||
                                    padding: '24px',
 | 
			
		||||
                                }}
 | 
			
		||||
                            >
 | 
			
		||||
                                <App/>
 | 
			
		||||
                            </Content>
 | 
			
		||||
                            <Layout.Footer>
 | 
			
		||||
                                <Footer></Footer>
 | 
			
		||||
                            </Layout.Footer>
 | 
			
		||||
                        </Layout>
 | 
			
		||||
                        <ToastContainer/>
 | 
			
		||||
                    </Layout>
 | 
			
		||||
                </BrowserRouter>
 | 
			
		||||
            </UserProvider>
 | 
			
		||||
        </StatusProvider>
 | 
			
		||||
    </React.StrictMode>
 | 
			
		||||
  <React.StrictMode>
 | 
			
		||||
    <StatusProvider>
 | 
			
		||||
      <UserProvider>
 | 
			
		||||
        <BrowserRouter>
 | 
			
		||||
          <Layout>
 | 
			
		||||
            <Sider>
 | 
			
		||||
              <SiderBar />
 | 
			
		||||
            </Sider>
 | 
			
		||||
            <Layout>
 | 
			
		||||
              <Header>
 | 
			
		||||
                <HeaderBar />
 | 
			
		||||
              </Header>
 | 
			
		||||
              <Content
 | 
			
		||||
                style={{
 | 
			
		||||
                  padding: '24px',
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                <App />
 | 
			
		||||
              </Content>
 | 
			
		||||
              <Layout.Footer>
 | 
			
		||||
                <Footer></Footer>
 | 
			
		||||
              </Layout.Footer>
 | 
			
		||||
            </Layout>
 | 
			
		||||
            <ToastContainer />
 | 
			
		||||
          </Layout>
 | 
			
		||||
        </BrowserRouter>
 | 
			
		||||
      </UserProvider>
 | 
			
		||||
    </StatusProvider>
 | 
			
		||||
  </React.StrictMode>,
 | 
			
		||||
);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { API, showError } from '../../helpers';
 | 
			
		||||
import { marked } from 'marked';
 | 
			
		||||
import {Layout} from "@douyinfe/semi-ui";
 | 
			
		||||
import { Layout } from '@douyinfe/semi-ui';
 | 
			
		||||
 | 
			
		||||
const About = () => {
 | 
			
		||||
  const [about, setAbout] = useState('');
 | 
			
		||||
@@ -31,37 +31,42 @@ const About = () => {
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {
 | 
			
		||||
        aboutLoaded && about === '' ? <>
 | 
			
		||||
      {aboutLoaded && about === '' ? (
 | 
			
		||||
        <>
 | 
			
		||||
          <Layout>
 | 
			
		||||
            <Layout.Header>
 | 
			
		||||
              <h3>关于</h3>
 | 
			
		||||
            </Layout.Header>
 | 
			
		||||
            <Layout.Content>
 | 
			
		||||
              <p>
 | 
			
		||||
                可在设置页面设置关于内容,支持 HTML & Markdown
 | 
			
		||||
              </p>
 | 
			
		||||
              <p>可在设置页面设置关于内容,支持 HTML & Markdown</p>
 | 
			
		||||
              new-api项目仓库地址:
 | 
			
		||||
              <a href='https://github.com/Calcium-Ion/new-api'>
 | 
			
		||||
                https://github.com/Calcium-Ion/new-api
 | 
			
		||||
              </a>
 | 
			
		||||
              <p>
 | 
			
		||||
                NewAPI © 2023 CalciumIon | 基于 One API v0.5.4 © 2023 JustSong。本项目根据MIT许可证授权。
 | 
			
		||||
                NewAPI © 2023 CalciumIon | 基于 One API v0.5.4 © 2023
 | 
			
		||||
                JustSong。本项目根据MIT许可证授权。
 | 
			
		||||
              </p>
 | 
			
		||||
            </Layout.Content>
 | 
			
		||||
          </Layout>
 | 
			
		||||
        </> : <>
 | 
			
		||||
          {
 | 
			
		||||
            about.startsWith('https://') ? <iframe
 | 
			
		||||
        </>
 | 
			
		||||
      ) : (
 | 
			
		||||
        <>
 | 
			
		||||
          {about.startsWith('https://') ? (
 | 
			
		||||
            <iframe
 | 
			
		||||
              src={about}
 | 
			
		||||
              style={{ width: '100%', height: '100vh', border: 'none' }}
 | 
			
		||||
            /> : <div style={{ fontSize: 'larger' }} dangerouslySetInnerHTML={{ __html: about }}></div>
 | 
			
		||||
          }
 | 
			
		||||
            />
 | 
			
		||||
          ) : (
 | 
			
		||||
            <div
 | 
			
		||||
              style={{ fontSize: 'larger' }}
 | 
			
		||||
              dangerouslySetInnerHTML={{ __html: about }}
 | 
			
		||||
            ></div>
 | 
			
		||||
          )}
 | 
			
		||||
        </>
 | 
			
		||||
      }
 | 
			
		||||
      )}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export default About;
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,18 +1,18 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import ChannelsTable from '../../components/ChannelsTable';
 | 
			
		||||
import {Layout} from "@douyinfe/semi-ui";
 | 
			
		||||
import { Layout } from '@douyinfe/semi-ui';
 | 
			
		||||
 | 
			
		||||
const File = () => (
 | 
			
		||||
    <>
 | 
			
		||||
        <Layout>
 | 
			
		||||
            <Layout.Header>
 | 
			
		||||
                <h3>管理渠道</h3>
 | 
			
		||||
            </Layout.Header>
 | 
			
		||||
            <Layout.Content>
 | 
			
		||||
                <ChannelsTable/>
 | 
			
		||||
            </Layout.Content>
 | 
			
		||||
        </Layout>
 | 
			
		||||
    </>
 | 
			
		||||
  <>
 | 
			
		||||
    <Layout>
 | 
			
		||||
      <Layout.Header>
 | 
			
		||||
        <h3>管理渠道</h3>
 | 
			
		||||
      </Layout.Header>
 | 
			
		||||
      <Layout.Content>
 | 
			
		||||
        <ChannelsTable />
 | 
			
		||||
      </Layout.Content>
 | 
			
		||||
    </Layout>
 | 
			
		||||
  </>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
export default File;
 | 
			
		||||
 
 | 
			
		||||
@@ -11,5 +11,4 @@ const Chat = () => {
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export default Chat;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,359 +1,423 @@
 | 
			
		||||
import React, {useEffect, useRef, useState} from 'react';
 | 
			
		||||
import {Button, Col, Form, Layout, Row, Spin} from "@douyinfe/semi-ui";
 | 
			
		||||
import React, { useEffect, useRef, useState } from 'react';
 | 
			
		||||
import { initVChartSemiTheme } from '@visactor/vchart-semi-theme';
 | 
			
		||||
 | 
			
		||||
import { Button, Col, Form, Layout, Row, Spin } from '@douyinfe/semi-ui';
 | 
			
		||||
import VChart from '@visactor/vchart';
 | 
			
		||||
import {API, isAdmin, showError, timestamp2string, timestamp2string1} from "../../helpers";
 | 
			
		||||
import {
 | 
			
		||||
    getQuotaWithUnit, modelColorMap,
 | 
			
		||||
    renderNumber,
 | 
			
		||||
    renderQuota,
 | 
			
		||||
    renderQuotaNumberWithDigit,
 | 
			
		||||
    stringToColor
 | 
			
		||||
} from "../../helpers/render";
 | 
			
		||||
  API,
 | 
			
		||||
  isAdmin,
 | 
			
		||||
  showError,
 | 
			
		||||
  timestamp2string,
 | 
			
		||||
  timestamp2string1,
 | 
			
		||||
} from '../../helpers';
 | 
			
		||||
import {
 | 
			
		||||
  getQuotaWithUnit,
 | 
			
		||||
  modelColorMap,
 | 
			
		||||
  renderNumber,
 | 
			
		||||
  renderQuota,
 | 
			
		||||
  renderQuotaNumberWithDigit,
 | 
			
		||||
  stringToColor,
 | 
			
		||||
} from '../../helpers/render';
 | 
			
		||||
 | 
			
		||||
const Detail = (props) => {
 | 
			
		||||
    const formRef = useRef();
 | 
			
		||||
    let now = new Date();
 | 
			
		||||
    const [inputs, setInputs] = useState({
 | 
			
		||||
        username: '',
 | 
			
		||||
        token_name: '',
 | 
			
		||||
        model_name: '',
 | 
			
		||||
        start_timestamp: localStorage.getItem('data_export_default_time') === 'hour' ? timestamp2string(now.getTime() / 1000 - 86400) : (localStorage.getItem('data_export_default_time') === 'week' ? timestamp2string(now.getTime() / 1000 - 86400 * 30) : timestamp2string(now.getTime() / 1000 - 86400 * 7)),
 | 
			
		||||
        end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
 | 
			
		||||
        channel: '',
 | 
			
		||||
        data_export_default_time: ''
 | 
			
		||||
    });
 | 
			
		||||
    const {username, model_name, start_timestamp, end_timestamp, channel} = inputs;
 | 
			
		||||
    const isAdminUser = isAdmin();
 | 
			
		||||
    const initialized = useRef(false)
 | 
			
		||||
    const [modelDataChart, setModelDataChart] = useState(null);
 | 
			
		||||
    const [modelDataPieChart, setModelDataPieChart] = useState(null);
 | 
			
		||||
    const [loading, setLoading] = useState(false);
 | 
			
		||||
    const [quotaData, setQuotaData] = useState([]);
 | 
			
		||||
    const [consumeQuota, setConsumeQuota] = useState(0);
 | 
			
		||||
    const [times, setTimes] = useState(0);
 | 
			
		||||
    const [dataExportDefaultTime, setDataExportDefaultTime] = useState(localStorage.getItem('data_export_default_time') || 'hour');
 | 
			
		||||
  const formRef = useRef();
 | 
			
		||||
  let now = new Date();
 | 
			
		||||
  const [inputs, setInputs] = useState({
 | 
			
		||||
    username: '',
 | 
			
		||||
    token_name: '',
 | 
			
		||||
    model_name: '',
 | 
			
		||||
    start_timestamp:
 | 
			
		||||
      localStorage.getItem('data_export_default_time') === 'hour'
 | 
			
		||||
        ? timestamp2string(now.getTime() / 1000 - 86400)
 | 
			
		||||
        : localStorage.getItem('data_export_default_time') === 'week'
 | 
			
		||||
          ? timestamp2string(now.getTime() / 1000 - 86400 * 30)
 | 
			
		||||
          : timestamp2string(now.getTime() / 1000 - 86400 * 7),
 | 
			
		||||
    end_timestamp: timestamp2string(now.getTime() / 1000 + 3600),
 | 
			
		||||
    channel: '',
 | 
			
		||||
    data_export_default_time: '',
 | 
			
		||||
  });
 | 
			
		||||
  const { username, model_name, start_timestamp, end_timestamp, channel } =
 | 
			
		||||
    inputs;
 | 
			
		||||
  const isAdminUser = isAdmin();
 | 
			
		||||
  const initialized = useRef(false);
 | 
			
		||||
  const [modelDataChart, setModelDataChart] = useState(null);
 | 
			
		||||
  const [modelDataPieChart, setModelDataPieChart] = useState(null);
 | 
			
		||||
  const [loading, setLoading] = useState(false);
 | 
			
		||||
  const [quotaData, setQuotaData] = useState([]);
 | 
			
		||||
  const [consumeQuota, setConsumeQuota] = useState(0);
 | 
			
		||||
  const [times, setTimes] = useState(0);
 | 
			
		||||
  const [dataExportDefaultTime, setDataExportDefaultTime] = useState(
 | 
			
		||||
    localStorage.getItem('data_export_default_time') || 'hour',
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
    const handleInputChange = (value, name) => {
 | 
			
		||||
        if (name === 'data_export_default_time') {
 | 
			
		||||
            setDataExportDefaultTime(value);
 | 
			
		||||
            return
 | 
			
		||||
        }
 | 
			
		||||
        setInputs((inputs) => ({...inputs, [name]: value}));
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const spec_line = {
 | 
			
		||||
        type: 'bar',
 | 
			
		||||
        data: [
 | 
			
		||||
            {
 | 
			
		||||
                id: 'barData',
 | 
			
		||||
                values: []
 | 
			
		||||
            }
 | 
			
		||||
        ],
 | 
			
		||||
        xField: 'Time',
 | 
			
		||||
        yField: 'Usage',
 | 
			
		||||
        seriesField: 'Model',
 | 
			
		||||
        stack: true,
 | 
			
		||||
        legends: {
 | 
			
		||||
            visible: true
 | 
			
		||||
        },
 | 
			
		||||
        title: {
 | 
			
		||||
            visible: true,
 | 
			
		||||
            text: '模型消耗分布',
 | 
			
		||||
            subtext: '0'
 | 
			
		||||
        },
 | 
			
		||||
        bar: {
 | 
			
		||||
            // The state style of bar
 | 
			
		||||
            state: {
 | 
			
		||||
                hover: {
 | 
			
		||||
                    stroke: '#000',
 | 
			
		||||
                    lineWidth: 1
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        tooltip: {
 | 
			
		||||
            mark: {
 | 
			
		||||
                content: [
 | 
			
		||||
                    {
 | 
			
		||||
                        key: datum => datum['Model'],
 | 
			
		||||
                        value: datum => renderQuotaNumberWithDigit(parseFloat(datum['Usage']), 4)
 | 
			
		||||
                    }
 | 
			
		||||
                ]
 | 
			
		||||
            },
 | 
			
		||||
            dimension: {
 | 
			
		||||
                content: [
 | 
			
		||||
                    {
 | 
			
		||||
                        key: datum => datum['Model'],
 | 
			
		||||
                        value: datum => datum['Usage']
 | 
			
		||||
                    }
 | 
			
		||||
                ],
 | 
			
		||||
                updateContent: array => {
 | 
			
		||||
                    // sort by value
 | 
			
		||||
                    array.sort((a, b) => b.value - a.value);
 | 
			
		||||
                    // add $
 | 
			
		||||
                    let sum = 0;
 | 
			
		||||
                    for (let i = 0; i < array.length; i++) {
 | 
			
		||||
                        sum += parseFloat(array[i].value);
 | 
			
		||||
                        array[i].value = renderQuotaNumberWithDigit(parseFloat(array[i].value), 4);
 | 
			
		||||
                    }
 | 
			
		||||
                    // add to first
 | 
			
		||||
                    array.unshift({
 | 
			
		||||
                        key: '总计',
 | 
			
		||||
                        value: renderQuotaNumberWithDigit(sum, 4)
 | 
			
		||||
                    });
 | 
			
		||||
                    return array;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        color: {
 | 
			
		||||
            specified: modelColorMap
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const spec_pie = {
 | 
			
		||||
        type: 'pie',
 | 
			
		||||
        data: [
 | 
			
		||||
            {
 | 
			
		||||
                id: 'id0',
 | 
			
		||||
                values: [
 | 
			
		||||
                    {type: 'null', value: '0'},
 | 
			
		||||
                ]
 | 
			
		||||
            }
 | 
			
		||||
        ],
 | 
			
		||||
        outerRadius: 0.8,
 | 
			
		||||
        innerRadius: 0.5,
 | 
			
		||||
        padAngle: 0.6,
 | 
			
		||||
        valueField: 'value',
 | 
			
		||||
        categoryField: 'type',
 | 
			
		||||
        pie: {
 | 
			
		||||
            style: {
 | 
			
		||||
                cornerRadius: 10
 | 
			
		||||
            },
 | 
			
		||||
            state: {
 | 
			
		||||
                hover: {
 | 
			
		||||
                    outerRadius: 0.85,
 | 
			
		||||
                    stroke: '#000',
 | 
			
		||||
                    lineWidth: 1
 | 
			
		||||
                },
 | 
			
		||||
                selected: {
 | 
			
		||||
                    outerRadius: 0.85,
 | 
			
		||||
                    stroke: '#000',
 | 
			
		||||
                    lineWidth: 1
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        title: {
 | 
			
		||||
            visible: true,
 | 
			
		||||
            text: '模型调用次数占比'
 | 
			
		||||
        },
 | 
			
		||||
        legends: {
 | 
			
		||||
            visible: true,
 | 
			
		||||
            orient: 'left'
 | 
			
		||||
        },
 | 
			
		||||
        label: {
 | 
			
		||||
            visible: true
 | 
			
		||||
        },
 | 
			
		||||
        tooltip: {
 | 
			
		||||
            mark: {
 | 
			
		||||
                content: [
 | 
			
		||||
                    {
 | 
			
		||||
                        key: datum => datum['type'],
 | 
			
		||||
                        value: datum => renderNumber(datum['value'])
 | 
			
		||||
                    }
 | 
			
		||||
                ]
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
        color: {
 | 
			
		||||
            specified: modelColorMap
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const loadQuotaData = async (lineChart, pieChart) => {
 | 
			
		||||
        setLoading(true);
 | 
			
		||||
 | 
			
		||||
        let url = '';
 | 
			
		||||
        let localStartTimestamp = Date.parse(start_timestamp) / 1000;
 | 
			
		||||
        let localEndTimestamp = Date.parse(end_timestamp) / 1000;
 | 
			
		||||
        if (isAdminUser) {
 | 
			
		||||
            url = `/api/data/?username=${username}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
 | 
			
		||||
        } else {
 | 
			
		||||
            url = `/api/data/self/?start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
 | 
			
		||||
        }
 | 
			
		||||
        const res = await API.get(url);
 | 
			
		||||
        const {success, message, data} = res.data;
 | 
			
		||||
        if (success) {
 | 
			
		||||
            setQuotaData(data);
 | 
			
		||||
            if (data.length === 0) {
 | 
			
		||||
                data.push({
 | 
			
		||||
                    'count': 0,
 | 
			
		||||
                    'model_name': '无数据',
 | 
			
		||||
                    'quota': 0,
 | 
			
		||||
                    'created_at': now.getTime() / 1000
 | 
			
		||||
                })
 | 
			
		||||
            }
 | 
			
		||||
            // 根据dataExportDefaultTime重制时间粒度
 | 
			
		||||
            let timeGranularity = 3600;
 | 
			
		||||
            if (dataExportDefaultTime === 'day') {
 | 
			
		||||
                timeGranularity = 86400;
 | 
			
		||||
            } else if (dataExportDefaultTime === 'week') {
 | 
			
		||||
                timeGranularity = 604800;
 | 
			
		||||
            }
 | 
			
		||||
            data.forEach(item => {
 | 
			
		||||
                item['created_at'] = Math.floor(item['created_at'] / timeGranularity) * timeGranularity;
 | 
			
		||||
            });
 | 
			
		||||
            updateChart(lineChart, pieChart, data);
 | 
			
		||||
        } else {
 | 
			
		||||
            showError(message);
 | 
			
		||||
        }
 | 
			
		||||
        setLoading(false);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const refresh = async () => {
 | 
			
		||||
        await loadQuotaData(modelDataChart, modelDataPieChart);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const initChart = async () => {
 | 
			
		||||
        let lineChart = modelDataChart
 | 
			
		||||
        if (!modelDataChart) {
 | 
			
		||||
            lineChart = new VChart(spec_line, {dom: 'model_data'});
 | 
			
		||||
            setModelDataChart(lineChart);
 | 
			
		||||
            lineChart.renderAsync();
 | 
			
		||||
        }
 | 
			
		||||
        let pieChart = modelDataPieChart
 | 
			
		||||
        if (!modelDataPieChart) {
 | 
			
		||||
            pieChart = new VChart(spec_pie, {dom: 'model_pie'});
 | 
			
		||||
            setModelDataPieChart(pieChart);
 | 
			
		||||
            pieChart.renderAsync();
 | 
			
		||||
        }
 | 
			
		||||
        console.log('init vchart');
 | 
			
		||||
        await loadQuotaData(lineChart, pieChart)
 | 
			
		||||
  const handleInputChange = (value, name) => {
 | 
			
		||||
    if (name === 'data_export_default_time') {
 | 
			
		||||
      setDataExportDefaultTime(value);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    setInputs((inputs) => ({ ...inputs, [name]: value }));
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
    const updateChart = (lineChart, pieChart, data) => {
 | 
			
		||||
        if (isAdminUser) {
 | 
			
		||||
            // 将所有用户合并
 | 
			
		||||
        }
 | 
			
		||||
        let pieData = [];
 | 
			
		||||
        let lineData = [];
 | 
			
		||||
        let consumeQuota = 0;
 | 
			
		||||
        let times = 0;
 | 
			
		||||
        for (let i = 0; i < data.length; i++) {
 | 
			
		||||
            const item = data[i];
 | 
			
		||||
            consumeQuota += item.quota;
 | 
			
		||||
            times += item.count;
 | 
			
		||||
            // 合并model_name
 | 
			
		||||
            let pieItem = pieData.find(it => it.type === item.model_name);
 | 
			
		||||
            if (pieItem) {
 | 
			
		||||
                pieItem.value += item.count;
 | 
			
		||||
            } else {
 | 
			
		||||
                pieData.push({
 | 
			
		||||
                    "type": item.model_name,
 | 
			
		||||
                    "value": item.count
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
            // 合并created_at和model_name 为 lineData, created_at 数据类型是小时的时间戳
 | 
			
		||||
            // 转换日期格式
 | 
			
		||||
            let createTime = timestamp2string1(item.created_at, dataExportDefaultTime);
 | 
			
		||||
            let lineItem = lineData.find(it => it.Time === createTime && it.Model === item.model_name);
 | 
			
		||||
            if (lineItem) {
 | 
			
		||||
                lineItem.Usage += parseFloat(getQuotaWithUnit(item.quota));
 | 
			
		||||
            } else {
 | 
			
		||||
                lineData.push({
 | 
			
		||||
                    "Time": createTime,
 | 
			
		||||
                    "Model": item.model_name,
 | 
			
		||||
                    "Usage": parseFloat(getQuotaWithUnit(item.quota))
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        setConsumeQuota(consumeQuota);
 | 
			
		||||
        setTimes(times);
 | 
			
		||||
  const spec_line = {
 | 
			
		||||
    type: 'bar',
 | 
			
		||||
    data: [
 | 
			
		||||
      {
 | 
			
		||||
        id: 'barData',
 | 
			
		||||
        values: [],
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
    xField: 'Time',
 | 
			
		||||
    yField: 'Usage',
 | 
			
		||||
    seriesField: 'Model',
 | 
			
		||||
    stack: true,
 | 
			
		||||
    legends: {
 | 
			
		||||
      visible: true,
 | 
			
		||||
    },
 | 
			
		||||
    title: {
 | 
			
		||||
      visible: true,
 | 
			
		||||
      text: '模型消耗分布',
 | 
			
		||||
      subtext: '0',
 | 
			
		||||
    },
 | 
			
		||||
    bar: {
 | 
			
		||||
      // The state style of bar
 | 
			
		||||
      state: {
 | 
			
		||||
        hover: {
 | 
			
		||||
          stroke: '#000',
 | 
			
		||||
          lineWidth: 1,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    tooltip: {
 | 
			
		||||
      mark: {
 | 
			
		||||
        content: [
 | 
			
		||||
          {
 | 
			
		||||
            key: (datum) => datum['Model'],
 | 
			
		||||
            value: (datum) =>
 | 
			
		||||
              renderQuotaNumberWithDigit(parseFloat(datum['Usage']), 4),
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
      dimension: {
 | 
			
		||||
        content: [
 | 
			
		||||
          {
 | 
			
		||||
            key: (datum) => datum['Model'],
 | 
			
		||||
            value: (datum) => datum['Usage'],
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
        updateContent: (array) => {
 | 
			
		||||
          // sort by value
 | 
			
		||||
          array.sort((a, b) => b.value - a.value);
 | 
			
		||||
          // add $
 | 
			
		||||
          let sum = 0;
 | 
			
		||||
          for (let i = 0; i < array.length; i++) {
 | 
			
		||||
            sum += parseFloat(array[i].value);
 | 
			
		||||
            array[i].value = renderQuotaNumberWithDigit(
 | 
			
		||||
              parseFloat(array[i].value),
 | 
			
		||||
              4,
 | 
			
		||||
            );
 | 
			
		||||
          }
 | 
			
		||||
          // add to first
 | 
			
		||||
          array.unshift({
 | 
			
		||||
            key: '总计',
 | 
			
		||||
            value: renderQuotaNumberWithDigit(sum, 4),
 | 
			
		||||
          });
 | 
			
		||||
          return array;
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    color: {
 | 
			
		||||
      specified: modelColorMap,
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
        // sort by count
 | 
			
		||||
        pieData.sort((a, b) => b.value - a.value);
 | 
			
		||||
        spec_pie.title.subtext = `总计:${renderNumber(times)}`;
 | 
			
		||||
        spec_pie.data[0].values = pieData;
 | 
			
		||||
  const spec_pie = {
 | 
			
		||||
    type: 'pie',
 | 
			
		||||
    data: [
 | 
			
		||||
      {
 | 
			
		||||
        id: 'id0',
 | 
			
		||||
        values: [{ type: 'null', value: '0' }],
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
    outerRadius: 0.8,
 | 
			
		||||
    innerRadius: 0.5,
 | 
			
		||||
    padAngle: 0.6,
 | 
			
		||||
    valueField: 'value',
 | 
			
		||||
    categoryField: 'type',
 | 
			
		||||
    pie: {
 | 
			
		||||
      style: {
 | 
			
		||||
        cornerRadius: 10,
 | 
			
		||||
      },
 | 
			
		||||
      state: {
 | 
			
		||||
        hover: {
 | 
			
		||||
          outerRadius: 0.85,
 | 
			
		||||
          stroke: '#000',
 | 
			
		||||
          lineWidth: 1,
 | 
			
		||||
        },
 | 
			
		||||
        selected: {
 | 
			
		||||
          outerRadius: 0.85,
 | 
			
		||||
          stroke: '#000',
 | 
			
		||||
          lineWidth: 1,
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    title: {
 | 
			
		||||
      visible: true,
 | 
			
		||||
      text: '模型调用次数占比',
 | 
			
		||||
    },
 | 
			
		||||
    legends: {
 | 
			
		||||
      visible: true,
 | 
			
		||||
      orient: 'left',
 | 
			
		||||
    },
 | 
			
		||||
    label: {
 | 
			
		||||
      visible: true,
 | 
			
		||||
    },
 | 
			
		||||
    tooltip: {
 | 
			
		||||
      mark: {
 | 
			
		||||
        content: [
 | 
			
		||||
          {
 | 
			
		||||
            key: (datum) => datum['type'],
 | 
			
		||||
            value: (datum) => renderNumber(datum['value']),
 | 
			
		||||
          },
 | 
			
		||||
        ],
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    color: {
 | 
			
		||||
      specified: modelColorMap,
 | 
			
		||||
    },
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
        spec_line.title.subtext = `总计:${renderQuota(consumeQuota, 2)}`;
 | 
			
		||||
        spec_line.data[0].values = lineData;
 | 
			
		||||
        pieChart.updateSpec(spec_pie);
 | 
			
		||||
        lineChart.updateSpec(spec_line);
 | 
			
		||||
  const loadQuotaData = async (lineChart, pieChart) => {
 | 
			
		||||
    setLoading(true);
 | 
			
		||||
 | 
			
		||||
        // pieChart.updateData('id0', pieData);
 | 
			
		||||
        // lineChart.updateData('barData', lineData);
 | 
			
		||||
        pieChart.reLayout();
 | 
			
		||||
        lineChart.reLayout();
 | 
			
		||||
    let url = '';
 | 
			
		||||
    let localStartTimestamp = Date.parse(start_timestamp) / 1000;
 | 
			
		||||
    let localEndTimestamp = Date.parse(end_timestamp) / 1000;
 | 
			
		||||
    if (isAdminUser) {
 | 
			
		||||
      url = `/api/data/?username=${username}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
 | 
			
		||||
    } else {
 | 
			
		||||
      url = `/api/data/self/?start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&default_time=${dataExportDefaultTime}`;
 | 
			
		||||
    }
 | 
			
		||||
    const res = await API.get(url);
 | 
			
		||||
    const { success, message, data } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      setQuotaData(data);
 | 
			
		||||
      if (data.length === 0) {
 | 
			
		||||
        data.push({
 | 
			
		||||
          count: 0,
 | 
			
		||||
          model_name: '无数据',
 | 
			
		||||
          quota: 0,
 | 
			
		||||
          created_at: now.getTime() / 1000,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      // 根据dataExportDefaultTime重制时间粒度
 | 
			
		||||
      let timeGranularity = 3600;
 | 
			
		||||
      if (dataExportDefaultTime === 'day') {
 | 
			
		||||
        timeGranularity = 86400;
 | 
			
		||||
      } else if (dataExportDefaultTime === 'week') {
 | 
			
		||||
        timeGranularity = 604800;
 | 
			
		||||
      }
 | 
			
		||||
      data.forEach((item) => {
 | 
			
		||||
        item['created_at'] =
 | 
			
		||||
          Math.floor(item['created_at'] / timeGranularity) * timeGranularity;
 | 
			
		||||
      });
 | 
			
		||||
      updateChart(lineChart, pieChart, data);
 | 
			
		||||
    } else {
 | 
			
		||||
      showError(message);
 | 
			
		||||
    }
 | 
			
		||||
    setLoading(false);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        // setDataExportDefaultTime(localStorage.getItem('data_export_default_time'));
 | 
			
		||||
        // if (dataExportDefaultTime === 'day') {
 | 
			
		||||
        //     // 设置开始时间为7天前
 | 
			
		||||
        //     let st = timestamp2string(now.getTime() / 1000 - 86400 * 7)
 | 
			
		||||
        //     inputs.start_timestamp = st;
 | 
			
		||||
        //     formRef.current.formApi.setValue('start_timestamp', st);
 | 
			
		||||
        // }
 | 
			
		||||
        if (!initialized.current) {
 | 
			
		||||
            initialized.current = true;
 | 
			
		||||
            initChart();
 | 
			
		||||
        }
 | 
			
		||||
    }, []);
 | 
			
		||||
  const refresh = async () => {
 | 
			
		||||
    await loadQuotaData(modelDataChart, modelDataPieChart);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <>
 | 
			
		||||
            <Layout>
 | 
			
		||||
                <Layout.Header>
 | 
			
		||||
                    <h3>数据看板</h3>
 | 
			
		||||
                </Layout.Header>
 | 
			
		||||
                <Layout.Content>
 | 
			
		||||
                    <Form ref={formRef} layout='horizontal' style={{marginTop: 10}}>
 | 
			
		||||
                        <>
 | 
			
		||||
                            <Form.DatePicker field="start_timestamp" label='起始时间' style={{width: 272}}
 | 
			
		||||
                                             initValue={start_timestamp}
 | 
			
		||||
                                             value={start_timestamp} type='dateTime'
 | 
			
		||||
                                             name='start_timestamp'
 | 
			
		||||
                                             onChange={value => handleInputChange(value, 'start_timestamp')}/>
 | 
			
		||||
                            <Form.DatePicker field="end_timestamp" fluid label='结束时间' style={{width: 272}}
 | 
			
		||||
                                             initValue={end_timestamp}
 | 
			
		||||
                                             value={end_timestamp} type='dateTime'
 | 
			
		||||
                                             name='end_timestamp'
 | 
			
		||||
                                             onChange={value => handleInputChange(value, 'end_timestamp')}/>
 | 
			
		||||
                            <Form.Select field="data_export_default_time" label='时间粒度' style={{width: 176}}
 | 
			
		||||
                                         initValue={dataExportDefaultTime}
 | 
			
		||||
                                         placeholder={'时间粒度'} name='data_export_default_time'
 | 
			
		||||
                                         optionList={
 | 
			
		||||
                                             [
 | 
			
		||||
                                                 {label: '小时', value: 'hour'},
 | 
			
		||||
                                                 {label: '天', value: 'day'},
 | 
			
		||||
                                                 {label: '周', value: 'week'}
 | 
			
		||||
                                             ]
 | 
			
		||||
                                         }
 | 
			
		||||
                                         onChange={value => handleInputChange(value, 'data_export_default_time')}>
 | 
			
		||||
                            </Form.Select>
 | 
			
		||||
                            {
 | 
			
		||||
                                isAdminUser && <>
 | 
			
		||||
                                    <Form.Input field="username" label='用户名称' style={{width: 176}} value={username}
 | 
			
		||||
                                                placeholder={'可选值'} name='username'
 | 
			
		||||
                                                onChange={value => handleInputChange(value, 'username')}/>
 | 
			
		||||
                                </>
 | 
			
		||||
                            }
 | 
			
		||||
                            <Form.Section>
 | 
			
		||||
                                <Button label='查询' type="primary" htmlType="submit" className="btn-margin-right"
 | 
			
		||||
                                        onClick={refresh} loading={loading}>查询</Button>
 | 
			
		||||
                            </Form.Section>
 | 
			
		||||
                        </>
 | 
			
		||||
                    </Form>
 | 
			
		||||
                    <Spin spinning={loading}>
 | 
			
		||||
                        <div style={{height: 500}}>
 | 
			
		||||
                            <div id="model_pie" style={{width: '100%', minWidth: 100}}></div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                        <div style={{height: 500}}>
 | 
			
		||||
                            <div id="model_data" style={{width: '100%', minWidth: 100}}></div>
 | 
			
		||||
                        </div>
 | 
			
		||||
                    </Spin>
 | 
			
		||||
                </Layout.Content>
 | 
			
		||||
            </Layout>
 | 
			
		||||
        </>
 | 
			
		||||
    );
 | 
			
		||||
  const initChart = async () => {
 | 
			
		||||
    let lineChart = modelDataChart;
 | 
			
		||||
    if (!modelDataChart) {
 | 
			
		||||
      lineChart = new VChart(spec_line, { dom: 'model_data' });
 | 
			
		||||
      setModelDataChart(lineChart);
 | 
			
		||||
      lineChart.renderAsync();
 | 
			
		||||
    }
 | 
			
		||||
    let pieChart = modelDataPieChart;
 | 
			
		||||
    if (!modelDataPieChart) {
 | 
			
		||||
      pieChart = new VChart(spec_pie, { dom: 'model_pie' });
 | 
			
		||||
      setModelDataPieChart(pieChart);
 | 
			
		||||
      pieChart.renderAsync();
 | 
			
		||||
    }
 | 
			
		||||
    console.log('init vchart');
 | 
			
		||||
    await loadQuotaData(lineChart, pieChart);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const updateChart = (lineChart, pieChart, data) => {
 | 
			
		||||
    if (isAdminUser) {
 | 
			
		||||
      // 将所有用户合并
 | 
			
		||||
    }
 | 
			
		||||
    let pieData = [];
 | 
			
		||||
    let lineData = [];
 | 
			
		||||
    let consumeQuota = 0;
 | 
			
		||||
    let times = 0;
 | 
			
		||||
    for (let i = 0; i < data.length; i++) {
 | 
			
		||||
      const item = data[i];
 | 
			
		||||
      consumeQuota += item.quota;
 | 
			
		||||
      times += item.count;
 | 
			
		||||
      // 合并model_name
 | 
			
		||||
      let pieItem = pieData.find((it) => it.type === item.model_name);
 | 
			
		||||
      if (pieItem) {
 | 
			
		||||
        pieItem.value += item.count;
 | 
			
		||||
      } else {
 | 
			
		||||
        pieData.push({
 | 
			
		||||
          type: item.model_name,
 | 
			
		||||
          value: item.count,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      // 合并created_at和model_name 为 lineData, created_at 数据类型是小时的时间戳
 | 
			
		||||
      // 转换日期格式
 | 
			
		||||
      let createTime = timestamp2string1(
 | 
			
		||||
        item.created_at,
 | 
			
		||||
        dataExportDefaultTime,
 | 
			
		||||
      );
 | 
			
		||||
      let lineItem = lineData.find(
 | 
			
		||||
        (it) => it.Time === createTime && it.Model === item.model_name,
 | 
			
		||||
      );
 | 
			
		||||
      if (lineItem) {
 | 
			
		||||
        lineItem.Usage += parseFloat(getQuotaWithUnit(item.quota));
 | 
			
		||||
      } else {
 | 
			
		||||
        lineData.push({
 | 
			
		||||
          Time: createTime,
 | 
			
		||||
          Model: item.model_name,
 | 
			
		||||
          Usage: parseFloat(getQuotaWithUnit(item.quota)),
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    setConsumeQuota(consumeQuota);
 | 
			
		||||
    setTimes(times);
 | 
			
		||||
 | 
			
		||||
    // sort by count
 | 
			
		||||
    pieData.sort((a, b) => b.value - a.value);
 | 
			
		||||
    spec_pie.title.subtext = `总计:${renderNumber(times)}`;
 | 
			
		||||
    spec_pie.data[0].values = pieData;
 | 
			
		||||
 | 
			
		||||
    spec_line.title.subtext = `总计:${renderQuota(consumeQuota, 2)}`;
 | 
			
		||||
    spec_line.data[0].values = lineData;
 | 
			
		||||
    pieChart.updateSpec(spec_pie);
 | 
			
		||||
    lineChart.updateSpec(spec_line);
 | 
			
		||||
 | 
			
		||||
    // pieChart.updateData('id0', pieData);
 | 
			
		||||
    // lineChart.updateData('barData', lineData);
 | 
			
		||||
    pieChart.reLayout();
 | 
			
		||||
    lineChart.reLayout();
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    // setDataExportDefaultTime(localStorage.getItem('data_export_default_time'));
 | 
			
		||||
    // if (dataExportDefaultTime === 'day') {
 | 
			
		||||
    //     // 设置开始时间为7天前
 | 
			
		||||
    //     let st = timestamp2string(now.getTime() / 1000 - 86400 * 7)
 | 
			
		||||
    //     inputs.start_timestamp = st;
 | 
			
		||||
    //     formRef.current.formApi.setValue('start_timestamp', st);
 | 
			
		||||
    // }
 | 
			
		||||
    if (!initialized.current) {
 | 
			
		||||
      initVChartSemiTheme({
 | 
			
		||||
        isWatchingThemeSwitch: true,
 | 
			
		||||
      });
 | 
			
		||||
      initialized.current = true;
 | 
			
		||||
      initChart();
 | 
			
		||||
    }
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <Layout>
 | 
			
		||||
        <Layout.Header>
 | 
			
		||||
          <h3>数据看板</h3>
 | 
			
		||||
        </Layout.Header>
 | 
			
		||||
        <Layout.Content>
 | 
			
		||||
          <Form ref={formRef} layout='horizontal' style={{ marginTop: 10 }}>
 | 
			
		||||
            <>
 | 
			
		||||
              <Form.DatePicker
 | 
			
		||||
                field='start_timestamp'
 | 
			
		||||
                label='起始时间'
 | 
			
		||||
                style={{ width: 272 }}
 | 
			
		||||
                initValue={start_timestamp}
 | 
			
		||||
                value={start_timestamp}
 | 
			
		||||
                type='dateTime'
 | 
			
		||||
                name='start_timestamp'
 | 
			
		||||
                onChange={(value) =>
 | 
			
		||||
                  handleInputChange(value, 'start_timestamp')
 | 
			
		||||
                }
 | 
			
		||||
              />
 | 
			
		||||
              <Form.DatePicker
 | 
			
		||||
                field='end_timestamp'
 | 
			
		||||
                fluid
 | 
			
		||||
                label='结束时间'
 | 
			
		||||
                style={{ width: 272 }}
 | 
			
		||||
                initValue={end_timestamp}
 | 
			
		||||
                value={end_timestamp}
 | 
			
		||||
                type='dateTime'
 | 
			
		||||
                name='end_timestamp'
 | 
			
		||||
                onChange={(value) => handleInputChange(value, 'end_timestamp')}
 | 
			
		||||
              />
 | 
			
		||||
              <Form.Select
 | 
			
		||||
                field='data_export_default_time'
 | 
			
		||||
                label='时间粒度'
 | 
			
		||||
                style={{ width: 176 }}
 | 
			
		||||
                initValue={dataExportDefaultTime}
 | 
			
		||||
                placeholder={'时间粒度'}
 | 
			
		||||
                name='data_export_default_time'
 | 
			
		||||
                optionList={[
 | 
			
		||||
                  { label: '小时', value: 'hour' },
 | 
			
		||||
                  { label: '天', value: 'day' },
 | 
			
		||||
                  { label: '周', value: 'week' },
 | 
			
		||||
                ]}
 | 
			
		||||
                onChange={(value) =>
 | 
			
		||||
                  handleInputChange(value, 'data_export_default_time')
 | 
			
		||||
                }
 | 
			
		||||
              ></Form.Select>
 | 
			
		||||
              {isAdminUser && (
 | 
			
		||||
                <>
 | 
			
		||||
                  <Form.Input
 | 
			
		||||
                    field='username'
 | 
			
		||||
                    label='用户名称'
 | 
			
		||||
                    style={{ width: 176 }}
 | 
			
		||||
                    value={username}
 | 
			
		||||
                    placeholder={'可选值'}
 | 
			
		||||
                    name='username'
 | 
			
		||||
                    onChange={(value) => handleInputChange(value, 'username')}
 | 
			
		||||
                  />
 | 
			
		||||
                </>
 | 
			
		||||
              )}
 | 
			
		||||
              <Form.Section>
 | 
			
		||||
                <Button
 | 
			
		||||
                  label='查询'
 | 
			
		||||
                  type='primary'
 | 
			
		||||
                  htmlType='submit'
 | 
			
		||||
                  className='btn-margin-right'
 | 
			
		||||
                  onClick={refresh}
 | 
			
		||||
                  loading={loading}
 | 
			
		||||
                >
 | 
			
		||||
                  查询
 | 
			
		||||
                </Button>
 | 
			
		||||
              </Form.Section>
 | 
			
		||||
            </>
 | 
			
		||||
          </Form>
 | 
			
		||||
          <Spin spinning={loading}>
 | 
			
		||||
            <div style={{ height: 500 }}>
 | 
			
		||||
              <div
 | 
			
		||||
                id='model_pie'
 | 
			
		||||
                style={{ width: '100%', minWidth: 100 }}
 | 
			
		||||
              ></div>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div style={{ height: 500 }}>
 | 
			
		||||
              <div
 | 
			
		||||
                id='model_data'
 | 
			
		||||
                style={{ width: '100%', minWidth: 100 }}
 | 
			
		||||
              ></div>
 | 
			
		||||
            </div>
 | 
			
		||||
          </Spin>
 | 
			
		||||
        </Layout.Content>
 | 
			
		||||
      </Layout>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
export default Detail;
 | 
			
		||||
 
 | 
			
		||||
@@ -53,82 +53,121 @@ const Home = () => {
 | 
			
		||||
  }, []);
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      {
 | 
			
		||||
        homePageContentLoaded && homePageContent === '' ?
 | 
			
		||||
          <>
 | 
			
		||||
            <Card
 | 
			
		||||
              bordered={false}
 | 
			
		||||
              headerLine={false}
 | 
			
		||||
              title='系统状况'
 | 
			
		||||
              bodyStyle={{ padding: '10px 20px' }}
 | 
			
		||||
            >
 | 
			
		||||
              <Row gutter={16}>
 | 
			
		||||
                <Col span={12}>
 | 
			
		||||
                  <Card
 | 
			
		||||
                    title='系统信息'
 | 
			
		||||
                    headerExtraContent={<span
 | 
			
		||||
                      style={{ fontSize: '12px', color: 'var(--semi-color-text-1)' }}>系统信息总览</span>}>
 | 
			
		||||
                    <p>名称:{statusState?.status?.system_name}</p>
 | 
			
		||||
                    <p>版本:{statusState?.status?.version ? statusState?.status?.version : 'unknown'}</p>
 | 
			
		||||
                    <p>
 | 
			
		||||
                      源码:
 | 
			
		||||
                      <a
 | 
			
		||||
                        href='https://github.com/songquanpeng/one-api'
 | 
			
		||||
                        target='_blank' rel='noreferrer'
 | 
			
		||||
                      >
 | 
			
		||||
                        https://github.com/songquanpeng/one-api
 | 
			
		||||
                      </a>
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <p>启动时间:{getStartTimeString()}</p>
 | 
			
		||||
                  </Card>
 | 
			
		||||
                </Col>
 | 
			
		||||
                <Col span={12}>
 | 
			
		||||
                  <Card
 | 
			
		||||
                    title='系统配置'
 | 
			
		||||
                    headerExtraContent={<span
 | 
			
		||||
                      style={{ fontSize: '12px', color: 'var(--semi-color-text-1)' }}>系统配置总览</span>}>
 | 
			
		||||
                    <p>
 | 
			
		||||
                      邮箱验证:
 | 
			
		||||
                      {statusState?.status?.email_verification === true ? '已启用' : '未启用'}
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <p>
 | 
			
		||||
                      GitHub 身份验证:
 | 
			
		||||
                      {statusState?.status?.github_oauth === true ? '已启用' : '未启用'}
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <p>
 | 
			
		||||
                      LINUX DO 身份验证:
 | 
			
		||||
                      {statusState?.status?.linuxdo_oauth === true ? '已启用' : '未启用'}
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <p>
 | 
			
		||||
                      微信身份验证:
 | 
			
		||||
                      {statusState?.status?.wechat_login === true ? '已启用' : '未启用'}
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <p>
 | 
			
		||||
                      Turnstile 用户校验:
 | 
			
		||||
                      {statusState?.status?.turnstile_check === true ? '已启用' : '未启用'}
 | 
			
		||||
                    </p>
 | 
			
		||||
                    <p>
 | 
			
		||||
                      Telegram 身份验证:
 | 
			
		||||
                      {statusState?.status?.telegram_oauth === true
 | 
			
		||||
                        ? '已启用' : '未启用'}
 | 
			
		||||
                    </p>
 | 
			
		||||
                  </Card>
 | 
			
		||||
                </Col>
 | 
			
		||||
              </Row>
 | 
			
		||||
            </Card>
 | 
			
		||||
 | 
			
		||||
          </>
 | 
			
		||||
          : <>
 | 
			
		||||
            {
 | 
			
		||||
              homePageContent.startsWith('https://') ?
 | 
			
		||||
                <iframe src={homePageContent} style={{ width: '100%', height: '100vh', border: 'none' }} /> :
 | 
			
		||||
                <div style={{ fontSize: 'larger' }} dangerouslySetInnerHTML={{ __html: homePageContent }}></div>
 | 
			
		||||
            }
 | 
			
		||||
          </>
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      {homePageContentLoaded && homePageContent === '' ? (
 | 
			
		||||
        <>
 | 
			
		||||
          <Card
 | 
			
		||||
            bordered={false}
 | 
			
		||||
            headerLine={false}
 | 
			
		||||
            title='系统状况'
 | 
			
		||||
            bodyStyle={{ padding: '10px 20px' }}
 | 
			
		||||
          >
 | 
			
		||||
            <Row gutter={16}>
 | 
			
		||||
              <Col span={12}>
 | 
			
		||||
                <Card
 | 
			
		||||
                  title='系统信息'
 | 
			
		||||
                  headerExtraContent={
 | 
			
		||||
                    <span
 | 
			
		||||
                      style={{
 | 
			
		||||
                        fontSize: '12px',
 | 
			
		||||
                        color: 'var(--semi-color-text-1)',
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                      系统信息总览
 | 
			
		||||
                    </span>
 | 
			
		||||
                  }
 | 
			
		||||
                >
 | 
			
		||||
                  <p>名称:{statusState?.status?.system_name}</p>
 | 
			
		||||
                  <p>
 | 
			
		||||
                    版本:
 | 
			
		||||
                    {statusState?.status?.version
 | 
			
		||||
                      ? statusState?.status?.version
 | 
			
		||||
                      : 'unknown'}
 | 
			
		||||
                  </p>
 | 
			
		||||
                  <p>
 | 
			
		||||
                    源码:
 | 
			
		||||
                    <a
 | 
			
		||||
                      href='https://github.com/songquanpeng/one-api'
 | 
			
		||||
                      target='_blank'
 | 
			
		||||
                      rel='noreferrer'
 | 
			
		||||
                    >
 | 
			
		||||
                      https://github.com/songquanpeng/one-api
 | 
			
		||||
                    </a>
 | 
			
		||||
                  </p>
 | 
			
		||||
                  <p>启动时间:{getStartTimeString()}</p>
 | 
			
		||||
                </Card>
 | 
			
		||||
              </Col>
 | 
			
		||||
              <Col span={12}>
 | 
			
		||||
                <Card
 | 
			
		||||
                  title='系统配置'
 | 
			
		||||
                  headerExtraContent={
 | 
			
		||||
                    <span
 | 
			
		||||
                      style={{
 | 
			
		||||
                        fontSize: '12px',
 | 
			
		||||
                        color: 'var(--semi-color-text-1)',
 | 
			
		||||
                      }}
 | 
			
		||||
                    >
 | 
			
		||||
                      系统配置总览
 | 
			
		||||
                    </span>
 | 
			
		||||
                  }
 | 
			
		||||
                >
 | 
			
		||||
                  <p>
 | 
			
		||||
                    邮箱验证:
 | 
			
		||||
                    {statusState?.status?.email_verification === true
 | 
			
		||||
                      ? '已启用'
 | 
			
		||||
                      : '未启用'}
 | 
			
		||||
                  </p>
 | 
			
		||||
                  <p>
 | 
			
		||||
                    GitHub 身份验证:
 | 
			
		||||
                    {statusState?.status?.github_oauth === true
 | 
			
		||||
                      ? '已启用'
 | 
			
		||||
                      : '未启用'}
 | 
			
		||||
                  </p>
 | 
			
		||||
                  <p>
 | 
			
		||||
                    LINUX DO 身份验证:
 | 
			
		||||
                    {statusState?.status?.linuxdo_oauth === true
 | 
			
		||||
                      ? '已启用'
 | 
			
		||||
                      : '未启用'}
 | 
			
		||||
                  </p>
 | 
			
		||||
                  <p>
 | 
			
		||||
                    微信身份验证:
 | 
			
		||||
                    {statusState?.status?.wechat_login === true
 | 
			
		||||
                      ? '已启用'
 | 
			
		||||
                      : '未启用'}
 | 
			
		||||
                  </p>
 | 
			
		||||
                  <p>
 | 
			
		||||
                    Turnstile 用户校验:
 | 
			
		||||
                    {statusState?.status?.turnstile_check === true
 | 
			
		||||
                      ? '已启用'
 | 
			
		||||
                      : '未启用'}
 | 
			
		||||
                  </p>
 | 
			
		||||
                  <p>
 | 
			
		||||
                    Telegram 身份验证:
 | 
			
		||||
                    {statusState?.status?.telegram_oauth === true
 | 
			
		||||
                      ? '已启用'
 | 
			
		||||
                      : '未启用'}
 | 
			
		||||
                  </p>
 | 
			
		||||
                </Card>
 | 
			
		||||
              </Col>
 | 
			
		||||
            </Row>
 | 
			
		||||
          </Card>
 | 
			
		||||
        </>
 | 
			
		||||
      ) : (
 | 
			
		||||
        <>
 | 
			
		||||
          {homePageContent.startsWith('https://') ? (
 | 
			
		||||
            <iframe
 | 
			
		||||
              src={homePageContent}
 | 
			
		||||
              style={{ width: '100%', height: '100vh', border: 'none' }}
 | 
			
		||||
            />
 | 
			
		||||
          ) : (
 | 
			
		||||
            <div
 | 
			
		||||
              style={{ fontSize: 'larger' }}
 | 
			
		||||
              dangerouslySetInnerHTML={{ __html: homePageContent }}
 | 
			
		||||
            ></div>
 | 
			
		||||
          )}
 | 
			
		||||
        </>
 | 
			
		||||
      )}
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Home;
 | 
			
		||||
export default Home;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,23 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { useNavigate, useParams } from 'react-router-dom';
 | 
			
		||||
import { API, downloadTextAsFile, isMobile, showError, showSuccess } from '../../helpers';
 | 
			
		||||
import {
 | 
			
		||||
  API,
 | 
			
		||||
  downloadTextAsFile,
 | 
			
		||||
  isMobile,
 | 
			
		||||
  showError,
 | 
			
		||||
  showSuccess,
 | 
			
		||||
} from '../../helpers';
 | 
			
		||||
import { renderQuotaWithPrompt } from '../../helpers/render';
 | 
			
		||||
import { AutoComplete, Button, Input, Modal, SideSheet, Space, Spin, Typography } from '@douyinfe/semi-ui';
 | 
			
		||||
import {
 | 
			
		||||
  AutoComplete,
 | 
			
		||||
  Button,
 | 
			
		||||
  Input,
 | 
			
		||||
  Modal,
 | 
			
		||||
  SideSheet,
 | 
			
		||||
  Space,
 | 
			
		||||
  Spin,
 | 
			
		||||
  Typography,
 | 
			
		||||
} from '@douyinfe/semi-ui';
 | 
			
		||||
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
 | 
			
		||||
import { Divider } from 'semantic-ui-react';
 | 
			
		||||
 | 
			
		||||
@@ -15,7 +30,7 @@ const EditRedemption = (props) => {
 | 
			
		||||
  const originInputs = {
 | 
			
		||||
    name: '',
 | 
			
		||||
    quota: 100000,
 | 
			
		||||
    count: 1
 | 
			
		||||
    count: 1,
 | 
			
		||||
  };
 | 
			
		||||
  const [inputs, setInputs] = useState(originInputs);
 | 
			
		||||
  const { name, quota, count } = inputs;
 | 
			
		||||
@@ -42,11 +57,9 @@ const EditRedemption = (props) => {
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (isEdit) {
 | 
			
		||||
      loadRedemption().then(
 | 
			
		||||
        () => {
 | 
			
		||||
          // console.log(inputs);
 | 
			
		||||
        }
 | 
			
		||||
      );
 | 
			
		||||
      loadRedemption().then(() => {
 | 
			
		||||
        // console.log(inputs);
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      setInputs(originInputs);
 | 
			
		||||
    }
 | 
			
		||||
@@ -60,10 +73,13 @@ const EditRedemption = (props) => {
 | 
			
		||||
    localInputs.quota = parseInt(localInputs.quota);
 | 
			
		||||
    let res;
 | 
			
		||||
    if (isEdit) {
 | 
			
		||||
      res = await API.put(`/api/redemption/`, { ...localInputs, id: parseInt(props.editingRedemption.id) });
 | 
			
		||||
      res = await API.put(`/api/redemption/`, {
 | 
			
		||||
        ...localInputs,
 | 
			
		||||
        id: parseInt(props.editingRedemption.id),
 | 
			
		||||
      });
 | 
			
		||||
    } else {
 | 
			
		||||
      res = await API.post(`/api/redemption/`, {
 | 
			
		||||
        ...localInputs
 | 
			
		||||
        ...localInputs,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    const { success, message, data } = res.data;
 | 
			
		||||
@@ -97,7 +113,7 @@ const EditRedemption = (props) => {
 | 
			
		||||
        ),
 | 
			
		||||
        onOk: () => {
 | 
			
		||||
          downloadTextAsFile(text, `${inputs.name}.txt`);
 | 
			
		||||
        }
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    setLoading(false);
 | 
			
		||||
@@ -107,15 +123,28 @@ const EditRedemption = (props) => {
 | 
			
		||||
    <>
 | 
			
		||||
      <SideSheet
 | 
			
		||||
        placement={isEdit ? 'right' : 'left'}
 | 
			
		||||
        title={<Title level={3}>{isEdit ? '更新兑换码信息' : '创建新的兑换码'}</Title>}
 | 
			
		||||
        title={
 | 
			
		||||
          <Title level={3}>
 | 
			
		||||
            {isEdit ? '更新兑换码信息' : '创建新的兑换码'}
 | 
			
		||||
          </Title>
 | 
			
		||||
        }
 | 
			
		||||
        headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
 | 
			
		||||
        bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
 | 
			
		||||
        visible={props.visiable}
 | 
			
		||||
        footer={
 | 
			
		||||
          <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
 | 
			
		||||
            <Space>
 | 
			
		||||
              <Button theme="solid" size={'large'} onClick={submit}>提交</Button>
 | 
			
		||||
              <Button theme="solid" size={'large'} type={'tertiary'} onClick={handleCancel}>取消</Button>
 | 
			
		||||
              <Button theme='solid' size={'large'} onClick={submit}>
 | 
			
		||||
                提交
 | 
			
		||||
              </Button>
 | 
			
		||||
              <Button
 | 
			
		||||
                theme='solid'
 | 
			
		||||
                size={'large'}
 | 
			
		||||
                type={'tertiary'}
 | 
			
		||||
                onClick={handleCancel}
 | 
			
		||||
              >
 | 
			
		||||
                取消
 | 
			
		||||
              </Button>
 | 
			
		||||
            </Space>
 | 
			
		||||
          </div>
 | 
			
		||||
        }
 | 
			
		||||
@@ -126,12 +155,12 @@ const EditRedemption = (props) => {
 | 
			
		||||
        <Spin spinning={loading}>
 | 
			
		||||
          <Input
 | 
			
		||||
            style={{ marginTop: 20 }}
 | 
			
		||||
            label="名称"
 | 
			
		||||
            name="name"
 | 
			
		||||
            label='名称'
 | 
			
		||||
            name='name'
 | 
			
		||||
            placeholder={'请输入名称'}
 | 
			
		||||
            onChange={value => handleInputChange('name', value)}
 | 
			
		||||
            onChange={(value) => handleInputChange('name', value)}
 | 
			
		||||
            value={name}
 | 
			
		||||
            autoComplete="new-password"
 | 
			
		||||
            autoComplete='new-password'
 | 
			
		||||
            required={!isEdit}
 | 
			
		||||
          />
 | 
			
		||||
          <Divider />
 | 
			
		||||
@@ -140,12 +169,12 @@ const EditRedemption = (props) => {
 | 
			
		||||
          </div>
 | 
			
		||||
          <AutoComplete
 | 
			
		||||
            style={{ marginTop: 8 }}
 | 
			
		||||
            name="quota"
 | 
			
		||||
            name='quota'
 | 
			
		||||
            placeholder={'请输入额度'}
 | 
			
		||||
            onChange={(value) => handleInputChange('quota', value)}
 | 
			
		||||
            value={quota}
 | 
			
		||||
            autoComplete="new-password"
 | 
			
		||||
            type="number"
 | 
			
		||||
            autoComplete='new-password'
 | 
			
		||||
            type='number'
 | 
			
		||||
            position={'bottom'}
 | 
			
		||||
            data={[
 | 
			
		||||
              { value: 500000, label: '1$' },
 | 
			
		||||
@@ -153,25 +182,25 @@ const EditRedemption = (props) => {
 | 
			
		||||
              { value: 25000000, label: '50$' },
 | 
			
		||||
              { value: 50000000, label: '100$' },
 | 
			
		||||
              { value: 250000000, label: '500$' },
 | 
			
		||||
              { value: 500000000, label: '1000$' }
 | 
			
		||||
              { value: 500000000, label: '1000$' },
 | 
			
		||||
            ]}
 | 
			
		||||
          />
 | 
			
		||||
          {
 | 
			
		||||
            !isEdit && <>
 | 
			
		||||
          {!isEdit && (
 | 
			
		||||
            <>
 | 
			
		||||
              <Divider />
 | 
			
		||||
              <Typography.Text>生成数量</Typography.Text>
 | 
			
		||||
              <Input
 | 
			
		||||
                style={{ marginTop: 8 }}
 | 
			
		||||
                label="生成数量"
 | 
			
		||||
                name="count"
 | 
			
		||||
                label='生成数量'
 | 
			
		||||
                name='count'
 | 
			
		||||
                placeholder={'请输入生成数量'}
 | 
			
		||||
                onChange={value => handleInputChange('count', value)}
 | 
			
		||||
                onChange={(value) => handleInputChange('count', value)}
 | 
			
		||||
                value={count}
 | 
			
		||||
                autoComplete="new-password"
 | 
			
		||||
                type="number"
 | 
			
		||||
                autoComplete='new-password'
 | 
			
		||||
                type='number'
 | 
			
		||||
              />
 | 
			
		||||
            </>
 | 
			
		||||
          }
 | 
			
		||||
          )}
 | 
			
		||||
        </Spin>
 | 
			
		||||
      </SideSheet>
 | 
			
		||||
    </>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,17 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import RedemptionsTable from '../../components/RedemptionsTable';
 | 
			
		||||
import {Layout} from "@douyinfe/semi-ui";
 | 
			
		||||
import { Layout } from '@douyinfe/semi-ui';
 | 
			
		||||
 | 
			
		||||
const Redemption = () => (
 | 
			
		||||
  <>
 | 
			
		||||
      <Layout>
 | 
			
		||||
          <Layout.Header>
 | 
			
		||||
              <h3>管理兑换码</h3>
 | 
			
		||||
          </Layout.Header>
 | 
			
		||||
          <Layout.Content>
 | 
			
		||||
              <RedemptionsTable/>
 | 
			
		||||
          </Layout.Content>
 | 
			
		||||
      </Layout>
 | 
			
		||||
    <Layout>
 | 
			
		||||
      <Layout.Header>
 | 
			
		||||
        <h3>管理兑换码</h3>
 | 
			
		||||
      </Layout.Header>
 | 
			
		||||
      <Layout.Content>
 | 
			
		||||
        <RedemptionsTable />
 | 
			
		||||
      </Layout.Content>
 | 
			
		||||
    </Layout>
 | 
			
		||||
  </>
 | 
			
		||||
);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,53 +1,53 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import SystemSetting from '../../components/SystemSetting';
 | 
			
		||||
import {isRoot} from '../../helpers';
 | 
			
		||||
import { isRoot } from '../../helpers';
 | 
			
		||||
import OtherSetting from '../../components/OtherSetting';
 | 
			
		||||
import PersonalSetting from '../../components/PersonalSetting';
 | 
			
		||||
import OperationSetting from '../../components/OperationSetting';
 | 
			
		||||
import {Layout, TabPane, Tabs} from "@douyinfe/semi-ui";
 | 
			
		||||
import { Layout, TabPane, Tabs } from '@douyinfe/semi-ui';
 | 
			
		||||
 | 
			
		||||
const Setting = () => {
 | 
			
		||||
    let panes = [
 | 
			
		||||
        {
 | 
			
		||||
            tab: '个人设置',
 | 
			
		||||
            content: <PersonalSetting/>,
 | 
			
		||||
            itemKey: '1'
 | 
			
		||||
        }
 | 
			
		||||
    ];
 | 
			
		||||
  let panes = [
 | 
			
		||||
    {
 | 
			
		||||
      tab: '个人设置',
 | 
			
		||||
      content: <PersonalSetting />,
 | 
			
		||||
      itemKey: '1',
 | 
			
		||||
    },
 | 
			
		||||
  ];
 | 
			
		||||
 | 
			
		||||
    if (isRoot()) {
 | 
			
		||||
        panes.push({
 | 
			
		||||
            tab: '运营设置',
 | 
			
		||||
            content: <OperationSetting/>,
 | 
			
		||||
            itemKey: '2'
 | 
			
		||||
        });
 | 
			
		||||
        panes.push({
 | 
			
		||||
            tab: '系统设置',
 | 
			
		||||
            content: <SystemSetting/>,
 | 
			
		||||
            itemKey: '3'
 | 
			
		||||
        });
 | 
			
		||||
        panes.push({
 | 
			
		||||
            tab: '其他设置',
 | 
			
		||||
            content: <OtherSetting/>,
 | 
			
		||||
            itemKey: '4'
 | 
			
		||||
        });
 | 
			
		||||
    }
 | 
			
		||||
  if (isRoot()) {
 | 
			
		||||
    panes.push({
 | 
			
		||||
      tab: '运营设置',
 | 
			
		||||
      content: <OperationSetting />,
 | 
			
		||||
      itemKey: '2',
 | 
			
		||||
    });
 | 
			
		||||
    panes.push({
 | 
			
		||||
      tab: '系统设置',
 | 
			
		||||
      content: <SystemSetting />,
 | 
			
		||||
      itemKey: '3',
 | 
			
		||||
    });
 | 
			
		||||
    panes.push({
 | 
			
		||||
      tab: '其他设置',
 | 
			
		||||
      content: <OtherSetting />,
 | 
			
		||||
      itemKey: '4',
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div>
 | 
			
		||||
            <Layout>
 | 
			
		||||
                <Layout.Content>
 | 
			
		||||
                    <Tabs type="line" defaultActiveKey="1">
 | 
			
		||||
                        {panes.map(pane => (
 | 
			
		||||
                            <TabPane itemKey={pane.itemKey} tab={pane.tab}>
 | 
			
		||||
                                {pane.content}
 | 
			
		||||
                            </TabPane>
 | 
			
		||||
                        ))}
 | 
			
		||||
                    </Tabs>
 | 
			
		||||
                </Layout.Content>
 | 
			
		||||
            </Layout>
 | 
			
		||||
        </div>
 | 
			
		||||
    );
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <Layout>
 | 
			
		||||
        <Layout.Content>
 | 
			
		||||
          <Tabs type='line' defaultActiveKey='1'>
 | 
			
		||||
            {panes.map((pane) => (
 | 
			
		||||
              <TabPane itemKey={pane.itemKey} tab={pane.tab}>
 | 
			
		||||
                {pane.content}
 | 
			
		||||
              </TabPane>
 | 
			
		||||
            ))}
 | 
			
		||||
          </Tabs>
 | 
			
		||||
        </Layout.Content>
 | 
			
		||||
      </Layout>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default Setting;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,25 @@
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { useNavigate } from 'react-router-dom';
 | 
			
		||||
import { API, isMobile, showError, showSuccess, timestamp2string } from '../../helpers';
 | 
			
		||||
import {
 | 
			
		||||
  API,
 | 
			
		||||
  isMobile,
 | 
			
		||||
  showError,
 | 
			
		||||
  showSuccess,
 | 
			
		||||
  timestamp2string,
 | 
			
		||||
} from '../../helpers';
 | 
			
		||||
import { renderQuotaWithPrompt } from '../../helpers/render';
 | 
			
		||||
import {
 | 
			
		||||
    AutoComplete,
 | 
			
		||||
    Banner,
 | 
			
		||||
    Button,
 | 
			
		||||
    Checkbox,
 | 
			
		||||
    DatePicker,
 | 
			
		||||
    Input,
 | 
			
		||||
    Select,
 | 
			
		||||
    SideSheet,
 | 
			
		||||
    Space,
 | 
			
		||||
    Spin,
 | 
			
		||||
    Typography
 | 
			
		||||
  AutoComplete,
 | 
			
		||||
  Banner,
 | 
			
		||||
  Button,
 | 
			
		||||
  Checkbox,
 | 
			
		||||
  DatePicker,
 | 
			
		||||
  Input,
 | 
			
		||||
  Select,
 | 
			
		||||
  SideSheet,
 | 
			
		||||
  Space,
 | 
			
		||||
  Spin,
 | 
			
		||||
  Typography,
 | 
			
		||||
} from '@douyinfe/semi-ui';
 | 
			
		||||
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
 | 
			
		||||
import { Divider } from 'semantic-ui-react';
 | 
			
		||||
@@ -27,10 +33,17 @@ const EditToken = (props) => {
 | 
			
		||||
    expired_time: -1,
 | 
			
		||||
    unlimited_quota: false,
 | 
			
		||||
    model_limits_enabled: false,
 | 
			
		||||
    model_limits: []
 | 
			
		||||
    model_limits: [],
 | 
			
		||||
  };
 | 
			
		||||
  const [inputs, setInputs] = useState(originInputs);
 | 
			
		||||
  const { name, remain_quota, expired_time, unlimited_quota, model_limits_enabled, model_limits } = inputs;
 | 
			
		||||
  const {
 | 
			
		||||
    name,
 | 
			
		||||
    remain_quota,
 | 
			
		||||
    expired_time,
 | 
			
		||||
    unlimited_quota,
 | 
			
		||||
    model_limits_enabled,
 | 
			
		||||
    model_limits,
 | 
			
		||||
  } = inputs;
 | 
			
		||||
  // const [visible, setVisible] = useState(false);
 | 
			
		||||
  const [models, setModels] = useState({});
 | 
			
		||||
  const navigate = useNavigate();
 | 
			
		||||
@@ -65,7 +78,7 @@ const EditToken = (props) => {
 | 
			
		||||
    if (success) {
 | 
			
		||||
      let localModelOptions = data.map((model) => ({
 | 
			
		||||
        label: model,
 | 
			
		||||
        value: model
 | 
			
		||||
        value: model,
 | 
			
		||||
      }));
 | 
			
		||||
      setModels(localModelOptions);
 | 
			
		||||
    } else {
 | 
			
		||||
@@ -100,11 +113,9 @@ const EditToken = (props) => {
 | 
			
		||||
    if (!isEdit) {
 | 
			
		||||
      setInputs(originInputs);
 | 
			
		||||
    } else {
 | 
			
		||||
      loadToken().then(
 | 
			
		||||
        () => {
 | 
			
		||||
          // console.log(inputs);
 | 
			
		||||
        }
 | 
			
		||||
      );
 | 
			
		||||
      loadToken().then(() => {
 | 
			
		||||
        // console.log(inputs);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    loadModels();
 | 
			
		||||
  }, [isEdit]);
 | 
			
		||||
@@ -123,10 +134,13 @@ const EditToken = (props) => {
 | 
			
		||||
 | 
			
		||||
  // 生成一个随机的四位字母数字字符串
 | 
			
		||||
  const generateRandomSuffix = () => {
 | 
			
		||||
    const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
 | 
			
		||||
    const characters =
 | 
			
		||||
      'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
 | 
			
		||||
    let result = '';
 | 
			
		||||
    for (let i = 0; i < 6; i++) {
 | 
			
		||||
      result += characters.charAt(Math.floor(Math.random() * characters.length));
 | 
			
		||||
      result += characters.charAt(
 | 
			
		||||
        Math.floor(Math.random() * characters.length),
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    return result;
 | 
			
		||||
  };
 | 
			
		||||
@@ -147,7 +161,10 @@ const EditToken = (props) => {
 | 
			
		||||
        localInputs.expired_time = Math.ceil(time / 1000);
 | 
			
		||||
      }
 | 
			
		||||
      localInputs.model_limits = localInputs.model_limits.join(',');
 | 
			
		||||
      let res = await API.put(`/api/token/`, { ...localInputs, id: parseInt(props.editingToken.id) });
 | 
			
		||||
      let res = await API.put(`/api/token/`, {
 | 
			
		||||
        ...localInputs,
 | 
			
		||||
        id: parseInt(props.editingToken.id),
 | 
			
		||||
      });
 | 
			
		||||
      const { success, message } = res.data;
 | 
			
		||||
      if (success) {
 | 
			
		||||
        showSuccess('令牌更新成功!');
 | 
			
		||||
@@ -189,7 +206,9 @@ const EditToken = (props) => {
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (successCount > 0) {
 | 
			
		||||
        showSuccess(`${successCount}个令牌创建成功,请在列表页面点击复制获取令牌!`);
 | 
			
		||||
        showSuccess(
 | 
			
		||||
          `${successCount}个令牌创建成功,请在列表页面点击复制获取令牌!`,
 | 
			
		||||
        );
 | 
			
		||||
        props.refresh();
 | 
			
		||||
        props.handleClose();
 | 
			
		||||
      }
 | 
			
		||||
@@ -199,20 +218,30 @@ const EditToken = (props) => {
 | 
			
		||||
    setTokenCount(1); // 重置数量为默认值
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <SideSheet
 | 
			
		||||
        placement={isEdit ? 'right' : 'left'}
 | 
			
		||||
        title={<Title level={3}>{isEdit ? '更新令牌信息' : '创建新的令牌'}</Title>}
 | 
			
		||||
        title={
 | 
			
		||||
          <Title level={3}>{isEdit ? '更新令牌信息' : '创建新的令牌'}</Title>
 | 
			
		||||
        }
 | 
			
		||||
        headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
 | 
			
		||||
        bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }}
 | 
			
		||||
        visible={props.visiable}
 | 
			
		||||
        footer={
 | 
			
		||||
          <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
 | 
			
		||||
            <Space>
 | 
			
		||||
              <Button theme="solid" size={'large'} onClick={submit}>提交</Button>
 | 
			
		||||
              <Button theme="solid" size={'large'} type={'tertiary'} onClick={handleCancel}>取消</Button>
 | 
			
		||||
              <Button theme='solid' size={'large'} onClick={submit}>
 | 
			
		||||
                提交
 | 
			
		||||
              </Button>
 | 
			
		||||
              <Button
 | 
			
		||||
                theme='solid'
 | 
			
		||||
                size={'large'}
 | 
			
		||||
                type={'tertiary'}
 | 
			
		||||
                onClick={handleCancel}
 | 
			
		||||
              >
 | 
			
		||||
                取消
 | 
			
		||||
              </Button>
 | 
			
		||||
            </Space>
 | 
			
		||||
          </div>
 | 
			
		||||
        }
 | 
			
		||||
@@ -223,55 +252,79 @@ const EditToken = (props) => {
 | 
			
		||||
        <Spin spinning={loading}>
 | 
			
		||||
          <Input
 | 
			
		||||
            style={{ marginTop: 20 }}
 | 
			
		||||
            label="名称"
 | 
			
		||||
            name="name"
 | 
			
		||||
            label='名称'
 | 
			
		||||
            name='name'
 | 
			
		||||
            placeholder={'请输入名称'}
 | 
			
		||||
            onChange={(value) => handleInputChange('name', value)}
 | 
			
		||||
            value={name}
 | 
			
		||||
            autoComplete="new-password"
 | 
			
		||||
            autoComplete='new-password'
 | 
			
		||||
            required={!isEdit}
 | 
			
		||||
          />
 | 
			
		||||
          <Divider />
 | 
			
		||||
          <DatePicker
 | 
			
		||||
            label="过期时间"
 | 
			
		||||
            name="expired_time"
 | 
			
		||||
            label='过期时间'
 | 
			
		||||
            name='expired_time'
 | 
			
		||||
            placeholder={'请选择过期时间'}
 | 
			
		||||
            onChange={(value) => handleInputChange('expired_time', value)}
 | 
			
		||||
            value={expired_time}
 | 
			
		||||
            autoComplete="new-password"
 | 
			
		||||
            type="dateTime"
 | 
			
		||||
            autoComplete='new-password'
 | 
			
		||||
            type='dateTime'
 | 
			
		||||
          />
 | 
			
		||||
          <div style={{ marginTop: 20 }}>
 | 
			
		||||
            <Space>
 | 
			
		||||
              <Button type={'tertiary'} onClick={() => {
 | 
			
		||||
                setExpiredTime(0, 0, 0, 0);
 | 
			
		||||
              }}>永不过期</Button>
 | 
			
		||||
              <Button type={'tertiary'} onClick={() => {
 | 
			
		||||
                setExpiredTime(0, 0, 1, 0);
 | 
			
		||||
              }}>一小时</Button>
 | 
			
		||||
              <Button type={'tertiary'} onClick={() => {
 | 
			
		||||
                setExpiredTime(1, 0, 0, 0);
 | 
			
		||||
              }}>一个月</Button>
 | 
			
		||||
              <Button type={'tertiary'} onClick={() => {
 | 
			
		||||
                setExpiredTime(0, 1, 0, 0);
 | 
			
		||||
              }}>一天</Button>
 | 
			
		||||
              <Button
 | 
			
		||||
                type={'tertiary'}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  setExpiredTime(0, 0, 0, 0);
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                永不过期
 | 
			
		||||
              </Button>
 | 
			
		||||
              <Button
 | 
			
		||||
                type={'tertiary'}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  setExpiredTime(0, 0, 1, 0);
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                一小时
 | 
			
		||||
              </Button>
 | 
			
		||||
              <Button
 | 
			
		||||
                type={'tertiary'}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  setExpiredTime(1, 0, 0, 0);
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                一个月
 | 
			
		||||
              </Button>
 | 
			
		||||
              <Button
 | 
			
		||||
                type={'tertiary'}
 | 
			
		||||
                onClick={() => {
 | 
			
		||||
                  setExpiredTime(0, 1, 0, 0);
 | 
			
		||||
                }}
 | 
			
		||||
              >
 | 
			
		||||
                一天
 | 
			
		||||
              </Button>
 | 
			
		||||
            </Space>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <Divider />
 | 
			
		||||
          <Banner type={'warning'}
 | 
			
		||||
                  description={'注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。'}></Banner>
 | 
			
		||||
          <Banner
 | 
			
		||||
            type={'warning'}
 | 
			
		||||
            description={
 | 
			
		||||
              '注意,令牌的额度仅用于限制令牌本身的最大额度使用量,实际的使用受到账户的剩余额度限制。'
 | 
			
		||||
            }
 | 
			
		||||
          ></Banner>
 | 
			
		||||
          <div style={{ marginTop: 20 }}>
 | 
			
		||||
            <Typography.Text>{`额度${renderQuotaWithPrompt(remain_quota)}`}</Typography.Text>
 | 
			
		||||
          </div>
 | 
			
		||||
          <AutoComplete
 | 
			
		||||
            style={{ marginTop: 8 }}
 | 
			
		||||
            name="remain_quota"
 | 
			
		||||
            name='remain_quota'
 | 
			
		||||
            placeholder={'请输入额度'}
 | 
			
		||||
            onChange={(value) => handleInputChange('remain_quota', value)}
 | 
			
		||||
            value={remain_quota}
 | 
			
		||||
            autoComplete="new-password"
 | 
			
		||||
            type="number"
 | 
			
		||||
            autoComplete='new-password'
 | 
			
		||||
            type='number'
 | 
			
		||||
            // position={'top'}
 | 
			
		||||
            data={[
 | 
			
		||||
              { value: 500000, label: '1$' },
 | 
			
		||||
@@ -279,7 +332,7 @@ const EditToken = (props) => {
 | 
			
		||||
              { value: 25000000, label: '50$' },
 | 
			
		||||
              { value: 50000000, label: '100$' },
 | 
			
		||||
              { value: 250000000, label: '500$' },
 | 
			
		||||
              { value: 500000000, label: '1000$' }
 | 
			
		||||
              { value: 500000000, label: '1000$' },
 | 
			
		||||
            ]}
 | 
			
		||||
            disabled={unlimited_quota}
 | 
			
		||||
          />
 | 
			
		||||
@@ -291,18 +344,18 @@ const EditToken = (props) => {
 | 
			
		||||
              </div>
 | 
			
		||||
              <AutoComplete
 | 
			
		||||
                style={{ marginTop: 8 }}
 | 
			
		||||
                label="数量"
 | 
			
		||||
                label='数量'
 | 
			
		||||
                placeholder={'请选择或输入创建令牌的数量'}
 | 
			
		||||
                onChange={(value) => handleTokenCountChange(value)}
 | 
			
		||||
                onSelect={(value) => handleTokenCountChange(value)}
 | 
			
		||||
                value={tokenCount.toString()}
 | 
			
		||||
                autoComplete="off"
 | 
			
		||||
                type="number"
 | 
			
		||||
                autoComplete='off'
 | 
			
		||||
                type='number'
 | 
			
		||||
                data={[
 | 
			
		||||
                  { value: 10, label: '10个' },
 | 
			
		||||
                  { value: 20, label: '20个' },
 | 
			
		||||
                  { value: 30, label: '30个' },
 | 
			
		||||
                  { value: 100, label: '100个' }
 | 
			
		||||
                  { value: 100, label: '100个' },
 | 
			
		||||
                ]}
 | 
			
		||||
                disabled={unlimited_quota}
 | 
			
		||||
              />
 | 
			
		||||
@@ -310,35 +363,44 @@ const EditToken = (props) => {
 | 
			
		||||
          )}
 | 
			
		||||
 | 
			
		||||
          <div>
 | 
			
		||||
            <Button style={{ marginTop: 8 }} type={'warning'} onClick={() => {
 | 
			
		||||
              setUnlimitedQuota();
 | 
			
		||||
            }}>{unlimited_quota ? '取消无限额度' : '设为无限额度'}</Button>
 | 
			
		||||
            <Button
 | 
			
		||||
              style={{ marginTop: 8 }}
 | 
			
		||||
              type={'warning'}
 | 
			
		||||
              onClick={() => {
 | 
			
		||||
                setUnlimitedQuota();
 | 
			
		||||
              }}
 | 
			
		||||
            >
 | 
			
		||||
              {unlimited_quota ? '取消无限额度' : '设为无限额度'}
 | 
			
		||||
            </Button>
 | 
			
		||||
          </div>
 | 
			
		||||
          <Divider />
 | 
			
		||||
          <div style={{ marginTop: 10, display: 'flex' }}>
 | 
			
		||||
            <Space>
 | 
			
		||||
              <Checkbox
 | 
			
		||||
                name="model_limits_enabled"
 | 
			
		||||
                name='model_limits_enabled'
 | 
			
		||||
                checked={model_limits_enabled}
 | 
			
		||||
                onChange={(e) => handleInputChange('model_limits_enabled', e.target.checked)}
 | 
			
		||||
              >
 | 
			
		||||
              </Checkbox>
 | 
			
		||||
              <Typography.Text>启用模型限制(非必要,不建议启用)</Typography.Text>
 | 
			
		||||
                onChange={(e) =>
 | 
			
		||||
                  handleInputChange('model_limits_enabled', e.target.checked)
 | 
			
		||||
                }
 | 
			
		||||
              ></Checkbox>
 | 
			
		||||
              <Typography.Text>
 | 
			
		||||
                启用模型限制(非必要,不建议启用)
 | 
			
		||||
              </Typography.Text>
 | 
			
		||||
            </Space>
 | 
			
		||||
          </div>
 | 
			
		||||
 | 
			
		||||
          <Select
 | 
			
		||||
            style={{ marginTop: 8 }}
 | 
			
		||||
            placeholder={'请选择该渠道所支持的模型'}
 | 
			
		||||
            name="models"
 | 
			
		||||
            name='models'
 | 
			
		||||
            required
 | 
			
		||||
            multiple
 | 
			
		||||
            selection
 | 
			
		||||
            onChange={value => {
 | 
			
		||||
            onChange={(value) => {
 | 
			
		||||
              handleInputChange('model_limits', value);
 | 
			
		||||
            }}
 | 
			
		||||
            value={inputs.model_limits}
 | 
			
		||||
            autoComplete="new-password"
 | 
			
		||||
            autoComplete='new-password'
 | 
			
		||||
            optionList={models}
 | 
			
		||||
            disabled={!model_limits_enabled}
 | 
			
		||||
          />
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,14 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import TokensTable from '../../components/TokensTable';
 | 
			
		||||
import {Layout} from "@douyinfe/semi-ui";
 | 
			
		||||
import { Layout } from '@douyinfe/semi-ui';
 | 
			
		||||
const Token = () => (
 | 
			
		||||
  <>
 | 
			
		||||
    <Layout>
 | 
			
		||||
      <Layout.Header>
 | 
			
		||||
          <h3>我的令牌</h3>
 | 
			
		||||
        <h3>我的令牌</h3>
 | 
			
		||||
      </Layout.Header>
 | 
			
		||||
      <Layout.Content>
 | 
			
		||||
          <TokensTable/>
 | 
			
		||||
        <TokensTable />
 | 
			
		||||
      </Layout.Content>
 | 
			
		||||
    </Layout>
 | 
			
		||||
  </>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,298 +1,321 @@
 | 
			
		||||
import React, {useEffect, useState} from 'react';
 | 
			
		||||
import {API, isMobile, showError, showInfo, showSuccess} from '../../helpers';
 | 
			
		||||
import {renderNumber, renderQuota} from '../../helpers/render';
 | 
			
		||||
import {Col, Layout, Row, Typography, Card, Button, Form, Divider, Space, Modal} from "@douyinfe/semi-ui";
 | 
			
		||||
import Title from "@douyinfe/semi-ui/lib/es/typography/title";
 | 
			
		||||
import React, { useEffect, useState } from 'react';
 | 
			
		||||
import { API, isMobile, showError, showInfo, showSuccess } from '../../helpers';
 | 
			
		||||
import { renderNumber, renderQuota } from '../../helpers/render';
 | 
			
		||||
import {
 | 
			
		||||
  Col,
 | 
			
		||||
  Layout,
 | 
			
		||||
  Row,
 | 
			
		||||
  Typography,
 | 
			
		||||
  Card,
 | 
			
		||||
  Button,
 | 
			
		||||
  Form,
 | 
			
		||||
  Divider,
 | 
			
		||||
  Space,
 | 
			
		||||
  Modal,
 | 
			
		||||
} from '@douyinfe/semi-ui';
 | 
			
		||||
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
 | 
			
		||||
import Text from '@douyinfe/semi-ui/lib/es/typography/text';
 | 
			
		||||
import { Link } from 'react-router-dom';
 | 
			
		||||
 | 
			
		||||
const TopUp = () => {
 | 
			
		||||
    const [redemptionCode, setRedemptionCode] = useState('');
 | 
			
		||||
    const [topUpCode, setTopUpCode] = useState('');
 | 
			
		||||
    const [topUpCount, setTopUpCount] = useState(10);
 | 
			
		||||
    const [minTopupCount, setMinTopUpCount] = useState(1);
 | 
			
		||||
    const [payAmount, setPayAmount] = useState(0.0);
 | 
			
		||||
    const [chargedAmount, setChargedAmount] = useState(0.0);
 | 
			
		||||
    const [minTopUp, setMinTopUp] = useState(1);
 | 
			
		||||
    const [topUpLink, setTopUpLink] = useState('');
 | 
			
		||||
    const [paymentEnabled, setPaymentEnabled] = useState(false);
 | 
			
		||||
    const [userQuota, setUserQuota] = useState(0);
 | 
			
		||||
    const [isSubmitting, setIsSubmitting] = useState(false);
 | 
			
		||||
    const [isPaying, setIsPaying] = useState(false);
 | 
			
		||||
    const [open, setOpen] = useState(false);
 | 
			
		||||
    const [payWay, setPayWay] = useState('');
 | 
			
		||||
  const [redemptionCode, setRedemptionCode] = useState('');
 | 
			
		||||
  const [topUpCode, setTopUpCode] = useState('');
 | 
			
		||||
  const [topUpCount, setTopUpCount] = useState(10);
 | 
			
		||||
  const [minTopupCount, setMinTopUpCount] = useState(1);
 | 
			
		||||
  const [payAmount, setPayAmount] = useState(0.0);
 | 
			
		||||
  const [chargedAmount, setChargedAmount] = useState(0.0);
 | 
			
		||||
  const [minTopUp, setMinTopUp] = useState(1);
 | 
			
		||||
  const [topUpLink, setTopUpLink] = useState('');
 | 
			
		||||
  const [paymentEnabled, setPaymentEnabled] = useState(false);
 | 
			
		||||
  const [userQuota, setUserQuota] = useState(0);
 | 
			
		||||
  const [isSubmitting, setIsSubmitting] = useState(false);
 | 
			
		||||
  const [isPaying, setIsPaying] = useState(false);
 | 
			
		||||
  const [open, setOpen] = useState(false);
 | 
			
		||||
  const [payWay, setPayWay] = useState('');
 | 
			
		||||
 | 
			
		||||
    const topUp = async () => {
 | 
			
		||||
        if (redemptionCode === '') {
 | 
			
		||||
            showError('请输入兑换码!')
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        setIsSubmitting(true);
 | 
			
		||||
        try {
 | 
			
		||||
            const res = await API.post('/api/user/topup', {
 | 
			
		||||
                key: redemptionCode
 | 
			
		||||
            });
 | 
			
		||||
            const {success, message, data} = res.data;
 | 
			
		||||
            if (success) {
 | 
			
		||||
                showSuccess('兑换成功!');
 | 
			
		||||
                Modal.success({title: '兑换成功!', content: '成功兑换额度:' + renderQuota(data), centered: true});
 | 
			
		||||
                setUserQuota((quota) => {
 | 
			
		||||
                    return quota + data;
 | 
			
		||||
                });
 | 
			
		||||
                setRedemptionCode('');
 | 
			
		||||
            } else {
 | 
			
		||||
                showError(message);
 | 
			
		||||
            }
 | 
			
		||||
        } catch (err) {
 | 
			
		||||
            showError('请求失败');
 | 
			
		||||
        } finally {
 | 
			
		||||
            setIsSubmitting(false);
 | 
			
		||||
        }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const openTopUpLink = () => {
 | 
			
		||||
        if (!topUpLink) {
 | 
			
		||||
            showError('超级管理员未设置充值链接!');
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        window.open(topUpLink, '_blank');
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    const preTopUp = async (payment) => {
 | 
			
		||||
        if (!paymentEnabled) {
 | 
			
		||||
            showError('管理员未开启在线充值!');
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (!Number.isInteger(Number(topUpCount))) {
 | 
			
		||||
            showError('充值数量必须是整数!');
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        if (payAmount === 0) {
 | 
			
		||||
            await getAmount();
 | 
			
		||||
        }
 | 
			
		||||
        if (topUpCount < minTopUp) {
 | 
			
		||||
            showError('充值数量不能小于' + minTopUp);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        setPayWay(payment)
 | 
			
		||||
        setOpen(true);
 | 
			
		||||
  const topUp = async () => {
 | 
			
		||||
    if (redemptionCode === '') {
 | 
			
		||||
      showError('请输入兑换码!');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    const onlineTopUp = async () => {
 | 
			
		||||
        if (payAmount === 0) {
 | 
			
		||||
            await getAmount();
 | 
			
		||||
        }
 | 
			
		||||
        if (topUpCount < minTopUp) {
 | 
			
		||||
            showError('充值数量不能小于' + minTopUp);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
        setOpen(false);
 | 
			
		||||
        try {
 | 
			
		||||
            setIsPaying(true)
 | 
			
		||||
            const res = await API.post('/api/user/pay', {
 | 
			
		||||
                amount: parseInt(topUpCount),
 | 
			
		||||
                top_up_code: topUpCode,
 | 
			
		||||
                payment_method: payWay
 | 
			
		||||
            });
 | 
			
		||||
            if (res !== undefined) {
 | 
			
		||||
                const {message, data} = res.data;
 | 
			
		||||
                // showInfo(message);
 | 
			
		||||
                if (message === 'success') {
 | 
			
		||||
                    location.href = data.payLink
 | 
			
		||||
                } else {
 | 
			
		||||
                    setIsPaying(false)
 | 
			
		||||
                    showError(data);
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                setIsPaying(false)
 | 
			
		||||
                showError(res);
 | 
			
		||||
            }
 | 
			
		||||
        } catch (err) {
 | 
			
		||||
            console.log(err);
 | 
			
		||||
        } finally {
 | 
			
		||||
        }
 | 
			
		||||
    setIsSubmitting(true);
 | 
			
		||||
    try {
 | 
			
		||||
      const res = await API.post('/api/user/topup', {
 | 
			
		||||
        key: redemptionCode,
 | 
			
		||||
      });
 | 
			
		||||
      const { success, message, data } = res.data;
 | 
			
		||||
      if (success) {
 | 
			
		||||
        showSuccess('兑换成功!');
 | 
			
		||||
        Modal.success({
 | 
			
		||||
          title: '兑换成功!',
 | 
			
		||||
          content: '成功兑换额度:' + renderQuota(data),
 | 
			
		||||
          centered: true,
 | 
			
		||||
        });
 | 
			
		||||
        setUserQuota((quota) => {
 | 
			
		||||
          return quota + data;
 | 
			
		||||
        });
 | 
			
		||||
        setRedemptionCode('');
 | 
			
		||||
      } else {
 | 
			
		||||
        showError(message);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      showError('请求失败');
 | 
			
		||||
    } finally {
 | 
			
		||||
      setIsSubmitting(false);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
    const getUserQuota = async () => {
 | 
			
		||||
        let res = await API.get(`/api/user/self`);
 | 
			
		||||
        const {success, message, data} = res.data;
 | 
			
		||||
        if (success) {
 | 
			
		||||
            setUserQuota(data.quota);
 | 
			
		||||
  const openTopUpLink = () => {
 | 
			
		||||
    if (!topUpLink) {
 | 
			
		||||
      showError('超级管理员未设置充值链接!');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    window.open(topUpLink, '_blank');
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const preTopUp = async (payment) => {
 | 
			
		||||
    if (!paymentEnabled) {
 | 
			
		||||
      showError('管理员未开启在线充值!');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (!Number.isInteger(Number(topUpCount))) {
 | 
			
		||||
      showError('充值数量必须是整数!');
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (payAmount === 0) {
 | 
			
		||||
      await getAmount();
 | 
			
		||||
    }
 | 
			
		||||
    if (topUpCount < minTopUp) {
 | 
			
		||||
      showError('充值数量不能小于' + minTopUp);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    setPayWay(payment);
 | 
			
		||||
    setOpen(true);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const onlineTopUp = async () => {
 | 
			
		||||
    if (payAmount === 0) {
 | 
			
		||||
      await getAmount();
 | 
			
		||||
    }
 | 
			
		||||
    if (topUpCount < minTopUp) {
 | 
			
		||||
      showError('充值数量不能小于' + minTopUp);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    setOpen(false);
 | 
			
		||||
    try {
 | 
			
		||||
      setIsPaying(true);
 | 
			
		||||
      const res = await API.post('/api/user/pay', {
 | 
			
		||||
        amount: parseInt(topUpCount),
 | 
			
		||||
        top_up_code: topUpCode,
 | 
			
		||||
        payment_method: payWay,
 | 
			
		||||
      });
 | 
			
		||||
      if (res !== undefined) {
 | 
			
		||||
        const { message, data } = res.data;
 | 
			
		||||
        // showInfo(message);
 | 
			
		||||
        if (message === 'success') {
 | 
			
		||||
          location.href = data.payLink;
 | 
			
		||||
        } else {
 | 
			
		||||
            showError(message);
 | 
			
		||||
          setIsPaying(false);
 | 
			
		||||
          showError(data);
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        setIsPaying(false);
 | 
			
		||||
        showError(res);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      console.log(err);
 | 
			
		||||
    } finally {
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
    useEffect(() => {
 | 
			
		||||
        let status = localStorage.getItem('status');
 | 
			
		||||
        if (status) {
 | 
			
		||||
            status = JSON.parse(status);
 | 
			
		||||
            if (status.top_up_link) {
 | 
			
		||||
                setTopUpLink(status.top_up_link);
 | 
			
		||||
            }
 | 
			
		||||
            if (status.min_topup) {
 | 
			
		||||
                setMinTopUp(status.min_topup);
 | 
			
		||||
            }
 | 
			
		||||
            if (status.payment_enabled) {
 | 
			
		||||
                setPaymentEnabled(status.payment_enabled);
 | 
			
		||||
            }
 | 
			
		||||
  const getUserQuota = async () => {
 | 
			
		||||
    let res = await API.get(`/api/user/self`);
 | 
			
		||||
    const { success, message, data } = res.data;
 | 
			
		||||
    if (success) {
 | 
			
		||||
      setUserQuota(data.quota);
 | 
			
		||||
    } else {
 | 
			
		||||
      showError(message);
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    let status = localStorage.getItem('status');
 | 
			
		||||
    if (status) {
 | 
			
		||||
      status = JSON.parse(status);
 | 
			
		||||
      if (status.top_up_link) {
 | 
			
		||||
        setTopUpLink(status.top_up_link);
 | 
			
		||||
      }
 | 
			
		||||
      if (status.min_topup) {
 | 
			
		||||
        setMinTopUp(status.min_topup);
 | 
			
		||||
      }
 | 
			
		||||
      if (status.payment_enabled) {
 | 
			
		||||
        setPaymentEnabled(status.payment_enabled);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    getUserQuota().then();
 | 
			
		||||
  }, []);
 | 
			
		||||
 | 
			
		||||
  const renderAmount = () => {
 | 
			
		||||
    // console.log(amount);
 | 
			
		||||
    return payAmount + '元';
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  const getAmount = async (value) => {
 | 
			
		||||
    if (value === undefined) {
 | 
			
		||||
      value = topUpCount;
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      const res = await API.post('/api/user/amount', {
 | 
			
		||||
        amount: parseFloat(value),
 | 
			
		||||
        top_up_code: topUpCode,
 | 
			
		||||
      });
 | 
			
		||||
      if (res !== undefined) {
 | 
			
		||||
        const { message, data } = res.data;
 | 
			
		||||
        // showInfo(message);
 | 
			
		||||
        if (message === 'success') {
 | 
			
		||||
          setPayAmount(parseFloat(data.payAmount));
 | 
			
		||||
          setChargedAmount(parseFloat(data.chargedAmount));
 | 
			
		||||
        } else {
 | 
			
		||||
          showError(data);
 | 
			
		||||
          // setTopUpCount(parseInt(res.data.count));
 | 
			
		||||
          // setAmount(parseInt(data));
 | 
			
		||||
        }
 | 
			
		||||
        getUserQuota().then();
 | 
			
		||||
    }, []);
 | 
			
		||||
 | 
			
		||||
    const renderAmount = () => {
 | 
			
		||||
        // console.log(amount);
 | 
			
		||||
        return payAmount + '元';
 | 
			
		||||
      } else {
 | 
			
		||||
        showError(res);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      console.log(err);
 | 
			
		||||
    } finally {
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
    const getAmount = async (value) => {
 | 
			
		||||
        if (value === undefined) {
 | 
			
		||||
            value = topUpCount;
 | 
			
		||||
        }
 | 
			
		||||
        try {
 | 
			
		||||
            const res = await API.post('/api/user/amount', {
 | 
			
		||||
                amount: parseFloat(value),
 | 
			
		||||
                top_up_code: topUpCode
 | 
			
		||||
            });
 | 
			
		||||
            if (res !== undefined) {
 | 
			
		||||
                const {message, data} = res.data;
 | 
			
		||||
                // showInfo(message);
 | 
			
		||||
                if (message === 'success') {
 | 
			
		||||
                    setPayAmount(parseFloat(data.payAmount));
 | 
			
		||||
                    setChargedAmount(parseFloat(data.chargedAmount));
 | 
			
		||||
                } else {
 | 
			
		||||
                    showError(data);
 | 
			
		||||
                    // setTopUpCount(parseInt(res.data.count));
 | 
			
		||||
                    // setAmount(parseInt(data));
 | 
			
		||||
                }
 | 
			
		||||
            } else {
 | 
			
		||||
                showError(res);
 | 
			
		||||
            }
 | 
			
		||||
        } catch (err) {
 | 
			
		||||
            console.log(err);
 | 
			
		||||
        } finally {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
  const handleCancel = () => {
 | 
			
		||||
    setOpen(false);
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
    const handleCancel = () => {
 | 
			
		||||
        setOpen(false);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return (
 | 
			
		||||
        <div>
 | 
			
		||||
            <Layout>
 | 
			
		||||
                <Layout.Header>
 | 
			
		||||
                    <h3>我的钱包</h3>
 | 
			
		||||
                </Layout.Header>
 | 
			
		||||
                <Layout.Content>
 | 
			
		||||
                    <Modal
 | 
			
		||||
                        title="确定要充值吗"
 | 
			
		||||
                        visible={open}
 | 
			
		||||
                        onOk={onlineTopUp}
 | 
			
		||||
                        onCancel={handleCancel}
 | 
			
		||||
                        maskClosable={false}
 | 
			
		||||
                        size={'small'}
 | 
			
		||||
                        centered={true}
 | 
			
		||||
  return (
 | 
			
		||||
    <div>
 | 
			
		||||
      <Layout>
 | 
			
		||||
        <Layout.Header>
 | 
			
		||||
          <h3>我的钱包</h3>
 | 
			
		||||
        </Layout.Header>
 | 
			
		||||
        <Layout.Content>
 | 
			
		||||
          <Modal
 | 
			
		||||
            title='确定要充值吗'
 | 
			
		||||
            visible={open}
 | 
			
		||||
            onOk={onlineTopUp}
 | 
			
		||||
            onCancel={handleCancel}
 | 
			
		||||
            maskClosable={false}
 | 
			
		||||
            size={'small'}
 | 
			
		||||
            centered={true}
 | 
			
		||||
          >
 | 
			
		||||
            <p>
 | 
			
		||||
              充值数量:{topUpCount}$(实到:{chargedAmount}$)
 | 
			
		||||
            </p>
 | 
			
		||||
            <p>实付金额:{renderAmount()}</p>
 | 
			
		||||
            <p>是否确认充值?</p>
 | 
			
		||||
          </Modal>
 | 
			
		||||
          <div
 | 
			
		||||
            style={{ marginTop: 20, display: 'flex', justifyContent: 'center' }}
 | 
			
		||||
          >
 | 
			
		||||
            <Card style={{ width: '500px', padding: '20px' }}>
 | 
			
		||||
              <Title level={3} style={{ textAlign: 'center' }}>
 | 
			
		||||
                余额 {renderQuota(userQuota)}
 | 
			
		||||
              </Title>
 | 
			
		||||
              <div style={{ marginTop: 20 }}>
 | 
			
		||||
                <Divider>兑换余额</Divider>
 | 
			
		||||
                <Form>
 | 
			
		||||
                  <Form.Input
 | 
			
		||||
                    field={'redemptionCode'}
 | 
			
		||||
                    label={'兑换码'}
 | 
			
		||||
                    placeholder='兑换码'
 | 
			
		||||
                    name='redemptionCode'
 | 
			
		||||
                    value={redemptionCode}
 | 
			
		||||
                    onChange={(value) => {
 | 
			
		||||
                      setRedemptionCode(value);
 | 
			
		||||
                    }}
 | 
			
		||||
                  />
 | 
			
		||||
                  <Space>
 | 
			
		||||
                    {topUpLink ? (
 | 
			
		||||
                      <Button
 | 
			
		||||
                        type={'primary'}
 | 
			
		||||
                        theme={'solid'}
 | 
			
		||||
                        onClick={openTopUpLink}
 | 
			
		||||
                      >
 | 
			
		||||
                        获取兑换码
 | 
			
		||||
                      </Button>
 | 
			
		||||
                    ) : null}
 | 
			
		||||
                    <Button
 | 
			
		||||
                      type={'warning'}
 | 
			
		||||
                      theme={'solid'}
 | 
			
		||||
                      onClick={topUp}
 | 
			
		||||
                      disabled={isSubmitting}
 | 
			
		||||
                    >
 | 
			
		||||
                        <p>充值数量:{topUpCount}$(实到:{chargedAmount}$)</p>
 | 
			
		||||
                        <p>实付金额:{renderAmount()}</p>
 | 
			
		||||
                        <p>是否确认充值?</p>
 | 
			
		||||
                    </Modal>
 | 
			
		||||
                    <div style={{marginTop: 20, display: 'flex', justifyContent: 'center'}}>
 | 
			
		||||
                        <Card
 | 
			
		||||
                            style={{width: '500px', padding: '20px'}}
 | 
			
		||||
                        >
 | 
			
		||||
                            <Title level={3} style={{textAlign: 'center'}}>余额 {renderQuota(userQuota)}</Title>
 | 
			
		||||
                            <div style={{marginTop: 20}}>
 | 
			
		||||
                                <Divider>
 | 
			
		||||
                                    兑换余额
 | 
			
		||||
                                </Divider>
 | 
			
		||||
                                <Form>
 | 
			
		||||
                                    <Form.Input
 | 
			
		||||
                                        field={'redemptionCode'}
 | 
			
		||||
                                        label={'兑换码'}
 | 
			
		||||
                                        placeholder='兑换码'
 | 
			
		||||
                                        name='redemptionCode'
 | 
			
		||||
                                        value={redemptionCode}
 | 
			
		||||
                                        onChange={(value) => {
 | 
			
		||||
                                            setRedemptionCode(value);
 | 
			
		||||
                                        }}
 | 
			
		||||
                                    />
 | 
			
		||||
                                    <Space>
 | 
			
		||||
                                        {
 | 
			
		||||
                                            topUpLink ?
 | 
			
		||||
                                                <Button type={'primary'} theme={'solid'} onClick={openTopUpLink}>
 | 
			
		||||
                                                    获取兑换码
 | 
			
		||||
                                                </Button> : null
 | 
			
		||||
                                        }
 | 
			
		||||
                                        <Button type={"warning"} theme={'solid'} onClick={topUp}
 | 
			
		||||
                                                disabled={isSubmitting}>
 | 
			
		||||
                                            {isSubmitting ? '兑换中...' : '兑换'}
 | 
			
		||||
                                        </Button>
 | 
			
		||||
                                    </Space>
 | 
			
		||||
                                </Form>
 | 
			
		||||
                            </div>
 | 
			
		||||
                            {paymentEnabled ?
 | 
			
		||||
                                <div style={{marginTop: 20}}>
 | 
			
		||||
                                    <Divider>
 | 
			
		||||
                                        在线充值
 | 
			
		||||
                                    </Divider>
 | 
			
		||||
                                    <Form>
 | 
			
		||||
                                        <Form.Input
 | 
			
		||||
                                            disabled={!paymentEnabled}
 | 
			
		||||
                                            field={'redemptionCount'}
 | 
			
		||||
                                            label={'实付金额:' + renderAmount()}
 | 
			
		||||
                                            placeholder={'充值数量,必须整数,最低' + minTopUp + '$'}
 | 
			
		||||
                                            name='redemptionCount'
 | 
			
		||||
                                            type={'number'}
 | 
			
		||||
                                            value={topUpCount}
 | 
			
		||||
                                            suffix={'$'}
 | 
			
		||||
                                            min={minTopUp}
 | 
			
		||||
                                            defaultValue={minTopUp}
 | 
			
		||||
                                            max={100000}
 | 
			
		||||
                                            onChange={async (value) => {
 | 
			
		||||
                                                if (value < 1) {
 | 
			
		||||
                                                    value = 1;
 | 
			
		||||
                                                }
 | 
			
		||||
                                                if (value > 100000) {
 | 
			
		||||
                                                    value = 100000;
 | 
			
		||||
                                                }
 | 
			
		||||
                                                setTopUpCount(value);
 | 
			
		||||
                                                await getAmount(value);
 | 
			
		||||
                                            }}
 | 
			
		||||
                                        />
 | 
			
		||||
                                        <Space>
 | 
			
		||||
                                            <Button style={{backgroundColor: '#b161fe'}}
 | 
			
		||||
                                                    type={'primary'}
 | 
			
		||||
                                                    disabled={isPaying}
 | 
			
		||||
                                                    theme={'solid'} onClick={
 | 
			
		||||
                                                async () => {
 | 
			
		||||
                                                    preTopUp('stripe')
 | 
			
		||||
                                                }
 | 
			
		||||
                                            }>
 | 
			
		||||
                                                {isPaying ? '支付中...' : '去支付'}
 | 
			
		||||
                                            </Button>
 | 
			
		||||
                                        </Space>
 | 
			
		||||
                                    </Form>
 | 
			
		||||
                                </div> : <></>
 | 
			
		||||
                            }
 | 
			
		||||
                            {/*<div style={{ display: 'flex', justifyContent: 'right' }}>*/}
 | 
			
		||||
                            {/*    <Text>*/}
 | 
			
		||||
                            {/*        <Link onClick={*/}
 | 
			
		||||
                            {/*            async () => {*/}
 | 
			
		||||
                            {/*                window.location.href = '/topup/history'*/}
 | 
			
		||||
                            {/*            }*/}
 | 
			
		||||
                            {/*        }>充值记录</Link>*/}
 | 
			
		||||
                            {/*    </Text>*/}
 | 
			
		||||
                            {/*</div>*/}
 | 
			
		||||
                        </Card>
 | 
			
		||||
                    </div>
 | 
			
		||||
 | 
			
		||||
                </Layout.Content>
 | 
			
		||||
            </Layout>
 | 
			
		||||
        </div>
 | 
			
		||||
 | 
			
		||||
    );
 | 
			
		||||
                      {isSubmitting ? '兑换中...' : '兑换'}
 | 
			
		||||
                    </Button>
 | 
			
		||||
                  </Space>
 | 
			
		||||
                </Form>
 | 
			
		||||
              </div>
 | 
			
		||||
              {paymentEnabled ? (
 | 
			
		||||
                <div style={{ marginTop: 20 }}>
 | 
			
		||||
                  <Divider>在线充值</Divider>
 | 
			
		||||
                  <Form>
 | 
			
		||||
                    <Form.Input
 | 
			
		||||
                      disabled={!paymentEnabled}
 | 
			
		||||
                      field={'redemptionCount'}
 | 
			
		||||
                      label={'实付金额:' + renderAmount()}
 | 
			
		||||
                      placeholder={'充值数量,必须整数,最低' + minTopUp + '$'}
 | 
			
		||||
                      name='redemptionCount'
 | 
			
		||||
                      type={'number'}
 | 
			
		||||
                      value={topUpCount}
 | 
			
		||||
                      suffix={'$'}
 | 
			
		||||
                      min={minTopUp}
 | 
			
		||||
                      defaultValue={minTopUp}
 | 
			
		||||
                      max={100000}
 | 
			
		||||
                      onChange={async (value) => {
 | 
			
		||||
                        if (value < 1) {
 | 
			
		||||
                          value = 1;
 | 
			
		||||
                        }
 | 
			
		||||
                        if (value > 100000) {
 | 
			
		||||
                          value = 100000;
 | 
			
		||||
                        }
 | 
			
		||||
                        setTopUpCount(value);
 | 
			
		||||
                        await getAmount(value);
 | 
			
		||||
                      }}
 | 
			
		||||
                    />
 | 
			
		||||
                    <Space>
 | 
			
		||||
                      <Button
 | 
			
		||||
                        style={{ backgroundColor: '#b161fe' }}
 | 
			
		||||
                        type={'primary'}
 | 
			
		||||
                        disabled={isPaying}
 | 
			
		||||
                        theme={'solid'}
 | 
			
		||||
                        onClick={async () => {
 | 
			
		||||
                          preTopUp('stripe');
 | 
			
		||||
                        }}
 | 
			
		||||
                      >
 | 
			
		||||
                        {isPaying ? '支付中...' : '去支付'}
 | 
			
		||||
                      </Button>
 | 
			
		||||
                    </Space>
 | 
			
		||||
                  </Form>
 | 
			
		||||
                </div>
 | 
			
		||||
              ) : (
 | 
			
		||||
                <></>
 | 
			
		||||
              )}
 | 
			
		||||
              {/*<div style={{ display: 'flex', justifyContent: 'right' }}>*/}
 | 
			
		||||
              {/*    <Text>*/}
 | 
			
		||||
              {/*        <Link onClick={*/}
 | 
			
		||||
              {/*            async () => {*/}
 | 
			
		||||
              {/*                window.location.href = '/topup/history'*/}
 | 
			
		||||
              {/*            }*/}
 | 
			
		||||
              {/*        }>充值记录</Link>*/}
 | 
			
		||||
              {/*    </Text>*/}
 | 
			
		||||
              {/*</div>*/}
 | 
			
		||||
            </Card>
 | 
			
		||||
          </div>
 | 
			
		||||
        </Layout.Content>
 | 
			
		||||
      </Layout>
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default TopUp;
 | 
			
		||||
export default TopUp;
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ const AddUser = (props) => {
 | 
			
		||||
  const originInputs = {
 | 
			
		||||
    username: '',
 | 
			
		||||
    display_name: '',
 | 
			
		||||
    password: ''
 | 
			
		||||
    password: '',
 | 
			
		||||
  };
 | 
			
		||||
  const [inputs, setInputs] = useState(originInputs);
 | 
			
		||||
  const [loading, setLoading] = useState(false);
 | 
			
		||||
@@ -48,8 +48,17 @@ const AddUser = (props) => {
 | 
			
		||||
        footer={
 | 
			
		||||
          <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
 | 
			
		||||
            <Space>
 | 
			
		||||
              <Button theme="solid" size={'large'} onClick={submit}>提交</Button>
 | 
			
		||||
              <Button theme="solid" size={'large'} type={'tertiary'} onClick={handleCancel}>取消</Button>
 | 
			
		||||
              <Button theme='solid' size={'large'} onClick={submit}>
 | 
			
		||||
                提交
 | 
			
		||||
              </Button>
 | 
			
		||||
              <Button
 | 
			
		||||
                theme='solid'
 | 
			
		||||
                size={'large'}
 | 
			
		||||
                type={'tertiary'}
 | 
			
		||||
                onClick={handleCancel}
 | 
			
		||||
              >
 | 
			
		||||
                取消
 | 
			
		||||
              </Button>
 | 
			
		||||
            </Space>
 | 
			
		||||
          </div>
 | 
			
		||||
        }
 | 
			
		||||
@@ -60,34 +69,34 @@ const AddUser = (props) => {
 | 
			
		||||
        <Spin spinning={loading}>
 | 
			
		||||
          <Input
 | 
			
		||||
            style={{ marginTop: 20 }}
 | 
			
		||||
            label="用户名"
 | 
			
		||||
            name="username"
 | 
			
		||||
            label='用户名'
 | 
			
		||||
            name='username'
 | 
			
		||||
            addonBefore={'用户名'}
 | 
			
		||||
            placeholder={'请输入用户名'}
 | 
			
		||||
            onChange={value => handleInputChange('username', value)}
 | 
			
		||||
            onChange={(value) => handleInputChange('username', value)}
 | 
			
		||||
            value={username}
 | 
			
		||||
            autoComplete="off"
 | 
			
		||||
            autoComplete='off'
 | 
			
		||||
          />
 | 
			
		||||
          <Input
 | 
			
		||||
            style={{ marginTop: 20 }}
 | 
			
		||||
            addonBefore={'显示名'}
 | 
			
		||||
            label="显示名称"
 | 
			
		||||
            name="display_name"
 | 
			
		||||
            autoComplete="off"
 | 
			
		||||
            label='显示名称'
 | 
			
		||||
            name='display_name'
 | 
			
		||||
            autoComplete='off'
 | 
			
		||||
            placeholder={'请输入显示名称'}
 | 
			
		||||
            onChange={value => handleInputChange('display_name', value)}
 | 
			
		||||
            onChange={(value) => handleInputChange('display_name', value)}
 | 
			
		||||
            value={display_name}
 | 
			
		||||
          />
 | 
			
		||||
          <Input
 | 
			
		||||
            style={{ marginTop: 20 }}
 | 
			
		||||
            label="密 码"
 | 
			
		||||
            name="password"
 | 
			
		||||
            label='密 码'
 | 
			
		||||
            name='password'
 | 
			
		||||
            type={'password'}
 | 
			
		||||
            addonBefore={'密码'}
 | 
			
		||||
            placeholder={'请输入密码'}
 | 
			
		||||
            onChange={value => handleInputChange('password', value)}
 | 
			
		||||
            onChange={(value) => handleInputChange('password', value)}
 | 
			
		||||
            value={password}
 | 
			
		||||
            autoComplete="off"
 | 
			
		||||
            autoComplete='off'
 | 
			
		||||
          />
 | 
			
		||||
        </Spin>
 | 
			
		||||
      </SideSheet>
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,16 @@ import { useNavigate } from 'react-router-dom';
 | 
			
		||||
import { API, isMobile, showError, showSuccess } from '../../helpers';
 | 
			
		||||
import { renderQuotaWithPrompt } from '../../helpers/render';
 | 
			
		||||
import Title from '@douyinfe/semi-ui/lib/es/typography/title';
 | 
			
		||||
import { Button, Divider, Input, Select, SideSheet, Space, Spin, Typography } from '@douyinfe/semi-ui';
 | 
			
		||||
import {
 | 
			
		||||
  Button,
 | 
			
		||||
  Divider,
 | 
			
		||||
  Input,
 | 
			
		||||
  Select,
 | 
			
		||||
  SideSheet,
 | 
			
		||||
  Space,
 | 
			
		||||
  Spin,
 | 
			
		||||
  Typography,
 | 
			
		||||
} from '@douyinfe/semi-ui';
 | 
			
		||||
 | 
			
		||||
const EditUser = (props) => {
 | 
			
		||||
  const userId = props.editingUser.id;
 | 
			
		||||
@@ -19,21 +28,34 @@ const EditUser = (props) => {
 | 
			
		||||
    telegram_id: '',
 | 
			
		||||
    email: '',
 | 
			
		||||
    quota: 0,
 | 
			
		||||
    group: 'default'
 | 
			
		||||
    group: 'default',
 | 
			
		||||
  });
 | 
			
		||||
  const [groupOptions, setGroupOptions] = useState([]);
 | 
			
		||||
  const { username, display_name, password, github_id, linuxdo_id, linuxdo_level, wechat_id, telegram_id, email, quota, group } =
 | 
			
		||||
    inputs;
 | 
			
		||||
  const {
 | 
			
		||||
    username,
 | 
			
		||||
    display_name,
 | 
			
		||||
    password,
 | 
			
		||||
    github_id,
 | 
			
		||||
    linuxdo_id,
 | 
			
		||||
    linuxdo_level,
 | 
			
		||||
    wechat_id,
 | 
			
		||||
    telegram_id,
 | 
			
		||||
    email,
 | 
			
		||||
    quota,
 | 
			
		||||
    group,
 | 
			
		||||
  } = inputs;
 | 
			
		||||
  const handleInputChange = (name, value) => {
 | 
			
		||||
    setInputs((inputs) => ({ ...inputs, [name]: value }));
 | 
			
		||||
  };
 | 
			
		||||
  const fetchGroups = async () => {
 | 
			
		||||
    try {
 | 
			
		||||
      let res = await API.get(`/api/group/`);
 | 
			
		||||
      setGroupOptions(res.data.data.map((group) => ({
 | 
			
		||||
        label: group,
 | 
			
		||||
        value: group
 | 
			
		||||
      })));
 | 
			
		||||
      setGroupOptions(
 | 
			
		||||
        res.data.data.map((group) => ({
 | 
			
		||||
          label: group,
 | 
			
		||||
          value: group,
 | 
			
		||||
        })),
 | 
			
		||||
      );
 | 
			
		||||
    } catch (error) {
 | 
			
		||||
      showError(error.message);
 | 
			
		||||
    }
 | 
			
		||||
@@ -101,8 +123,17 @@ const EditUser = (props) => {
 | 
			
		||||
        footer={
 | 
			
		||||
          <div style={{ display: 'flex', justifyContent: 'flex-end' }}>
 | 
			
		||||
            <Space>
 | 
			
		||||
              <Button theme="solid" size={'large'} onClick={submit}>提交</Button>
 | 
			
		||||
              <Button theme="solid" size={'large'} type={'tertiary'} onClick={handleCancel}>取消</Button>
 | 
			
		||||
              <Button theme='solid' size={'large'} onClick={submit}>
 | 
			
		||||
                提交
 | 
			
		||||
              </Button>
 | 
			
		||||
              <Button
 | 
			
		||||
                theme='solid'
 | 
			
		||||
                size={'large'}
 | 
			
		||||
                type={'tertiary'}
 | 
			
		||||
                onClick={handleCancel}
 | 
			
		||||
              >
 | 
			
		||||
                取消
 | 
			
		||||
              </Button>
 | 
			
		||||
            </Space>
 | 
			
		||||
          </div>
 | 
			
		||||
        }
 | 
			
		||||
@@ -115,116 +146,116 @@ const EditUser = (props) => {
 | 
			
		||||
            <Typography.Text>用户名</Typography.Text>
 | 
			
		||||
          </div>
 | 
			
		||||
          <Input
 | 
			
		||||
            label="用户名"
 | 
			
		||||
            name="username"
 | 
			
		||||
            label='用户名'
 | 
			
		||||
            name='username'
 | 
			
		||||
            placeholder={'请输入新的用户名'}
 | 
			
		||||
            onChange={value => handleInputChange('username', value)}
 | 
			
		||||
            onChange={(value) => handleInputChange('username', value)}
 | 
			
		||||
            value={username}
 | 
			
		||||
            autoComplete="new-password"
 | 
			
		||||
            autoComplete='new-password'
 | 
			
		||||
          />
 | 
			
		||||
          <div style={{ marginTop: 20 }}>
 | 
			
		||||
            <Typography.Text>密码</Typography.Text>
 | 
			
		||||
          </div>
 | 
			
		||||
          <Input
 | 
			
		||||
            label="密码"
 | 
			
		||||
            name="password"
 | 
			
		||||
            label='密码'
 | 
			
		||||
            name='password'
 | 
			
		||||
            type={'password'}
 | 
			
		||||
            placeholder={'请输入新的密码,最短 8 位'}
 | 
			
		||||
            onChange={value => handleInputChange('password', value)}
 | 
			
		||||
            onChange={(value) => handleInputChange('password', value)}
 | 
			
		||||
            value={password}
 | 
			
		||||
            autoComplete="new-password"
 | 
			
		||||
            autoComplete='new-password'
 | 
			
		||||
          />
 | 
			
		||||
          <div style={{ marginTop: 20 }}>
 | 
			
		||||
            <Typography.Text>显示名称</Typography.Text>
 | 
			
		||||
          </div>
 | 
			
		||||
          <Input
 | 
			
		||||
            label="显示名称"
 | 
			
		||||
            name="display_name"
 | 
			
		||||
            label='显示名称'
 | 
			
		||||
            name='display_name'
 | 
			
		||||
            placeholder={'请输入新的显示名称'}
 | 
			
		||||
            onChange={value => handleInputChange('display_name', value)}
 | 
			
		||||
            onChange={(value) => handleInputChange('display_name', value)}
 | 
			
		||||
            value={display_name}
 | 
			
		||||
            autoComplete="new-password"
 | 
			
		||||
            autoComplete='new-password'
 | 
			
		||||
          />
 | 
			
		||||
          {
 | 
			
		||||
            userId && <>
 | 
			
		||||
          {userId && (
 | 
			
		||||
            <>
 | 
			
		||||
              <div style={{ marginTop: 20 }}>
 | 
			
		||||
                <Typography.Text>分组</Typography.Text>
 | 
			
		||||
              </div>
 | 
			
		||||
              <Select
 | 
			
		||||
                placeholder={'请选择分组'}
 | 
			
		||||
                name="group"
 | 
			
		||||
                name='group'
 | 
			
		||||
                fluid
 | 
			
		||||
                search
 | 
			
		||||
                selection
 | 
			
		||||
                allowAdditions
 | 
			
		||||
                additionLabel={'请在系统设置页面编辑分组倍率以添加新的分组:'}
 | 
			
		||||
                onChange={value => handleInputChange('group', value)}
 | 
			
		||||
                onChange={(value) => handleInputChange('group', value)}
 | 
			
		||||
                value={inputs.group}
 | 
			
		||||
                autoComplete="new-password"
 | 
			
		||||
                autoComplete='new-password'
 | 
			
		||||
                optionList={groupOptions}
 | 
			
		||||
              />
 | 
			
		||||
              <div style={{ marginTop: 20 }}>
 | 
			
		||||
                <Typography.Text>{`剩余额度${renderQuotaWithPrompt(quota)}`}</Typography.Text>
 | 
			
		||||
              </div>
 | 
			
		||||
              <Input
 | 
			
		||||
                name="quota"
 | 
			
		||||
                name='quota'
 | 
			
		||||
                placeholder={'请输入新的剩余额度'}
 | 
			
		||||
                onChange={value => handleInputChange('quota', value)}
 | 
			
		||||
                onChange={(value) => handleInputChange('quota', value)}
 | 
			
		||||
                value={quota}
 | 
			
		||||
                type={'number'}
 | 
			
		||||
                autoComplete="new-password"
 | 
			
		||||
                autoComplete='new-password'
 | 
			
		||||
              />
 | 
			
		||||
            </>
 | 
			
		||||
          }
 | 
			
		||||
          )}
 | 
			
		||||
          <Divider style={{ marginTop: 20 }}>以下信息不可修改</Divider>
 | 
			
		||||
          <div style={{ marginTop: 20 }}>
 | 
			
		||||
            <Typography.Text>已绑定的 GitHub 账户</Typography.Text>
 | 
			
		||||
          </div>
 | 
			
		||||
          <Input
 | 
			
		||||
            name="github_id"
 | 
			
		||||
            name='github_id'
 | 
			
		||||
            value={github_id}
 | 
			
		||||
            autoComplete="new-password"
 | 
			
		||||
            placeholder="此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改"
 | 
			
		||||
            autoComplete='new-password'
 | 
			
		||||
            placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
 | 
			
		||||
            readonly
 | 
			
		||||
          />
 | 
			
		||||
          <div style={{ marginTop: 20 }}>
 | 
			
		||||
            <Typography.Text>已绑定的 LINUX DO 账户</Typography.Text>
 | 
			
		||||
          </div>
 | 
			
		||||
          <Input
 | 
			
		||||
              name='linuxdo_id'
 | 
			
		||||
              value={linuxdo_id + '(' + linuxdo_level + '级)'}
 | 
			
		||||
              autoComplete='new-password'
 | 
			
		||||
              placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
 | 
			
		||||
              readonly
 | 
			
		||||
            name='linuxdo_id'
 | 
			
		||||
            value={linuxdo_id + '(' + linuxdo_level + '级)'}
 | 
			
		||||
            autoComplete='new-password'
 | 
			
		||||
            placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
 | 
			
		||||
            readonly
 | 
			
		||||
          />
 | 
			
		||||
          <div style={{ marginTop: 20 }}>
 | 
			
		||||
            <Typography.Text>已绑定的微信账户</Typography.Text>
 | 
			
		||||
          </div>
 | 
			
		||||
          <Input
 | 
			
		||||
            name="wechat_id"
 | 
			
		||||
            name='wechat_id'
 | 
			
		||||
            value={wechat_id}
 | 
			
		||||
            autoComplete="new-password"
 | 
			
		||||
            placeholder="此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改"
 | 
			
		||||
            autoComplete='new-password'
 | 
			
		||||
            placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
 | 
			
		||||
            readonly
 | 
			
		||||
          />
 | 
			
		||||
          <div style={{ marginTop: 20 }}>
 | 
			
		||||
            <Typography.Text>已绑定的 Telegram 账户</Typography.Text>
 | 
			
		||||
          </div>
 | 
			
		||||
          <Input
 | 
			
		||||
            name="telegram_id"
 | 
			
		||||
            name='telegram_id'
 | 
			
		||||
            value={telegram_id}
 | 
			
		||||
            autoComplete="new-password"
 | 
			
		||||
            placeholder="此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改"
 | 
			
		||||
            autoComplete='new-password'
 | 
			
		||||
            placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
 | 
			
		||||
            readonly
 | 
			
		||||
          />
 | 
			
		||||
          <div style={{ marginTop: 20 }}>
 | 
			
		||||
            <Typography.Text>已绑定的邮箱账户</Typography.Text>
 | 
			
		||||
          </div>
 | 
			
		||||
          <Input
 | 
			
		||||
            name="email"
 | 
			
		||||
            name='email'
 | 
			
		||||
            value={email}
 | 
			
		||||
            autoComplete="new-password"
 | 
			
		||||
            placeholder="此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改"
 | 
			
		||||
            autoComplete='new-password'
 | 
			
		||||
            placeholder='此项只读,需要用户通过个人设置页面的相关绑定按钮进行绑定,不可直接修改'
 | 
			
		||||
            readonly
 | 
			
		||||
          />
 | 
			
		||||
        </Spin>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +1,16 @@
 | 
			
		||||
import React from 'react';
 | 
			
		||||
import UsersTable from '../../components/UsersTable';
 | 
			
		||||
import {Layout} from "@douyinfe/semi-ui";
 | 
			
		||||
import { Layout } from '@douyinfe/semi-ui';
 | 
			
		||||
 | 
			
		||||
const User = () => (
 | 
			
		||||
  <>
 | 
			
		||||
    <Layout>
 | 
			
		||||
        <Layout.Header>
 | 
			
		||||
            <h3>管理用户</h3>
 | 
			
		||||
        </Layout.Header>
 | 
			
		||||
        <Layout.Content>
 | 
			
		||||
            <UsersTable/>
 | 
			
		||||
        </Layout.Content>
 | 
			
		||||
      <Layout.Header>
 | 
			
		||||
        <h3>管理用户</h3>
 | 
			
		||||
      </Layout.Header>
 | 
			
		||||
      <Layout.Content>
 | 
			
		||||
        <UsersTable />
 | 
			
		||||
      </Layout.Content>
 | 
			
		||||
    </Layout>
 | 
			
		||||
  </>
 | 
			
		||||
);
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										60
									
								
								web/vite.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								web/vite.config.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,60 @@
 | 
			
		||||
import react from '@vitejs/plugin-react';
 | 
			
		||||
import { defineConfig, transformWithEsbuild } from 'vite';
 | 
			
		||||
 | 
			
		||||
// https://vitejs.dev/config/
 | 
			
		||||
export default defineConfig({
 | 
			
		||||
  plugins: [
 | 
			
		||||
    {
 | 
			
		||||
      name: 'treat-js-files-as-jsx',
 | 
			
		||||
      async transform(code, id) {
 | 
			
		||||
        if (!/src\/.*\.js$/.test(id)) {
 | 
			
		||||
          return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        // Use the exposed transform from vite, instead of directly
 | 
			
		||||
        // transforming with esbuild
 | 
			
		||||
        return transformWithEsbuild(code, id, {
 | 
			
		||||
          loader: 'jsx',
 | 
			
		||||
          jsx: 'automatic',
 | 
			
		||||
        });
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    react(),
 | 
			
		||||
  ],
 | 
			
		||||
  optimizeDeps: {
 | 
			
		||||
    force: true,
 | 
			
		||||
    esbuildOptions: {
 | 
			
		||||
      loader: {
 | 
			
		||||
        '.js': 'jsx',
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  build: {
 | 
			
		||||
    rollupOptions: {
 | 
			
		||||
      output: {
 | 
			
		||||
        manualChunks: {
 | 
			
		||||
          'react-core': ['react', 'react-dom', 'react-router-dom'],
 | 
			
		||||
          'semi-ui': ['@douyinfe/semi-icons', '@douyinfe/semi-ui'],
 | 
			
		||||
          semantic: ['semantic-ui-offline', 'semantic-ui-react'],
 | 
			
		||||
          visactor: ['@visactor/react-vchart', '@visactor/vchart'],
 | 
			
		||||
          tools: ['axios', 'history', 'marked'],
 | 
			
		||||
          'react-components': [
 | 
			
		||||
            'react-dropzone',
 | 
			
		||||
            'react-fireworks',
 | 
			
		||||
            'react-telegram-login',
 | 
			
		||||
            'react-toastify',
 | 
			
		||||
            'react-turnstile',
 | 
			
		||||
          ],
 | 
			
		||||
        },
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
  server: {
 | 
			
		||||
    proxy: {
 | 
			
		||||
      '/api': {
 | 
			
		||||
        target: 'http://localhost:3000',
 | 
			
		||||
        changeOrigin: true,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
  },
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										9250
									
								
								web/yarn.lock
									
									
									
									
									
								
							
							
						
						
									
										9250
									
								
								web/yarn.lock
									
									
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Reference in New Issue
	
	Block a user