diff --git a/Midjourney.md b/Midjourney.md index 5733a11..6fa3aa5 100644 --- a/Midjourney.md +++ b/Midjourney.md @@ -2,9 +2,7 @@ **简介**:Midjourney Proxy API文档 -## 模型价格设置(在设置-运营设置-模型固定价格设置中设置) - -### 模型列表 +## 模型列表 ### midjourney-proxy支持 @@ -27,6 +25,7 @@ - mj_pan (平移) - swap_face (换脸) +## 模型价格设置(在设置-运营设置-模型固定价格设置中设置) ```json { "mj_imagine": 0.1, @@ -46,6 +45,7 @@ "swap_face": 0.05 } ``` +其中mj_inpaint和mj_custom_zoom的价格设置为0,是因为这两个模型需要搭配mj_modal使用,所以价格由mj_modal决定。 ## 渠道设置 @@ -56,12 +56,12 @@ 部署Midjourney-Proxy,并配置好midjourney账号等(强烈建议设置密钥),[项目地址](https://github.com/novicezk/midjourney-proxy) 2. 在渠道管理中添加渠道,渠道类型选择**Midjourney Proxy**,如果是plus版本选择**Midjourney Proxy Plus** - ,模型选择midjourney,如果有换脸模型,可以选择swap_face + ,模型请参考上方模型列表 3. 地址填写midjourney-proxy部署的地址,例如:http://localhost:8080 4. 密钥填写midjourney-proxy的密钥,如果没有设置密钥,可以随便填 ### 对接上游new api -1. 在渠道管理中添加渠道,渠道类型选择**Midjourney Proxy Plus**,模型选择midjourney,如果有换脸模型,可以选择swap_face +1. 在渠道管理中添加渠道,渠道类型选择**Midjourney Proxy Plus**,模型请参考上方模型列表 2. 地址填写上游new api的地址,例如:http://localhost:3000 3. 密钥填写上游new api的密钥 \ No newline at end of file diff --git a/README.md b/README.md index ce3f27f..ea9ba0f 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ 此分叉版本的主要变更如下: 1. 全新的UI界面(部分界面还待更新) -2. 添加[Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy)接口的支持 +2. 添加[Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy)接口的支持,[对接文档](Midjourney.md),支持的接口如下: + [x] /mj/submit/imagine + [x] /mj/submit/change + [x] /mj/submit/blend @@ -54,7 +54,7 @@ 2. 智谱glm-4v,glm-4v识图 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)接口 +5. [Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy)接口,[对接文档](Midjourney.md) 您可以在渠道中添加自定义模型gpt-4-gizmo-*,此模型并非OpenAI官方模型,而是第三方模型,使用官方key无法调用。 @@ -98,6 +98,12 @@ docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:1234 ![image](https://github.com/Calcium-Ion/new-api/assets/61247483/5b3228e8-2556-44f7-97d6-4f8d8ee6effa) ![image](https://github.com/Calcium-Ion/new-api/assets/61247483/af9a07ee-5101-4b3d-8bd9-ae21a4fd7e9e) +## 相关项目 +- [One API](https://github.com/songquanpeng/one-api):原版项目 +- [Midjourney-Proxy](https://github.com/novicezk/midjourney-proxy):Midjourney接口支持 +- [chatnio](https://github.com/Deeptrain-Community/chatnio):下一代 AI 一站式 B/C 端解决方案 +- [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool):用key查询使用额度 + ## Star History [![Star History Chart](https://api.star-history.com/svg?repos=Calcium-Ion/new-api&type=Date)](https://star-history.com/#Calcium-Ion/new-api&Date) diff --git a/common/constants.go b/common/constants.go index 9ba7d9e..2c5e43a 100644 --- a/common/constants.go +++ b/common/constants.go @@ -215,6 +215,7 @@ const ( ChannelTypeGemini = 24 ChannelTypeMoonshot = 25 ChannelTypeZhipu_v4 = 26 + ChannelTypePerplexity = 27 ) var ChannelBaseURLs = []string{ @@ -245,4 +246,5 @@ var ChannelBaseURLs = []string{ "https://generativelanguage.googleapis.com", //24 "https://api.moonshot.cn", //25 "https://open.bigmodel.cn", //26 + "https://api.perplexity.ai", //27 } diff --git a/common/image.go b/common/image.go index 4f2d012..41ff51f 100644 --- a/common/image.go +++ b/common/image.go @@ -5,7 +5,7 @@ import ( "encoding/base64" "errors" "fmt" - "github.com/chai2010/webp" + "golang.org/x/image/webp" "image" "io" "net/http" diff --git a/common/model-ratio.go b/common/model-ratio.go index 5e6163f..7e36105 100644 --- a/common/model-ratio.go +++ b/common/model-ratio.go @@ -13,7 +13,7 @@ import ( // TODO: when a new api is enabled, check the pricing here // 1 === $0.002 / 1K tokens // 1 === ¥0.014 / 1k tokens -var ModelRatio = map[string]float64{ +var DefaultModelRatio = map[string]float64{ //"midjourney": 50, "gpt-4-gizmo-*": 15, "gpt-4": 15, @@ -115,6 +115,7 @@ var DefaultModelPrice = map[string]float64{ } var ModelPrice = map[string]float64{} +var ModelRatio = map[string]float64{} func ModelPrice2JSONString() string { if len(ModelPrice) == 0 { @@ -150,6 +151,9 @@ func GetModelPrice(name string, printErr bool) float64 { } func ModelRatio2JSONString() string { + if len(ModelRatio) == 0 { + ModelRatio = DefaultModelRatio + } jsonBytes, err := json.Marshal(ModelRatio) if err != nil { SysError("error marshalling model ratio: " + err.Error()) @@ -163,6 +167,9 @@ func UpdateModelRatioByJSONString(jsonStr string) error { } func GetModelRatio(name string) float64 { + if len(ModelRatio) == 0 { + ModelRatio = DefaultModelRatio + } if strings.HasPrefix(name, "gpt-4-gizmo") { name = "gpt-4-gizmo-*" } diff --git a/constant/midjourney.go b/constant/midjourney.go index 3d321ca..8b88a44 100644 --- a/constant/midjourney.go +++ b/constant/midjourney.go @@ -1,5 +1,7 @@ package constant +var MjNotifyEnabled = false + const ( MjErrorUnknown = 5 MjRequestError = 4 diff --git a/controller/log.go b/controller/log.go index 3e9721a..3902810 100644 --- a/controller/log.go +++ b/controller/log.go @@ -10,9 +10,13 @@ import ( func GetAllLogs(c *gin.Context) { p, _ := strconv.Atoi(c.Query("p")) + pageSize, _ := strconv.Atoi(c.Query("page_size")) if p < 0 { p = 0 } + if pageSize < 0 { + pageSize = common.ItemsPerPage + } logType, _ := strconv.Atoi(c.Query("type")) startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64) endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64) @@ -20,7 +24,7 @@ func GetAllLogs(c *gin.Context) { tokenName := c.Query("token_name") modelName := c.Query("model_name") channel, _ := strconv.Atoi(c.Query("channel")) - logs, err := model.GetAllLogs(logType, startTimestamp, endTimestamp, modelName, username, tokenName, p*common.ItemsPerPage, common.ItemsPerPage, channel) + logs, err := model.GetAllLogs(logType, startTimestamp, endTimestamp, modelName, username, tokenName, p*pageSize, pageSize, channel) if err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, @@ -38,16 +42,23 @@ func GetAllLogs(c *gin.Context) { func GetUserLogs(c *gin.Context) { p, _ := strconv.Atoi(c.Query("p")) + pageSize, _ := strconv.Atoi(c.Query("page_size")) if p < 0 { p = 0 } + if pageSize < 0 { + pageSize = common.ItemsPerPage + } + if pageSize > 100 { + pageSize = 100 + } userId := c.GetInt("id") logType, _ := strconv.Atoi(c.Query("type")) startTimestamp, _ := strconv.ParseInt(c.Query("start_timestamp"), 10, 64) endTimestamp, _ := strconv.ParseInt(c.Query("end_timestamp"), 10, 64) tokenName := c.Query("token_name") modelName := c.Query("model_name") - logs, err := model.GetUserLogs(userId, logType, startTimestamp, endTimestamp, modelName, tokenName, p*common.ItemsPerPage, common.ItemsPerPage) + logs, err := model.GetUserLogs(userId, logType, startTimestamp, endTimestamp, modelName, tokenName, p*pageSize, pageSize) if err != nil { c.JSON(http.StatusOK, gin.H{ "success": false, diff --git a/controller/misc.go b/controller/misc.go index bf0ec11..fc524a9 100644 --- a/controller/misc.go +++ b/controller/misc.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "one-api/common" + "one-api/constant" "one-api/model" "strings" @@ -61,6 +62,7 @@ func GetStatus(c *gin.Context) { "data_export_default_time": common.DataExportDefaultTime, "default_collapse_sidebar": common.DefaultCollapseSidebar, "enable_online_topup": common.PayAddress != "" && common.EpayId != "" && common.EpayKey != "", + "mj_notify_enabled": constant.MjNotifyEnabled, }, }) return diff --git a/go.mod b/go.mod index 4673085..5b877ad 100644 --- a/go.mod +++ b/go.mod @@ -4,13 +4,12 @@ module one-api go 1.18 require ( - github.com/chai2010/webp v1.1.1 github.com/gin-contrib/cors v1.4.0 github.com/gin-contrib/gzip v0.0.6 github.com/gin-contrib/sessions v0.0.5 github.com/gin-contrib/static v0.0.1 github.com/gin-gonic/gin v1.9.1 - github.com/go-playground/validator/v10 v10.16.0 + github.com/go-playground/validator/v10 v10.19.0 github.com/go-redis/redis/v8 v8.11.5 github.com/golang-jwt/jwt v3.2.2+incompatible github.com/google/uuid v1.3.0 @@ -19,7 +18,8 @@ require ( github.com/samber/lo v1.38.1 github.com/shirou/gopsutil v3.21.11+incompatible github.com/star-horizon/go-epay v0.0.0-20230204124159-fa2e2293fdc2 - golang.org/x/crypto v0.17.0 + golang.org/x/crypto v0.21.0 + golang.org/x/image v0.15.0 gorm.io/driver/mysql v1.4.3 gorm.io/driver/postgres v1.5.2 gorm.io/driver/sqlite v1.4.3 @@ -32,7 +32,7 @@ require ( github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dlclark/regexp2 v1.10.0 // indirect - github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-playground/locales v0.14.1 // indirect @@ -50,7 +50,7 @@ require ( github.com/jinzhu/now v1.1.5 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect - github.com/leodido/go-urn v1.2.4 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect @@ -64,9 +64,9 @@ require ( github.com/yusufpapurcu/wmi v1.2.3 // indirect golang.org/x/arch v0.3.0 // indirect golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect - golang.org/x/net v0.17.0 // indirect + golang.org/x/net v0.21.0 // indirect golang.org/x/sync v0.1.0 // indirect - golang.org/x/sys v0.15.0 // indirect + golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 0006e8d..4ff383b 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,6 @@ github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chai2010/webp v1.1.1 h1:jTRmEccAJ4MGrhFOrPMpNGIJ/eybIgwKpcACsrTEapk= -github.com/chai2010/webp v1.1.1/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= @@ -19,6 +17,8 @@ github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cn github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g= github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs= github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= @@ -37,6 +37,7 @@ github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -47,10 +48,10 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= -github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= -github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-playground/validator/v10 v10.16.0 h1:x+plE831WK4vaKHO/jpgUGsvLKIqRRkz6M78GuJAfGE= github.com/go-playground/validator/v10 v10.16.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/go-playground/validator/v10 v10.19.0 h1:ol+5Fu+cSq9JD7SoSqe04GMI92cbn0+wvQ3bZ8b/AU4= +github.com/go-playground/validator/v10 v10.19.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= @@ -79,8 +80,6 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU= -github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8= github.com/jackc/pgx/v5 v5.5.1 h1:5I9etrGkLrN+2XPCsi6XLlV5DITbSL/xBZdmAxFcXPI= github.com/jackc/pgx/v5 v5.5.1/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= @@ -108,10 +107,10 @@ github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgx github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= @@ -176,15 +175,19 @@ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUu golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc= -golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= +golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM= golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE= +golang.org/x/image v0.15.0 h1:kOELfmgrmJlw4Cdb7g/QGuB3CvDrXbqEIww/pNtNBm8= +golang.org/x/image v0.15.0/go.mod h1:HUYqC05R2ZcZ3ejNQsIHQDQiwWM4JBqmm6MKANTp4LE= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -197,16 +200,14 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= +golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/middleware/distributor.go b/middleware/distributor.go index ed457a3..a5e40b0 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -155,6 +155,9 @@ func Distribute() func(c *gin.Context) { if channel.AutoBan != nil && *channel.AutoBan == 0 { ban = false } + if nil != channel.OpenAIOrganization { + c.Set("channel_organization", *channel.OpenAIOrganization) + } c.Set("auto_ban", ban) c.Set("model_mapping", channel.GetModelMapping()) c.Request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", channel.Key)) diff --git a/model/midjourney.go b/model/midjourney.go index dd065a3..5f85abf 100644 --- a/model/midjourney.go +++ b/model/midjourney.go @@ -4,18 +4,18 @@ type Midjourney struct { Id int `json:"id"` Code int `json:"code"` UserId int `json:"user_id" gorm:"index"` - Action string `json:"action"` + Action string `json:"action" gorm:"type:varchar(40);index"` MjId string `json:"mj_id" gorm:"index"` Prompt string `json:"prompt"` PromptEn string `json:"prompt_en"` Description string `json:"description"` State string `json:"state"` - SubmitTime int64 `json:"submit_time"` - StartTime int64 `json:"start_time"` - FinishTime int64 `json:"finish_time"` + SubmitTime int64 `json:"submit_time" gorm:"index"` + StartTime int64 `json:"start_time" gorm:"index"` + FinishTime int64 `json:"finish_time" gorm:"index"` ImageUrl string `json:"image_url"` - Status string `json:"status"` - Progress string `json:"progress"` + Status string `json:"status" gorm:"type:varchar(20);index"` + Progress string `json:"progress" gorm:"type:varchar(30);index"` FailReason string `json:"fail_reason"` ChannelId int `json:"channel_id"` Quota int `json:"quota"` diff --git a/model/option.go b/model/option.go index 8e08e04..faea053 100644 --- a/model/option.go +++ b/model/option.go @@ -2,6 +2,7 @@ package model import ( "one-api/common" + "one-api/constant" "strconv" "strings" "time" @@ -91,6 +92,7 @@ func InitOptionMap() { common.OptionMap["DataExportInterval"] = strconv.Itoa(common.DataExportInterval) common.OptionMap["DataExportDefaultTime"] = common.DataExportDefaultTime common.OptionMap["DefaultCollapseSidebar"] = strconv.FormatBool(common.DefaultCollapseSidebar) + common.OptionMap["MjNotifyEnabled"] = strconv.FormatBool(constant.MjNotifyEnabled) common.OptionMapRWMutex.Unlock() loadOptionsFromDatabase() @@ -186,6 +188,8 @@ func updateOptionMap(key string, value string) (err error) { common.DataExportEnabled = boolValue case "DefaultCollapseSidebar": common.DefaultCollapseSidebar = boolValue + case "MjNotifyEnabled": + constant.MjNotifyEnabled = boolValue } } switch key { diff --git a/relay/channel/openai/adaptor.go b/relay/channel/openai/adaptor.go index 188ff4c..92621d5 100644 --- a/relay/channel/openai/adaptor.go +++ b/relay/channel/openai/adaptor.go @@ -49,6 +49,9 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, info *re req.Header.Set("api-key", info.ApiKey) return nil } + if info.ChannelType == common.ChannelTypeOpenAI && "" != info.Organization { + req.Header.Set("OpenAI-Organization", info.Organization) + } req.Header.Set("Authorization", "Bearer "+info.ApiKey) //if info.ChannelType == common.ChannelTypeOpenRouter { // req.Header.Set("HTTP-Referer", "https://github.com/songquanpeng/one-api") diff --git a/relay/channel/perplexity/adaptor.go b/relay/channel/perplexity/adaptor.go new file mode 100644 index 0000000..4722bb7 --- /dev/null +++ b/relay/channel/perplexity/adaptor.go @@ -0,0 +1,63 @@ +package perplexity + +import ( + "errors" + "fmt" + "github.com/gin-gonic/gin" + "io" + "net/http" + "one-api/dto" + "one-api/relay/channel" + "one-api/relay/channel/openai" + relaycommon "one-api/relay/common" + "one-api/service" +) + +type Adaptor struct { +} + +func (a *Adaptor) Init(info *relaycommon.RelayInfo, request dto.GeneralOpenAIRequest) { +} + +func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { + return fmt.Sprintf("%s/chat/completions", info.BaseUrl), nil +} + +func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Request, info *relaycommon.RelayInfo) error { + channel.SetupApiRequestHeader(info, c, req) + req.Header.Set("Authorization", "Bearer "+info.ApiKey) + return nil +} + +func (a *Adaptor) ConvertRequest(c *gin.Context, relayMode int, request *dto.GeneralOpenAIRequest) (any, error) { + if request == nil { + return nil, errors.New("request is nil") + } + if request.TopP >= 1 { + request.TopP = 0.99 + } + return requestOpenAI2Perplexity(*request), nil +} + +func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (*http.Response, error) { + return channel.DoApiRequest(a, c, info, requestBody) +} + +func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage *dto.Usage, err *dto.OpenAIErrorWithStatusCode) { + if info.IsStream { + var responseText string + err, responseText = openai.OpenaiStreamHandler(c, resp, info.RelayMode) + usage = service.ResponseText2Usage(responseText, info.UpstreamModelName, info.PromptTokens) + } else { + err, usage = openai.OpenaiHandler(c, resp, info.PromptTokens, info.UpstreamModelName) + } + return +} + +func (a *Adaptor) GetModelList() []string { + return ModelList +} + +func (a *Adaptor) GetChannelName() string { + return ChannelName +} diff --git a/relay/channel/perplexity/constants.go b/relay/channel/perplexity/constants.go new file mode 100644 index 0000000..dc15541 --- /dev/null +++ b/relay/channel/perplexity/constants.go @@ -0,0 +1,7 @@ +package perplexity + +var ModelList = []string{ + "sonar-small-chat", "sonar-small-online", "sonar-medium-chat", "sonar-medium-online", "mistral-7b-instruct", "mixtral-8x7b-instruct", +} + +var ChannelName = "perplexity" diff --git a/relay/channel/perplexity/relay-perplexity.go b/relay/channel/perplexity/relay-perplexity.go new file mode 100644 index 0000000..9772aea --- /dev/null +++ b/relay/channel/perplexity/relay-perplexity.go @@ -0,0 +1,21 @@ +package perplexity + +import "one-api/dto" + +func requestOpenAI2Perplexity(request dto.GeneralOpenAIRequest) *dto.GeneralOpenAIRequest { + messages := make([]dto.Message, 0, len(request.Messages)) + for _, message := range request.Messages { + messages = append(messages, dto.Message{ + Role: message.Role, + Content: message.Content, + }) + } + return &dto.GeneralOpenAIRequest{ + Model: request.Model, + Stream: request.Stream, + Messages: messages, + Temperature: request.Temperature, + TopP: request.TopP, + MaxTokens: request.MaxTokens, + } +} diff --git a/relay/channel/zhipu/adaptor.go b/relay/channel/zhipu/adaptor.go index d437f1b..6f2d186 100644 --- a/relay/channel/zhipu/adaptor.go +++ b/relay/channel/zhipu/adaptor.go @@ -36,6 +36,9 @@ func (a *Adaptor) ConvertRequest(c *gin.Context, relayMode int, request *dto.Gen if request == nil { return nil, errors.New("request is nil") } + if request.TopP >= 1 { + request.TopP = 0.99 + } return requestOpenAI2Zhipu(*request), nil } diff --git a/relay/channel/zhipu_4v/adaptor.go b/relay/channel/zhipu_4v/adaptor.go index 546b048..9007e33 100644 --- a/relay/channel/zhipu_4v/adaptor.go +++ b/relay/channel/zhipu_4v/adaptor.go @@ -34,6 +34,9 @@ func (a *Adaptor) ConvertRequest(c *gin.Context, relayMode int, request *dto.Gen if request == nil { return nil, errors.New("request is nil") } + if request.TopP >= 1 { + request.TopP = 0.99 + } return requestOpenAI2Zhipu(*request), nil } diff --git a/relay/common/relay_info.go b/relay/common/relay_info.go index aea3cc7..27ed9a9 100644 --- a/relay/common/relay_info.go +++ b/relay/common/relay_info.go @@ -24,6 +24,7 @@ type RelayInfo struct { ApiVersion string PromptTokens int ApiKey string + Organization string BaseUrl string } @@ -52,6 +53,7 @@ func GenRelayInfo(c *gin.Context) *RelayInfo { ApiType: apiType, ApiVersion: c.GetString("api_version"), ApiKey: strings.TrimPrefix(c.Request.Header.Get("Authorization"), "Bearer "), + Organization: c.GetString("channel_organization"), } if info.BaseUrl == "" { info.BaseUrl = common.ChannelBaseURLs[channelType] diff --git a/relay/constant/api_type.go b/relay/constant/api_type.go index 51b62c1..8e6f67e 100644 --- a/relay/constant/api_type.go +++ b/relay/constant/api_type.go @@ -16,6 +16,8 @@ const ( APITypeTencent APITypeGemini APITypeZhipu_v4 + APITypeOllama + APITypePerplexity APITypeDummy // this one is only for count, do not add any channel after this ) @@ -43,6 +45,10 @@ func ChannelType2APIType(channelType int) int { apiType = APITypeGemini case common.ChannelTypeZhipu_v4: apiType = APITypeZhipu_v4 + case common.ChannelTypeOllama: + apiType = APITypeOllama + case common.ChannelTypePerplexity: + apiType = APITypePerplexity } return apiType } diff --git a/relay/relay-mj.go b/relay/relay-mj.go index 35353b4..3cd42cb 100644 --- a/relay/relay-mj.go +++ b/relay/relay-mj.go @@ -266,24 +266,6 @@ func RelayMidjourneyTaskImageSeed(c *gin.Context) *dto.MidjourneyResponse { if err != nil { return &midjResponseWithStatus.Response } - //defer func(ctx context.Context) { - // err := model.PostConsumeTokenQuota(tokenId, userQuota, quota, 0, true) - // if err != nil { - // common.SysError("error consuming token remain quota: " + err.Error()) - // } - // err = model.CacheUpdateUserQuota(userId) - // if err != nil { - // common.SysError("error update user quota cache: " + err.Error()) - // } - // if quota != 0 { - // tokenName := c.GetString("token_name") - // logContent := fmt.Sprintf("模型固定价格 %.2f,分组倍率 %.2f,操作 %s", modelPrice, groupRatio, midjRequest.Action) - // model.RecordConsumeLog(ctx, userId, channelId, 0, 0, modelName, tokenName, quota, logContent, tokenId, userQuota, 0, false) - // model.UpdateUserUsedQuotaAndRequestCount(userId, quota) - // channelId := c.GetInt("channel_id") - // model.UpdateChannelUsedQuota(channelId, quota) - // } - //}(c.Request.Context()) midjResponse := &midjResponseWithStatus.Response c.Writer.WriteHeader(midjResponseWithStatus.StatusCode) respBody, err := json.Marshal(midjResponse) diff --git a/relay/relay_adaptor.go b/relay/relay_adaptor.go index cc76270..e6afab5 100644 --- a/relay/relay_adaptor.go +++ b/relay/relay_adaptor.go @@ -6,8 +6,10 @@ import ( "one-api/relay/channel/baidu" "one-api/relay/channel/claude" "one-api/relay/channel/gemini" + "one-api/relay/channel/ollama" "one-api/relay/channel/openai" "one-api/relay/channel/palm" + "one-api/relay/channel/perplexity" "one-api/relay/channel/tencent" "one-api/relay/channel/xunfei" "one-api/relay/channel/zhipu" @@ -39,6 +41,10 @@ func GetAdaptor(apiType int) channel.Adaptor { return &zhipu.Adaptor{} case constant.APITypeZhipu_v4: return &zhipu_4v.Adaptor{} + case constant.APITypeOllama: + return &ollama.Adaptor{} + case constant.APITypePerplexity: + return &perplexity.Adaptor{} } return nil } diff --git a/service/midjourney.go b/service/midjourney.go index 7c47cd6..11ec5bd 100644 --- a/service/midjourney.go +++ b/service/midjourney.go @@ -7,6 +7,7 @@ import ( "io" "log" "net/http" + "one-api/common" "one-api/constant" "one-api/dto" relayconstant "one-api/relay/constant" @@ -158,14 +159,19 @@ func DoMidjourneyHttpRequest(c *gin.Context, timeout time.Duration, fullRequestU //requestBody = c.Request.Body // read request body to json, delete accountFilter and notifyHook var mapResult map[string]interface{} - err := json.NewDecoder(c.Request.Body).Decode(&mapResult) - if err != nil { - return MidjourneyErrorWithStatusCodeWrapper(constant.MjErrorUnknown, "read_request_body_failed", http.StatusInternalServerError), nullBytes, err + // if get request, no need to read request body + if c.Request.Method != "GET" { + err := json.NewDecoder(c.Request.Body).Decode(&mapResult) + if err != nil { + return MidjourneyErrorWithStatusCodeWrapper(constant.MjErrorUnknown, "read_request_body_failed", http.StatusInternalServerError), nullBytes, err + } + delete(mapResult, "accountFilter") + if !constant.MjNotifyEnabled { + delete(mapResult, "notifyHook") + } + //req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody) + // make new request with mapResult } - delete(mapResult, "accountFilter") - delete(mapResult, "notifyHook") - //req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody) - // make new request with mapResult reqBody, err := json.Marshal(mapResult) if err != nil { return MidjourneyErrorWithStatusCodeWrapper(constant.MjErrorUnknown, "marshal_request_body_failed", http.StatusInternalServerError), nullBytes, err @@ -183,10 +189,11 @@ func DoMidjourneyHttpRequest(c *gin.Context, timeout time.Duration, fullRequestU defer cancel() resp, err := GetHttpClient().Do(req) if err != nil { + common.SysError("do request failed: " + err.Error()) return MidjourneyErrorWithStatusCodeWrapper(constant.MjErrorUnknown, "do_request_failed", http.StatusInternalServerError), nullBytes, err } statusCode := resp.StatusCode - //if statusCode != 200 { + //if statusCode != 200 { // return MidjourneyErrorWithStatusCodeWrapper(constant.MjErrorUnknown, "bad_response_status_code", statusCode), nullBytes, nil //} err = req.Body.Close() @@ -207,11 +214,15 @@ func DoMidjourneyHttpRequest(c *gin.Context, timeout time.Duration, fullRequestU if err != nil { return MidjourneyErrorWithStatusCodeWrapper(constant.MjErrorUnknown, "close_response_body_failed", statusCode), responseBody, err } - - err = json.Unmarshal(responseBody, &midjResponse) - log.Printf("responseBody: %s", string(responseBody)) - if err != nil { - return MidjourneyErrorWithStatusCodeWrapper(constant.MjErrorUnknown, "unmarshal_response_body_failed", statusCode), responseBody, err + respStr := string(responseBody) + log.Printf("responseBody: %s", respStr) + if respStr == "" { + return MidjourneyErrorWithStatusCodeWrapper(constant.MjErrorUnknown, "empty_response_body", statusCode), responseBody, nil + } else { + err = json.Unmarshal(responseBody, &midjResponse) + if err != nil { + return MidjourneyErrorWithStatusCodeWrapper(constant.MjErrorUnknown, "unmarshal_response_body_failed", statusCode), responseBody, err + } } //log.Printf("midjResponse: %v", midjResponse) //for k, v := range resp.Header { diff --git a/web/package.json b/web/package.json index d6d7ad5..dc6ca10 100644 --- a/web/package.json +++ b/web/package.json @@ -49,7 +49,7 @@ ] }, "devDependencies": { - "prettier": "^2.7.1", + "prettier": "2.8.8", "typescript": "4.4.2" }, "prettier": { diff --git a/web/src/App.js b/web/src/App.js index 6d4fbc0..3824f81 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -8,13 +8,12 @@ import LoginForm from './components/LoginForm'; import NotFound from './pages/NotFound'; import Setting from './pages/Setting'; import EditUser from './pages/User/EditUser'; -import { API, getLogo, getSystemName, showError, showNotice } from './helpers'; +import { getLogo, getSystemName } from './helpers'; import PasswordResetForm from './components/PasswordResetForm'; import GitHubOAuth from './components/GitHubOAuth'; import LinuxDoOAuth from "./components/LinuxDoOAuth"; import PasswordResetConfirm from './components/PasswordResetConfirm'; import { UserContext } from './context/User'; -import { StatusContext } from './context/Status'; import Channel from './pages/Channel'; import Token from './pages/Token'; import EditChannel from './pages/Channel/EditChannel'; @@ -22,12 +21,13 @@ import Redemption from './pages/Redemption'; import TopUp from './pages/TopUp'; 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 { Layout } from '@douyinfe/semi-ui'; +import Midjourney from './pages/Midjourney'; +import Detail from './pages/Detail'; const Home = lazy(() => import('./pages/Home')); const About = lazy(() => import('./pages/About')); + function App() { const [userState, userDispatch] = useContext(UserContext); // const [statusState, statusDispatch] = useContext(StatusContext); @@ -48,7 +48,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; } @@ -57,193 +57,193 @@ function App() { return ( - - - }> - - - } - /> - - - - } - /> - }> - - - } - /> - }> - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - }> - - - } - /> - }> - - - } - /> - }> - - - } - /> - }> - - - } - /> - }> - - - } - /> - }> - - - } - /> - }> - - - } - /> - }> - - - } - /> - - }> - - - - } - /> - - }> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - }> - - - } - /> - }> - - - } - /> - - } /> - - + + + }> + + + } + /> + + + + } + /> + }> + + + } + /> + }> + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + }> + + + } + /> + + }> + + + + } + /> + + }> + + + + } + /> + + + + } + /> + + + + } + /> + + + + } + /> + }> + + + } + /> + }> + + + } + /> + + } /> + + ); } diff --git a/web/src/components/ChannelsTable.js b/web/src/components/ChannelsTable.js index 4a6b09b..144842e 100644 --- a/web/src/components/ChannelsTable.js +++ b/web/src/components/ChannelsTable.js @@ -1,769 +1,728 @@ -import React, {useEffect, useState} from 'react'; -import { - API, - isMobile, - shouldShowPrompt, - showError, - showInfo, - showSuccess, - timestamp2string -} from '../helpers'; +import React, { useEffect, useState } from 'react'; +import { API, isMobile, shouldShowPrompt, showError, showInfo, showSuccess, timestamp2string } from '../helpers'; -import {CHANNEL_OPTIONS, ITEMS_PER_PAGE} from '../constants'; -import {renderGroup, renderNumber, renderNumberWithPoint, renderQuota, renderQuotaWithPrompt} from '../helpers/render'; +import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../constants'; +import { renderGroup, renderNumberWithPoint, renderQuota } from '../helpers/render'; import { - Avatar, - Tag, - Table, Button, - Popover, + Dropdown, Form, - Modal, + InputNumber, Popconfirm, Space, - Tooltip, + SplitButtonGroup, Switch, - Typography, InputNumber, Dropdown, SplitButtonGroup -} from "@douyinfe/semi-ui"; -import EditChannel from "../pages/Channel/EditChannel"; -import {IconTreeTriangleDown} from "@douyinfe/semi-icons"; + 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; function renderType(type) { - if (!type2label) { - type2label = new Map; - for (let i = 0; i < CHANNEL_OPTIONS.length; i++) { - type2label[CHANNEL_OPTIONS[i].value] = CHANNEL_OPTIONS[i]; - } - type2label[0] = {value: 0, text: '未知类型', color: 'grey'}; - } - return {type2label[type]?.text}; -} - -function renderBalance(type, balance) { - switch (type) { - case 1: // OpenAI - return ${balance.toFixed(2)}; - case 4: // CloseAI - return ¥{balance.toFixed(2)}; - case 8: // 自定义 - return ${balance.toFixed(2)}; - case 5: // OpenAI-SB - return ¥{(balance / 10000).toFixed(2)}; - case 10: // AI Proxy - return {renderNumber(balance)}; - case 12: // API2GPT - return ¥{balance.toFixed(2)}; - case 13: // AIGC2D - return {renderNumber(balance)}; - default: - return 不支持; + if (!type2label) { + type2label = new Map(); + for (let i = 0; i < CHANNEL_OPTIONS.length; i++) { + type2label[CHANNEL_OPTIONS[i].value] = CHANNEL_OPTIONS[i]; } + type2label[0] = { value: 0, text: '未知类型', color: 'grey' }; + } + return {type2label[type]?.text}; } const ChannelsTable = () => { - const columns = [ - // { - // title: '', - // dataIndex: 'checkbox', - // className: 'checkbox', - // }, - { - title: 'ID', - dataIndex: 'id', - }, - { - title: '名称', - dataIndex: 'name', - }, - { - title: '分组', - dataIndex: 'group', - render: (text, record, index) => { - return ( -
- - { - text.split(',').map((item, index) => { - return (renderGroup(item)) - }) - } - -
- ); - }, - }, - { - title: '类型', - dataIndex: 'type', - render: (text, record, index) => { - return ( -
- {renderType(text)} -
- ); - }, - }, - { - title: '状态', - dataIndex: 'status', - render: (text, record, index) => { - return ( -
- {renderStatus(text)} -
- ); - }, - }, - { - title: '响应时间', - dataIndex: 'response_time', - render: (text, record, index) => { - return ( -
- {renderResponseTime(text)} -
- ); - }, - }, - { - title: '已用/剩余', - dataIndex: 'expired_time', - render: (text, record, index) => { - return ( -
- - - {renderQuota(record.used_quota)} - - - {updateChannelBalance(record)}}>${renderNumberWithPoint(record.balance)} - - -
- ); - }, - }, - { - title: '优先级', - dataIndex: 'priority', - render: (text, record, index) => { - return ( -
- { - manageChannel(record.id, 'priority', record, value); - }} - defaultValue={record.priority} - min={-999} - /> -
- ); - }, - }, - { - title: '权重', - dataIndex: 'weight', - render: (text, record, index) => { - return ( -
- { - manageChannel(record.id, 'weight', record, value); - }} - defaultValue={record.weight} - min={0} - /> -
- ); - }, - }, - { - title: '', - dataIndex: 'operate', - render: (text, record, index) => ( -
- - - - - - - {/**/} - { - manageChannel(record.id, 'delete', record).then( - () => { - removeRecord(record.id); - } - ) - }} - > - - - { - record.status === 1 ? - : - - } - -
- ), - }, - ]; - - const [channels, setChannels] = useState([]); - const [loading, setLoading] = useState(true); - const [activePage, setActivePage] = useState(1); - const [idSort, setIdSort] = useState(false); - const [searchKeyword, setSearchKeyword] = useState(''); - const [searchGroup, setSearchGroup] = useState(''); - const [searchModel, setSearchModel] = useState(''); - 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 [channelCount, setChannelCount] = useState(pageSize); - const [groupOptions, setGroupOptions] = useState([]); - const [showEdit, setShowEdit] = useState(false); - const [enableBatchDelete, setEnableBatchDelete] = useState(false); - const [editingChannel, setEditingChannel] = useState({ - id: undefined, - }); - const [selectedChannels, setSelectedChannels] = useState([]); - - const removeRecord = id => { - let newDataSource = [...channels]; - if (id != null) { - let idx = newDataSource.findIndex(data => data.id === id); - - if (idx > -1) { - newDataSource.splice(idx, 1); - setChannels(newDataSource); - } - } - }; - - const setChannelFormat = (channels) => { - for (let i = 0; i < channels.length; i++) { - channels[i].key = '' + channels[i].id; - let test_models = [] - channels[i].models.split(',').forEach((item, index) => { - test_models.push({ - node: 'item', - name: item, - onClick: () => { - testChannel(channels[i], item) - } + const columns = [ + // { + // title: '', + // dataIndex: 'checkbox', + // className: 'checkbox', + // }, + { + title: 'ID', + dataIndex: 'id' + }, + { + title: '名称', + dataIndex: 'name' + }, + { + title: '分组', + dataIndex: 'group', + render: (text, record, index) => { + return ( +
+ + { + text.split(',').map((item, index) => { + return (renderGroup(item)); }) - }) - channels[i].test_models = test_models - } - // data.key = '' + data.id - setChannels(channels); - if (channels.length >= pageSize) { - setChannelCount(channels.length + pageSize); - } else { - setChannelCount(channels.length); - } + } + +
+ ); + } + }, + { + title: '类型', + dataIndex: 'type', + render: (text, record, index) => { + return ( +
+ {renderType(text)} +
+ ); + } + }, + { + title: '状态', + dataIndex: 'status', + render: (text, record, index) => { + return ( +
+ {renderStatus(text)} +
+ ); + } + }, + { + title: '响应时间', + dataIndex: 'response_time', + render: (text, record, index) => { + return ( +
+ {renderResponseTime(text)} +
+ ); + } + }, + { + title: '已用/剩余', + dataIndex: 'expired_time', + render: (text, record, index) => { + return ( +
+ + + {renderQuota(record.used_quota)} + + + { + updateChannelBalance(record); + }}>${renderNumberWithPoint(record.balance)} + + +
+ ); + } + }, + { + title: '优先级', + dataIndex: 'priority', + render: (text, record, index) => { + return ( +
+ { + manageChannel(record.id, 'priority', record, e.target.value); + }} + keepFocus={true} + innerButtons + defaultValue={record.priority} + min={-999} + /> +
+ ); + } + }, + { + title: '权重', + dataIndex: 'weight', + render: (text, record, index) => { + return ( +
+ { + manageChannel(record.id, 'weight', record, e.target.value); + }} + keepFocus={true} + innerButtons + defaultValue={record.weight} + min={0} + /> +
+ ); + } + }, + { + title: '', + dataIndex: 'operate', + render: (text, record, index) => ( +
+ + + + + + + {/**/} + { + manageChannel(record.id, 'delete', record).then( + () => { + removeRecord(record.id); + } + ); + }} + > + + + { + record.status === 1 ? + : + + } + +
+ ) } + ]; - const loadChannels = async (startIdx, pageSize, idSort) => { - setLoading(true); - 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) { - setChannelFormat(data); - } else { - let newChannels = [...channels]; - newChannels.splice(startIdx * pageSize, data.length, ...data); - setChannelFormat(newChannels); - } - } else { - showError(message); - } - setLoading(false); - }; + const [channels, setChannels] = useState([]); + const [loading, setLoading] = useState(true); + const [activePage, setActivePage] = useState(1); + const [idSort, setIdSort] = useState(false); + const [searchKeyword, setSearchKeyword] = useState(''); + const [searchGroup, setSearchGroup] = useState(''); + const [searchModel, setSearchModel] = useState(''); + 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 [channelCount, setChannelCount] = useState(pageSize); + const [groupOptions, setGroupOptions] = useState([]); + const [showEdit, setShowEdit] = useState(false); + const [enableBatchDelete, setEnableBatchDelete] = useState(false); + const [editingChannel, setEditingChannel] = useState({ + id: undefined + }); + const [selectedChannels, setSelectedChannels] = useState([]); - const refresh = async () => { - await loadChannels(activePage - 1, pageSize, idSort); - }; + const removeRecord = id => { + let newDataSource = [...channels]; + if (id != null) { + let idx = newDataSource.findIndex(data => data.id === id); - useEffect(() => { - // console.log('default effect') - const localIdSort = localStorage.getItem('id-sort') === 'true'; - setIdSort(localIdSort) - loadChannels(0, pageSize, localIdSort) - .then() - .catch((reason) => { - showError(reason); - }); - fetchGroups().then(); - }, []); + if (idx > -1) { + newDataSource.splice(idx, 1); + setChannels(newDataSource); + } + } + }; - // useEffect(() => { - // console.log('search effect') - // searchChannels() - // }, [searchGroup]); - - // useEffect(() => { - // localStorage.setItem('id-sort', idSort + ''); - // refresh() - // }, [idSort]); - - const manageChannel = async (id, action, record, value) => { - let data = {id}; - let res; - switch (action) { - case 'delete': - res = await API.delete(`/api/channel/${id}/`); - break; - case 'enable': - data.status = 1; - res = await API.put('/api/channel/', data); - break; - case 'disable': - data.status = 2; - res = await API.put('/api/channel/', data); - break; - case 'priority': - if (value === '') { - return; - } - data.priority = parseInt(value); - res = await API.put('/api/channel/', data); - break; - case 'weight': - if (value === '') { - return; - } - data.weight = parseInt(value); - if (data.weight < 0) { - data.weight = 0; - } - res = await API.put('/api/channel/', data); - break; - } - const {success, message} = res.data; - if (success) { - showSuccess('操作成功完成!'); - let channel = res.data.data; - let newChannels = [...channels]; - if (action === 'delete') { - - } else { - record.status = channel.status; - } - setChannels(newChannels); - } else { - showError(message); - } - }; - - const renderStatus = (status) => { - switch (status) { - case 1: - return 已启用; - case 2: - return ( - - 已禁用 - - ); - case 3: - return ( - - 自动禁用 - - ); - default: - return ( - - 未知状态 - - ); - } - }; - - const renderResponseTime = (responseTime) => { - let time = responseTime / 1000; - time = time.toFixed(2) + ' 秒'; - if (responseTime === 0) { - return 未测试; - } else if (responseTime <= 1000) { - return {time}; - } else if (responseTime <= 3000) { - return {time}; - } else if (responseTime <= 5000) { - return {time}; - } else { - return {time}; - } - }; - - const searchChannels = async (searchKeyword, searchGroup, searchModel) => { - if (searchKeyword === '' && searchGroup === '' && searchModel === '') { - // if keyword is blank, load files instead. - await loadChannels(0, pageSize, idSort); - setActivePage(1); - return; - } - setSearching(true); - const res = await API.get(`/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}&model=${searchModel}`); - const {success, message, data} = res.data; - if (success) { - setChannels(data); - setActivePage(1); - } else { - showError(message); - } - setSearching(false); - }; - - const testChannel = async (record, model) => { - const res = await API.get(`/api/channel/test/${record.id}?model=${model}`); - const {success, message, time} = res.data; - if (success) { - record.response_time = time * 1000; - record.test_time = Date.now() / 1000; - showInfo(`通道 ${record.name} 测试成功,耗时 ${time.toFixed(2)} 秒。`); - } else { - showError(message); - } - }; - - const testAllChannels = async () => { - const res = await API.get(`/api/channel/test`); - const {success, message} = res.data; - if (success) { - showInfo('已成功开始测试所有通道,请刷新页面查看结果。'); - } else { - showError(message); - } - }; - - const deleteAllDisabledChannels = async () => { - const res = await API.delete(`/api/channel/disabled`); - const {success, message, data} = res.data; - if (success) { - showSuccess(`已删除所有禁用渠道,共计 ${data} 个`); - await refresh(); - } else { - showError(message); - } - }; - - const updateChannelBalance = async (record) => { - const res = await API.get(`/api/channel/update_balance/${record.id}/`); - const {success, message, balance} = res.data; - if (success) { - record.balance = balance; - record.balance_updated_time = Date.now() / 1000; - showInfo(`通道 ${record.name} 余额更新成功!`); - } else { - showError(message); - } - }; - - const updateAllChannelsBalance = async () => { - setUpdatingBalance(true); - const res = await API.get(`/api/channel/update_balance`); - const {success, message} = res.data; - if (success) { - showInfo('已更新完毕所有已启用通道余额!'); - } else { - showError(message); - } - setUpdatingBalance(false); - }; - - const batchDeleteChannels = async () => { - if (selectedChannels.length === 0) { - showError('请先选择要删除的通道!'); - return; - } - setLoading(true); - let ids = []; - selectedChannels.forEach((channel) => { - ids.push(channel.id); + const setChannelFormat = (channels) => { + for (let i = 0; i < channels.length; i++) { + channels[i].key = '' + channels[i].id; + let test_models = []; + channels[i].models.split(',').forEach((item, index) => { + test_models.push({ + node: 'item', + name: item, + onClick: () => { + testChannel(channels[i], item); + } }); - const res = await API.post(`/api/channel/batch`, {ids: ids}); - const {success, message, data} = res.data; - if (success) { - showSuccess(`已删除 ${data} 个通道!`); - await refresh(); - } else { - showError(message); - } - setLoading(false); + }); + channels[i].test_models = test_models; } - - const fixChannelsAbilities = async () => { - const res = await API.post(`/api/channel/fix`); - const {success, message, data} = res.data; - if (success) { - showSuccess(`已修复 ${data} 个通道!`); - await refresh(); - } else { - showError(message); - } + // data.key = '' + data.id + setChannels(channels); + if (channels.length >= pageSize) { + setChannelCount(channels.length + pageSize); + } else { + setChannelCount(channels.length); } + }; - const sortChannel = (key) => { - if (channels.length === 0) return; - setLoading(true); - let sortedChannels = [...channels]; - if (typeof sortedChannels[0][key] === 'string') { - sortedChannels.sort((a, b) => { - return ('' + a[key]).localeCompare(b[key]); - }); - } else { - sortedChannels.sort((a, b) => { - if (a[key] === b[key]) return 0; - if (a[key] > b[key]) return -1; - if (a[key] < b[key]) return 1; - }); - } - if (sortedChannels[0].id === channels[0].id) { - sortedChannels.reverse(); - } - setChannels(sortedChannels); - setLoading(false); - }; - - let pageData = channels.slice((activePage - 1) * pageSize, activePage * pageSize); - - 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 => { - }); - } - }; - - const handlePageSizeChange = async(size) => { - setPageSize(size) - setActivePage(1) - loadChannels(0, size, idSort) - .then() - .catch((reason) => { - showError(reason); - }) - }; - - const fetchGroups = async () => { - try { - 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, - }))); - } catch (error) { - showError(error.message); - } - }; - - const closeEdit = () => { - setShowEdit(false); + const loadChannels = async (startIdx, pageSize, idSort) => { + setLoading(true); + 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) { + setChannelFormat(data); + } else { + let newChannels = [...channels]; + newChannels.splice(startIdx * pageSize, data.length, ...data); + setChannelFormat(newChannels); + } + } else { + showError(message); } + setLoading(false); + }; - const handleRow = (record, index) => { - if (record.status !== 1) { - return { - style: { - background: 'var(--semi-color-disabled-border)', - }, - }; - } else { - return {}; + const refresh = async () => { + await loadChannels(activePage - 1, pageSize, idSort); + }; + + useEffect(() => { + // console.log('default effect') + const localIdSort = localStorage.getItem('id-sort') === 'true'; + const localPageSize = parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE; + setIdSort(localIdSort); + setPageSize(localPageSize); + loadChannels(0, localPageSize, localIdSort) + .then() + .catch((reason) => { + showError(reason); + }); + fetchGroups().then(); + }, []); + + const manageChannel = async (id, action, record, value) => { + let data = { id }; + let res; + switch (action) { + case 'delete': + res = await API.delete(`/api/channel/${id}/`); + break; + case 'enable': + data.status = 1; + res = await API.put('/api/channel/', data); + break; + case 'disable': + data.status = 2; + res = await API.put('/api/channel/', data); + break; + case 'priority': + if (value === '') { + return; } - }; + data.priority = parseInt(value); + res = await API.put('/api/channel/', data); + break; + case 'weight': + if (value === '') { + return; + } + data.weight = parseInt(value); + if (data.weight < 0) { + data.weight = 0; + } + res = await API.put('/api/channel/', data); + break; + } + const { success, message } = res.data; + if (success) { + showSuccess('操作成功完成!'); + let channel = res.data.data; + let newChannels = [...channels]; + if (action === 'delete') { + + } else { + record.status = channel.status; + } + setChannels(newChannels); + } else { + showError(message); + } + }; + + const renderStatus = (status) => { + switch (status) { + case 1: + return 已启用; + case 2: + return ( + + 已禁用 + + ); + case 3: + return ( + + 自动禁用 + + ); + default: + return ( + + 未知状态 + + ); + } + }; + + const renderResponseTime = (responseTime) => { + let time = responseTime / 1000; + time = time.toFixed(2) + ' 秒'; + if (responseTime === 0) { + return 未测试; + } else if (responseTime <= 1000) { + return {time}; + } else if (responseTime <= 3000) { + return {time}; + } else if (responseTime <= 5000) { + return {time}; + } else { + return {time}; + } + }; + + const searchChannels = async (searchKeyword, searchGroup, searchModel) => { + if (searchKeyword === '' && searchGroup === '' && searchModel === '') { + // if keyword is blank, load files instead. + await loadChannels(0, pageSize, idSort); + setActivePage(1); + return; + } + setSearching(true); + const res = await API.get(`/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}&model=${searchModel}`); + const { success, message, data } = res.data; + if (success) { + setChannels(data); + setActivePage(1); + } else { + showError(message); + } + setSearching(false); + }; + + const testChannel = async (record, model) => { + const res = await API.get(`/api/channel/test/${record.id}?model=${model}`); + const { success, message, time } = res.data; + if (success) { + record.response_time = time * 1000; + record.test_time = Date.now() / 1000; + showInfo(`通道 ${record.name} 测试成功,耗时 ${time.toFixed(2)} 秒。`); + } else { + showError(message); + } + }; + + const testAllChannels = async () => { + const res = await API.get(`/api/channel/test`); + const { success, message } = res.data; + if (success) { + showInfo('已成功开始测试所有通道,请刷新页面查看结果。'); + } else { + showError(message); + } + }; + + const deleteAllDisabledChannels = async () => { + const res = await API.delete(`/api/channel/disabled`); + const { success, message, data } = res.data; + if (success) { + showSuccess(`已删除所有禁用渠道,共计 ${data} 个`); + await refresh(); + } else { + showError(message); + } + }; + + const updateChannelBalance = async (record) => { + const res = await API.get(`/api/channel/update_balance/${record.id}/`); + const { success, message, balance } = res.data; + if (success) { + record.balance = balance; + record.balance_updated_time = Date.now() / 1000; + showInfo(`通道 ${record.name} 余额更新成功!`); + } else { + showError(message); + } + }; + + const updateAllChannelsBalance = async () => { + setUpdatingBalance(true); + const res = await API.get(`/api/channel/update_balance`); + const { success, message } = res.data; + if (success) { + showInfo('已更新完毕所有已启用通道余额!'); + } else { + showError(message); + } + setUpdatingBalance(false); + }; + + const batchDeleteChannels = async () => { + if (selectedChannels.length === 0) { + showError('请先选择要删除的通道!'); + return; + } + setLoading(true); + let ids = []; + selectedChannels.forEach((channel) => { + ids.push(channel.id); + }); + const res = await API.post(`/api/channel/batch`, { ids: ids }); + const { success, message, data } = res.data; + if (success) { + showSuccess(`已删除 ${data} 个通道!`); + await refresh(); + } else { + showError(message); + } + setLoading(false); + }; + + const fixChannelsAbilities = async () => { + const res = await API.post(`/api/channel/fix`); + const { success, message, data } = res.data; + if (success) { + showSuccess(`已修复 ${data} 个通道!`); + await refresh(); + } else { + showError(message); + } + }; + + let pageData = channels.slice((activePage - 1) * pageSize, activePage * pageSize); + + 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 => { + }); + } + }; + + const handlePageSizeChange = async (size) => { + localStorage.setItem('page-size', size + ''); + setPageSize(size); + setActivePage(1); + loadChannels(0, size, idSort) + .then() + .catch((reason) => { + showError(reason); + }); + }; + + const fetchGroups = async () => { + try { + 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 + }))); + } catch (error) { + showError(error.message); + } + }; + + const closeEdit = () => { + setShowEdit(false); + }; + + const handleRow = (record, index) => { + if (record.status !== 1) { + return { + style: { + background: 'var(--semi-color-disabled-border)' + } + }; + } else { + return {}; + } + }; - return ( - <> - -
{searchChannels(searchKeyword, searchGroup, searchModel)}} labelPosition='left'> -
- - { - setSearchKeyword(v.trim()) - }} - /> - { - setSearchModel(v.trim()) - }} - /> - { - setSearchGroup(v) - searchChannels(searchKeyword, v, searchModel) - }}/> - - -
-
-
- - - 使用ID排序 - { - localStorage.setItem('id-sort', v + '') - setIdSort(v) - loadChannels(0, pageSize, v) - .then() - .catch((reason) => { - showError(reason); - }) - }}> - - -
+ return ( + <> + +
{ + searchChannels(searchKeyword, searchGroup, searchModel); + }} labelPosition="left"> +
+ + { + setSearchKeyword(v.trim()); + }} + /> + { + setSearchModel(v.trim()); + }} + /> + { + setSearchGroup(v); + searchChannels(searchKeyword, v, searchModel); + }} /> + + +
+
+
+ + + 使用ID排序 + { + localStorage.setItem('id-sort', v + ''); + setIdSort(v); + loadChannels(0, pageSize, v) + .then() + .catch((reason) => { + showError(reason); + }); + }}> + + +
- '', - 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 - }/> -
- - - - - - - - - - - +
'', + 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 + } /> +
+ + + + + + + + + + + - - - {/*
*/} + + + {/*
*/} - {/*
*/} -
-
- - 开启批量删除 - { - setEnableBatchDelete(v) - }}> - - - - - - - -
- - ); + {/*
*/} + +
+ + 开启批量删除 + { + setEnableBatchDelete(v); + }}> + + + + + + + +
+ + ); }; export default ChannelsTable; diff --git a/web/src/components/Footer.js b/web/src/components/Footer.js index 504f37b..9e29426 100644 --- a/web/src/components/Footer.js +++ b/web/src/components/Footer.js @@ -1,7 +1,7 @@ import React, { useEffect, useState } from 'react'; import { getFooterHTML, getSystemName } from '../helpers'; -import {Layout} from "@douyinfe/semi-ui"; +import { Layout } from '@douyinfe/semi-ui'; const Footer = () => { const systemName = getSystemName(); @@ -29,30 +29,30 @@ const Footer = () => { return ( - + {footer ? (
) : ( -
+
New API {process.env.REACT_APP_VERSION}{' '} 由{' '} - + Calcium-Ion {' '} 开发,基于{' '} - + One API v0.5.4 {' '} ,本项目根据{' '} - + MIT 许可证 {' '} 授权 diff --git a/web/src/components/GitHubOAuth.js b/web/src/components/GitHubOAuth.js index 965d93c..50c6306 100644 --- a/web/src/components/GitHubOAuth.js +++ b/web/src/components/GitHubOAuth.js @@ -58,7 +58,7 @@ const GitHubOAuth = () => { return ( - {prompt} + {prompt} ); diff --git a/web/src/components/HeaderBar.js b/web/src/components/HeaderBar.js index 244cbd8..eaf36c4 100644 --- a/web/src/components/HeaderBar.js +++ b/web/src/components/HeaderBar.js @@ -1,165 +1,161 @@ -import React, {useContext, useEffect, useRef, useState} from 'react'; -import {Link, useNavigate} from 'react-router-dom'; -import {UserContext} from '../context/User'; +import React, { useContext, useEffect, useState } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; +import { UserContext } from '../context/User'; -import {API, getLogo, getSystemName, isAdmin, isMobile, showSuccess} from '../helpers'; +import { API, getLogo, getSystemName, showSuccess } from '../helpers'; import '../index.css'; import fireworks from 'react-fireworks'; -import { - IconKey, - IconUser, - IconHelpCircle -} from '@douyinfe/semi-icons'; -import {Nav, Avatar, Dropdown, Layout, Switch} from '@douyinfe/semi-ui'; -import {stringToColor} from "../helpers/render"; +import { IconHelpCircle, IconKey, IconUser } from '@douyinfe/semi-icons'; +import { Avatar, Dropdown, Layout, Nav, Switch } from '@douyinfe/semi-ui'; +import { stringToColor } from '../helpers/render'; // HeaderBar Buttons let headerButtons = [ - { - text: '关于', - itemKey: 'about', - to: '/about', - icon: - }, + { + text: '关于', + itemKey: 'about', + to: '/about', + icon: + } ]; if (localStorage.getItem('chat_link')) { - headerButtons.splice(1, 0, { - name: '聊天', - to: '/chat', - icon: 'comments' - }); + headerButtons.splice(1, 0, { + name: '聊天', + to: '/chat', + icon: 'comments' + }); } const HeaderBar = () => { - const [userState, userDispatch] = useContext(UserContext); - let navigate = useNavigate(); + const [userState, userDispatch] = useContext(UserContext); + let navigate = useNavigate(); - const [showSidebar, setShowSidebar] = useState(false); - const [dark, setDark] = useState(false); - const systemName = getSystemName(); - const logo = getLogo(); - 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 [showSidebar, setShowSidebar] = useState(false); + const [dark, setDark] = useState(false); + const systemName = getSystemName(); + const logo = getLogo(); + 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); - async function logout() { - setShowSidebar(false); - await API.get('/api/user/logout'); - showSuccess('注销成功!'); - userDispatch({type: 'logout'}); - localStorage.removeItem('user'); - navigate('/login'); + async function logout() { + setShowSidebar(false); + await API.get('/api/user/logout'); + showSuccess('注销成功!'); + userDispatch({ type: 'logout' }); + localStorage.removeItem('user'); + navigate('/login'); + } + + const handleNewYearClick = () => { + fireworks.init('root', {}); + fireworks.start(); + setTimeout(() => { + fireworks.stop(); + setTimeout(() => { + window.location.reload(); + }, 10000); + }, 3000); + }; + + useEffect(() => { + if (themeMode === 'dark') { + switchMode(true); } + if (isNewYear) { + console.log('Happy New Year!'); + } + }, []); - const handleNewYearClick = () => { - fireworks.init("root",{}); - fireworks.start(); - setTimeout(() => { - fireworks.stop(); - setTimeout(() => { - window.location.reload(); - }, 10000); - }, 3000); - }; + const switchMode = (model) => { + const body = document.body; + if (!model) { + body.removeAttribute('theme-mode'); + localStorage.setItem('theme-mode', 'light'); + } else { + body.setAttribute('theme-mode', 'dark'); + localStorage.setItem('theme-mode', 'dark'); + } + setDark(model); + }; + return ( + <> + +
+ +
+
+ + ); }; export default HeaderBar; diff --git a/web/src/components/Loading.js b/web/src/components/Loading.js index 1210a56..bacb53b 100644 --- a/web/src/components/Loading.js +++ b/web/src/components/Loading.js @@ -1,5 +1,5 @@ import React from 'react'; -import { Segment, Dimmer, Loader } from 'semantic-ui-react'; +import { Dimmer, Loader, Segment } from 'semantic-ui-react'; const Loading = ({ prompt: name = 'page' }) => { return ( diff --git a/web/src/components/LoginForm.js b/web/src/components/LoginForm.js index baef358..9657085 100644 --- a/web/src/components/LoginForm.js +++ b/web/src/components/LoginForm.js @@ -1,12 +1,12 @@ import React, { useContext, useEffect, useState } from 'react'; import { Link, useNavigate, useSearchParams } from 'react-router-dom'; import { UserContext } from '../context/User'; -import { API, getLogo, isMobile, showError, showInfo, showSuccess, showWarning } from '../helpers'; +import { API, getLogo, showError, showInfo, showSuccess } from '../helpers'; import { onGitHubOAuthClicked, onLinuxDoOAuthClicked } from './utils'; -import Turnstile from "react-turnstile"; -import { Layout, Card, Image, Form, Button, Divider, Modal, Icon } 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 Turnstile from 'react-turnstile'; +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'; import { IconGithubLogo } from '@douyinfe/semi-icons'; @@ -14,252 +14,252 @@ import LinuxDoIcon from './LinuxDoIcon'; import WeChatIcon from './WeChatIcon'; const LoginForm = () => { - const [inputs, setInputs] = useState({ - username: '', - password: '', - wechat_verification_code: '' - }); - const [searchParams, setSearchParams] = useSearchParams(); - const [submitted, setSubmitted] = useState(false); - const { username, password } = inputs; - const [userState, userDispatch] = useContext(UserContext); - const [turnstileEnabled, setTurnstileEnabled] = useState(false); - const [turnstileSiteKey, setTurnstileSiteKey] = useState(''); - const [turnstileToken, setTurnstileToken] = useState(''); - let navigate = useNavigate(); - const [status, setStatus] = useState({}); - const logo = getLogo(); + const [inputs, setInputs] = useState({ + username: '', + password: '', + wechat_verification_code: '' + }); + const [searchParams, setSearchParams] = useSearchParams(); + const [submitted, setSubmitted] = useState(false); + const { username, password } = inputs; + const [userState, userDispatch] = useContext(UserContext); + const [turnstileEnabled, setTurnstileEnabled] = useState(false); + const [turnstileSiteKey, setTurnstileSiteKey] = useState(''); + const [turnstileToken, setTurnstileToken] = useState(''); + let navigate = useNavigate(); + const [status, setStatus] = useState({}); + const logo = getLogo(); - useEffect(() => { - if (searchParams.get('expired')) { - showError('未登录或登录已过期,请重新登录!'); - } - let status = localStorage.getItem('status'); - if (status) { - status = JSON.parse(status); - setStatus(status); - if (status.turnstile_check) { - setTurnstileEnabled(true); - setTurnstileSiteKey(status.turnstile_site_key); - } - } - }, []); - - const [showWeChatLoginModal, setShowWeChatLoginModal] = useState(false); - - const onWeChatLoginClicked = () => { - setShowWeChatLoginModal(true); - }; - - const onSubmitWeChatVerificationCode = async () => { - if (turnstileEnabled && turnstileToken === '') { - showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!'); - return; - } - const res = await API.get( - `/api/oauth/wechat?code=${inputs.wechat_verification_code}` - ); - const { success, message, data } = res.data; - if (success) { - userDispatch({ type: 'login', payload: data }); - localStorage.setItem('user', JSON.stringify(data)); - navigate('/'); - showSuccess('登录成功!'); - setShowWeChatLoginModal(false); - } else { - showError(message); - } - }; - - function handleChange(name, value) { - setInputs((inputs) => ({ ...inputs, [name]: value })); + useEffect(() => { + if (searchParams.get('expired')) { + showError('未登录或登录已过期,请重新登录!'); } - - async function handleSubmit(e) { - if (turnstileEnabled && turnstileToken === '') { - showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!'); - return; - } - setSubmitted(true); - if (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 }); - } - navigate('/token'); - } else { - showError(message); - } - } else { - showError('请输入用户名和密码!'); - } + let status = localStorage.getItem('status'); + if (status) { + status = JSON.parse(status); + setStatus(status); + if (status.turnstile_check) { + setTurnstileEnabled(true); + setTurnstileSiteKey(status.turnstile_site_key); + } } + }, []); - // 添加Telegram登录处理函数 - const onTelegramLoginClicked = async (response) => { - const fields = ["id", "first_name", "last_name", "username", "photo_url", "auth_date", "hash", "lang"]; - const params = {}; - fields.forEach((field) => { - if (response[field]) { - params[field] = response[field]; - } - }); - const res = await API.get(`/api/oauth/telegram/login`, { params }); - const { success, message, data } = res.data; - if (success) { - userDispatch({ type: 'login', payload: data }); - localStorage.setItem('user', JSON.stringify(data)); - showSuccess('登录成功!'); - navigate('/'); - } else { - showError(message); - } - }; + const [showWeChatLoginModal, setShowWeChatLoginModal] = useState(false); - return ( -
- - - - -
-
- - - 用户登录 - -
- handleChange('username', value)} - /> - handleChange('password', value)} - /> + const onWeChatLoginClicked = () => { + setShowWeChatLoginModal(true); + }; - - -
- - 没有账号请先 注册账号 - - - 忘记密码 点击重置 - -
- {status.github_oauth || status.linuxdo_oauth || status.wechat_login || status.telegram_oauth ? ( - <> - - 第三方登录 - -
- {status.github_oauth ? ( -
- - ) : ( - <> - )} - setShowWeChatLoginModal(false)} - okText={'登录'} - size={'small'} - centered={true} - > -
- -
-
-

- 微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效) -

-
-
- handleChange('wechat_verification_code', value)} - /> - -
-
- {turnstileEnabled ? ( -
- { - setTurnstileToken(token); - }} - /> -
- ) : ( - <> - )} -
-
- -
-
-
+ const onSubmitWeChatVerificationCode = async () => { + if (turnstileEnabled && turnstileToken === '') { + showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!'); + return; + } + const res = await API.get( + `/api/oauth/wechat?code=${inputs.wechat_verification_code}` ); + const { success, message, data } = res.data; + if (success) { + userDispatch({ type: 'login', payload: data }); + localStorage.setItem('user', JSON.stringify(data)); + navigate('/'); + showSuccess('登录成功!'); + setShowWeChatLoginModal(false); + } else { + showError(message); + } + }; + + function handleChange(name, value) { + setInputs((inputs) => ({ ...inputs, [name]: value })); + } + + async function handleSubmit(e) { + if (turnstileEnabled && turnstileToken === '') { + showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!'); + return; + } + setSubmitted(true); + if (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 }); + } + navigate('/token'); + } else { + showError(message); + } + } else { + showError('请输入用户名和密码!'); + } + } + + // 添加Telegram登录处理函数 + const onTelegramLoginClicked = async (response) => { + const fields = ['id', 'first_name', 'last_name', 'username', 'photo_url', 'auth_date', 'hash', 'lang']; + const params = {}; + fields.forEach((field) => { + if (response[field]) { + params[field] = response[field]; + } + }); + const res = await API.get(`/api/oauth/telegram/login`, { params }); + const { success, message, data } = res.data; + if (success) { + userDispatch({ type: 'login', payload: data }); + localStorage.setItem('user', JSON.stringify(data)); + showSuccess('登录成功!'); + navigate('/'); + } else { + showError(message); + } + }; + + return ( +
+ + + + +
+
+ + + 用户登录 + +
+ handleChange('username', value)} + /> + handleChange('password', value)} + /> + + + +
+ + 没有账号请先 注册账号 + + + 忘记密码 点击重置 + +
+ {status.github_oauth || status.linuxdo_oauth || status.wechat_login || status.telegram_oauth ? ( + <> + + 第三方登录 + +
+ {status.github_oauth ? ( +
+ + ) : ( + <> + )} + setShowWeChatLoginModal(false)} + okText={'登录'} + size={'small'} + centered={true} + > +
+ +
+
+

+ 微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效) +

+
+
+ handleChange('wechat_verification_code', value)} + /> + +
+
+ {turnstileEnabled ? ( +
+ { + setTurnstileToken(token); + }} + /> +
+ ) : ( + <> + )} +
+
+ +
+
+
+ ); }; export default LoginForm; diff --git a/web/src/components/LogsTable.js b/web/src/components/LogsTable.js index 9fb7e3c..b07682b 100644 --- a/web/src/components/LogsTable.js +++ b/web/src/components/LogsTable.js @@ -1,501 +1,399 @@ -import React, {useEffect, useState} from 'react'; -import {Label} from 'semantic-ui-react'; -import {API, copy, isAdmin, showError, showSuccess, timestamp2string} from '../helpers'; +import React, { useEffect, useState } from 'react'; +import { API, copy, isAdmin, showError, showSuccess, timestamp2string } from '../helpers'; -import {Table, Avatar, Tag, Form, Button, Layout, Select, Popover, Modal, Spin, Space} from '@douyinfe/semi-ui'; -import {ITEMS_PER_PAGE} from '../constants'; -import {renderNumber, renderQuota, stringToColor} from '../helpers/render'; -import { - IconAt, - IconHistogram, - IconGift, - IconKey, - IconUser, - IconLayers, - IconSetting, - IconCreditCard, - IconSemiLogo, - IconHome, - IconMore -} from '@douyinfe/semi-icons'; -import Paragraph from "@douyinfe/semi-ui/lib/es/typography/paragraph"; +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'; + +const { Header } = Layout; -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 充值 ; - case 2: - return 消费 ; - case 3: - return 管理 ; - case 4: - return 系统 ; - default: - return 未知 ; - } + switch (type) { + case 1: + return 充值 ; + case 2: + return 消费 ; + case 3: + return 管理 ; + case 4: + return 系统 ; + default: + return 未知 ; + } } function renderIsStream(bool) { - if (bool) { - return ; - } else { - return 非流; - } + if (bool) { + return ; + } else { + return 非流; + } } function renderUseTime(type) { - const time = parseInt(type); - if (time < 101) { - return {time} s ; - } else if (time < 300) { - return {time} s ; - } else { - return {time} s ; - } + const time = parseInt(type); + if (time < 101) { + return {time} s ; + } else if (time < 300) { + return {time} s ; + } else { + return {time} s ; + } } 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 ? -
- { {text} } -
- : - <> - : - <> - ); - }, - }, - { - title: '用户', - dataIndex: 'username', - className: isAdmin() ? 'tableShow' : 'tableHiddle', - render: (text, record, index) => { - return ( - isAdminUser ? -
- showUserInfo(record.user_id)}> - {typeof text === 'string' && text.slice(0, 1)} - - {text} -
- : - <> - ); - }, - }, - { - title: '令牌', - dataIndex: 'token_name', - render: (text, record, index) => { - return ( - record.type === 0 || record.type === 2 ? -
- { - copyText(text) - }}> {text} -
- : - <> - ); - }, - }, - { - title: '类型', - dataIndex: 'type', - render: (text, record, index) => { - return ( -
- {renderType(text)} -
- ); - }, - }, - { - title: '模型', - dataIndex: 'model_name', - render: (text, record, index) => { - return ( - record.type === 0 || record.type === 2 ? -
- { - copyText(text) - }}> {text} -
- : - <> - ); - }, - }, - { - title: '用时', - dataIndex: 'use_time', - render: (text, record, index) => { - return ( -
- - {renderUseTime(text)} - {renderIsStream(record.is_stream)} - -
- ); - }, - }, - { - title: '提示', - dataIndex: 'prompt_tokens', - render: (text, record, index) => { - return ( - record.type === 0 || record.type === 2 ? -
- { {text} } -
- : - <> - ); - }, - }, - { - title: '补全', - dataIndex: 'completion_tokens', - render: (text, record, index) => { - return ( - parseInt(text) > 0 && (record.type === 0 || record.type === 2) ? -
- { {text} } -
- : - <> - ); - }, - }, - { - title: '花费', - dataIndex: 'quota', - render: (text, record, index) => { - return ( - record.type === 0 || record.type === 2 ? -
- { - renderQuota(text, 6) - } -
- : - <> - ); - } - }, - { - title: '详情', - dataIndex: 'content', - render: (text, record, index) => { - return - {text} - - } - } - ]; - - const [logs, setLogs] = useState([]); - const [showStat, setShowStat] = useState(false); - const [loading, setLoading] = useState(false); - const [loadingStat, setLoadingStat] = useState(false); - const [activePage, setActivePage] = useState(1); - const [logCount, setLogCount] = useState(ITEMS_PER_PAGE); - const [searchKeyword, setSearchKeyword] = useState(''); - const [searching, setSearching] = useState(false); - const [logType, setLogType] = useState(0); - const isAdminUser = isAdmin(); - let now = new Date(); - // 初始化start_timestamp为前一天 - const [inputs, setInputs] = useState({ - username: '', - token_name: '', - model_name: '', - start_timestamp: timestamp2string(now.getTime() / 1000 - 86400), - end_timestamp: timestamp2string(now.getTime() / 1000 + 3600), - channel: '' - }); - const {username, token_name, model_name, start_timestamp, end_timestamp, channel} = inputs; - - const [stat, setStat] = useState({ - quota: 0, - token: 0 - }); - - const handleInputChange = (value, name) => { - setInputs((inputs) => ({...inputs, [name]: value})); - }; - - 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}`); - const {success, message, data} = res.data; - if (success) { - setStat(data); - } else { - showError(message); - } - }; - - 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}`); - const {success, message, data} = res.data; - if (success) { - setStat(data); - } else { - showError(message); - } - }; - - const handleEyeClick = async () => { - setLoadingStat(true); - if (isAdminUser) { - await getLogStat(); - } else { - await getLogSelfStat(); - } - setShowStat(true); - setLoadingStat(false); - }; - - const showUserInfo = async (userId) => { - if (!isAdminUser) { - return; - } - const res = await API.get(`/api/user/${userId}`); - const {success, message, data} = res.data; - if (success) { - Modal.info({ - title: '用户信息', - content:
-

用户名: {data.username}

-

余额: {renderQuota(data.quota)}

-

已用额度:{renderQuota(data.used_quota)}

-

请求次数:{renderNumber(data.request_count)}

-
, - centered: true, - }) - } else { - showError(message); - } - }; - - const setLogsFormat = (logs) => { - for (let i = 0; i < logs.length; i++) { - logs[i].timestamp2string = timestamp2string(logs[i].created_at); - logs[i].key = '' + logs[i].id; - } - // data.key = '' + data.id - setLogs(logs); - setLogCount(logs.length + ITEMS_PER_PAGE); - // console.log(logCount); + const columns = [{ + title: '时间', dataIndex: 'timestamp2string' + }, { + title: '渠道', + dataIndex: 'channel', + className: isAdmin() ? 'tableShow' : 'tableHiddle', + render: (text, record, index) => { + return (isAdminUser ? record.type === 0 || record.type === 2 ?
+ { {text} } +
: <> : <>); } - - const loadLogs = async (startIdx) => { - setLoading(true); - - let url = ''; - let localStartTimestamp = Date.parse(start_timestamp) / 1000; - let localEndTimestamp = Date.parse(end_timestamp) / 1000; - if (isAdminUser) { - url = `/api/log/?p=${startIdx}&type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}`; - } else { - url = `/api/log/self/?p=${startIdx}&type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`; - } - const res = await API.get(url); - const {success, message, data} = res.data; - if (success) { - if (startIdx === 0) { - setLogsFormat(data); - } else { - let newLogs = [...logs]; - newLogs.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data); - setLogsFormat(newLogs); - } - } else { - showError(message); - } - setLoading(false); - }; - - const pageData = logs.slice((activePage - 1) * ITEMS_PER_PAGE, activePage * ITEMS_PER_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 => { - }); - } - }; - - const refresh = async () => { - // setLoading(true); - setActivePage(1); - await loadLogs(0); - }; - - const copyText = async (text) => { - if (await copy(text)) { - showSuccess('已复制:' + text); - } else { - // setSearchKeyword(text); - Modal.error({title: '无法复制到剪贴板,请手动复制', content: text}); - } + }, { + title: '用户', + dataIndex: 'username', + className: isAdmin() ? 'tableShow' : 'tableHiddle', + render: (text, record, index) => { + return (isAdminUser ?
+ showUserInfo(record.user_id)}> + {typeof text === 'string' && text.slice(0, 1)} + + {text} +
: <>); } + }, { + title: '令牌', dataIndex: 'token_name', render: (text, record, index) => { + return (record.type === 0 || record.type === 2 ?
+ { + copyText(text); + }}> {text} +
: <>); + } + }, { + title: '类型', dataIndex: 'type', render: (text, record, index) => { + return (
+ {renderType(text)} +
); + } + }, { + title: '模型', dataIndex: 'model_name', render: (text, record, index) => { + return (record.type === 0 || record.type === 2 ?
+ { + copyText(text); + }}> {text} +
: <>); + } + }, { + title: '用时', dataIndex: 'use_time', render: (text, record, index) => { + return (
+ + {renderUseTime(text)} + {renderIsStream(record.is_stream)} + +
); + } + }, { + title: '提示', dataIndex: 'prompt_tokens', render: (text, record, index) => { + return (record.type === 0 || record.type === 2 ?
+ { {text} } +
: <>); + } + }, { + title: '补全', dataIndex: 'completion_tokens', render: (text, record, index) => { + return (parseInt(text) > 0 && (record.type === 0 || record.type === 2) ?
+ { {text} } +
: <>); + } + }, { + title: '花费', dataIndex: 'quota', render: (text, record, index) => { + return (record.type === 0 || record.type === 2 ?
+ {renderQuota(text, 6)} +
: <>); + } + }, { + title: '详情', dataIndex: 'content', render: (text, record, index) => { + return + {text} + ; + } + }]; - useEffect(() => { - refresh().then(); - }, [logType]); + const [logs, setLogs] = useState([]); + const [showStat, setShowStat] = useState(false); + const [loading, setLoading] = useState(false); + const [loadingStat, setLoadingStat] = useState(false); + const [activePage, setActivePage] = useState(1); + const [logCount, setLogCount] = useState(ITEMS_PER_PAGE); + const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE); + const [searchKeyword, setSearchKeyword] = useState(''); + const [searching, setSearching] = useState(false); + const [logType, setLogType] = useState(0); + const isAdminUser = isAdmin(); + let now = new Date(); + // 初始化start_timestamp为前一天 + const [inputs, setInputs] = useState({ + username: '', + token_name: '', + model_name: '', + start_timestamp: timestamp2string(now.getTime() / 1000 - 86400), + end_timestamp: timestamp2string(now.getTime() / 1000 + 3600), + channel: '' + }); + const { username, token_name, model_name, start_timestamp, end_timestamp, channel } = inputs; - const searchLogs = async () => { - if (searchKeyword === '') { - // if keyword is blank, load files instead. - await loadLogs(0); - setActivePage(1); - return; - } - setSearching(true); - const res = await API.get(`/api/log/self/search?keyword=${searchKeyword}`); - const {success, message, data} = res.data; - if (success) { - setLogs(data); - setActivePage(1); - } else { - showError(message); - } - setSearching(false); - }; + const [stat, setStat] = useState({ + quota: 0, token: 0 + }); - const handleKeywordChange = async (e, {value}) => { - setSearchKeyword(value.trim()); - }; + const handleInputChange = (value, name) => { + setInputs((inputs) => ({ ...inputs, [name]: value })); + }; - const sortLog = (key) => { - if (logs.length === 0) return; - setLoading(true); - let sortedLogs = [...logs]; - if (typeof sortedLogs[0][key] === 'string') { - sortedLogs.sort((a, b) => { - return ('' + a[key]).localeCompare(b[key]); - }); - } else { - sortedLogs.sort((a, b) => { - if (a[key] === b[key]) return 0; - if (a[key] > b[key]) return -1; - if (a[key] < b[key]) return 1; - }); - } - if (sortedLogs[0].id === logs[0].id) { - sortedLogs.reverse(); - } - setLogs(sortedLogs); - setLoading(false); - }; + 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}`); + const { success, message, data } = res.data; + if (success) { + setStat(data); + } else { + showError(message); + } + }; - return ( + 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}`); + const { success, message, data } = res.data; + if (success) { + setStat(data); + } else { + showError(message); + } + }; + + const handleEyeClick = async () => { + setLoadingStat(true); + if (isAdminUser) { + await getLogStat(); + } else { + await getLogSelfStat(); + } + setShowStat(true); + setLoadingStat(false); + }; + + const showUserInfo = async (userId) => { + if (!isAdminUser) { + return; + } + const res = await API.get(`/api/user/${userId}`); + const { success, message, data } = res.data; + if (success) { + Modal.info({ + title: '用户信息', content:
+

用户名: {data.username}

+

余额: {renderQuota(data.quota)}

+

已用额度:{renderQuota(data.used_quota)}

+

请求次数:{renderNumber(data.request_count)}

+
, centered: true + }); + } else { + showError(message); + } + }; + + const setLogsFormat = (logs) => { + for (let i = 0; i < logs.length; i++) { + logs[i].timestamp2string = timestamp2string(logs[i].created_at); + logs[i].key = '' + logs[i].id; + } + // data.key = '' + data.id + setLogs(logs); + setLogCount(logs.length + ITEMS_PER_PAGE); + // console.log(logCount); + }; + + const loadLogs = async (startIdx, pageSize, logType = 0) => { + setLoading(true); + + let url = ''; + let localStartTimestamp = Date.parse(start_timestamp) / 1000; + let localEndTimestamp = Date.parse(end_timestamp) / 1000; + if (isAdminUser) { + url = `/api/log/?p=${startIdx}&page_size=${pageSize}&type=${logType}&username=${username}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}&channel=${channel}`; + } else { + url = `/api/log/self/?p=${startIdx}&page_size=${pageSize}&type=${logType}&token_name=${token_name}&model_name=${model_name}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`; + } + const res = await API.get(url); + const { success, message, data } = res.data; + if (success) { + if (startIdx === 0) { + setLogsFormat(data); + } else { + let newLogs = [...logs]; + newLogs.splice(startIdx * pageSize, data.length, ...data); + setLogsFormat(newLogs); + } + } else { + showError(message); + } + setLoading(false); + }; + + const pageData = logs.slice((activePage - 1) * pageSize, activePage * pageSize); + + 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 => { + }); + } + }; + + const handlePageSizeChange = async (size) => { + localStorage.setItem('page-size', size + ''); + setPageSize(size); + setActivePage(1); + loadLogs(0, size) + .then() + .catch((reason) => { + showError(reason); + }); + }; + + const refresh = async (localLogType) => { + // setLoading(true); + setActivePage(1); + await loadLogs(0, pageSize, localLogType); + }; + + const copyText = async (text) => { + if (await copy(text)) { + showSuccess('已复制:' + text); + } else { + // setSearchKeyword(text); + Modal.error({ title: '无法复制到剪贴板,请手动复制', content: text }); + } + }; + + useEffect(() => { + // console.log('default effect') + const localPageSize = parseInt(localStorage.getItem('page-size')) || ITEMS_PER_PAGE; + setPageSize(localPageSize); + loadLogs(0, localPageSize) + .then() + .catch((reason) => { + showError(reason); + }); + }, []); + + const searchLogs = async () => { + if (searchKeyword === '') { + // if keyword is blank, load files instead. + await loadLogs(0, pageSize); + setActivePage(1); + return; + } + setSearching(true); + const res = await API.get(`/api/log/self/search?keyword=${searchKeyword}`); + const { success, message, data } = res.data; + if (success) { + setLogs(data); + setActivePage(1); + } else { + showError(message); + } + setSearching(false); + }; + + return (<> + +
+ +

使用明细(总消耗额度: + {showStat ? renderQuota(stat.quota) : '点击查看'} + ) +

+
+
+
<> - -
- -

使用明细(总消耗额度: - {showStat?renderQuota(stat.quota):"点击查看"} - ) -

-
-
- - <> - handleInputChange(value, 'token_name')}/> - handleInputChange(value, 'model_name')}/> - handleInputChange(value, 'start_timestamp')}/> - handleInputChange(value, 'end_timestamp')}/> - { - isAdminUser && <> - handleInputChange(value, 'channel')}/> - handleInputChange(value, 'username')}/> - - } - - - - - -
- - + handleInputChange(value, 'token_name')} /> + handleInputChange(value, 'model_name')} /> + handleInputChange(value, 'start_timestamp')} /> + handleInputChange(value, 'end_timestamp')} /> + {isAdminUser && <> + handleInputChange(value, 'channel')} /> + handleInputChange(value, 'username')} /> + } + + + - ); + +
{ + handlePageSizeChange(size).then(); + }, + onPageChange: handlePageChange + }} /> + + + ); }; export default LogsTable; diff --git a/web/src/components/MjLogsTable.js b/web/src/components/MjLogsTable.js index 90c55f1..6a6fbd9 100644 --- a/web/src/components/MjLogsTable.js +++ b/web/src/components/MjLogsTable.js @@ -1,454 +1,454 @@ -import React, {useEffect, useState} from 'react'; -import {API, copy, isAdmin, showError, showSuccess, timestamp2string} from '../helpers'; +import React, { useEffect, useState } from 'react'; +import { API, copy, isAdmin, showError, showSuccess, timestamp2string } from '../helpers'; -import { - Table, - Avatar, - Tag, - Form, - Button, - Layout, - Select, - Popover, - Modal, - ImagePreview, - Typography, Progress -} from '@douyinfe/semi-ui'; -import {ITEMS_PER_PAGE} from '../constants'; -import {renderNumber, renderQuota, stringToColor} from '../helpers/render'; +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' -] + 'light-blue', 'lime', 'orange', 'pink', + 'purple', 'red', 'teal', 'violet', 'yellow' +]; function renderType(type) { - switch (type) { - case 'IMAGINE': - return 绘图; - case 'UPSCALE': - return 放大; - case 'VARIATION': - return 变换; - case 'HIGH_VARIATION': - return 强变换; - case 'LOW_VARIATION': - return 弱变换; - case 'PAN': - return 平移; - case 'DESCRIBE': - return 图生文; - case 'BLEND': - return 图混合; - case 'SHORTEN': - return 缩词; - case 'REROLL': - return 重绘; - case 'INPAINT': - return 局部重绘-提交; - case 'ZOOM': - return 变焦; - case 'CUSTOM_ZOOM': - return 自定义变焦-提交; - case 'MODAL': - return 窗口处理; - case 'SWAP_FACE': - return 换脸; - default: - return 未知; - } + switch (type) { + case 'IMAGINE': + return 绘图; + case 'UPSCALE': + return 放大; + case 'VARIATION': + return 变换; + case 'HIGH_VARIATION': + return 强变换; + case 'LOW_VARIATION': + return 弱变换; + case 'PAN': + return 平移; + case 'DESCRIBE': + return 图生文; + case 'BLEND': + return 图混合; + case 'SHORTEN': + return 缩词; + case 'REROLL': + return 重绘; + case 'INPAINT': + return 局部重绘-提交; + case 'ZOOM': + return 变焦; + case 'CUSTOM_ZOOM': + return 自定义变焦-提交; + case 'MODAL': + return 窗口处理; + case 'SWAP_FACE': + return 换脸; + default: + return 未知; + } } function renderCode(code) { - switch (code) { - case 1: - return 已提交; - case 21: - return 等待中; - case 22: - return 重复提交; - case 0: - return 未提交; - default: - return 未知; - } + switch (code) { + case 1: + return 已提交; + case 21: + return 等待中; + case 22: + return 重复提交; + case 0: + return 未提交; + default: + return 未知; + } } function renderStatus(type) { - // Ensure all cases are string literals by adding quotes. - switch (type) { - case 'SUCCESS': - return 成功; - case 'NOT_START': - return 未启动; - case 'SUBMITTED': - return 队列中; - case 'IN_PROGRESS': - return 执行中; - case 'FAILURE': - return 失败; - case 'MODAL': - return 窗口等待; - default: - return 未知; - } + // Ensure all cases are string literals by adding quotes. + switch (type) { + case 'SUCCESS': + return 成功; + case 'NOT_START': + return 未启动; + case 'SUBMITTED': + return 队列中; + case 'IN_PROGRESS': + return 执行中; + case 'FAILURE': + return 失败; + case 'MODAL': + return 窗口等待; + default: + return 未知; + } } const renderTimestamp = (timestampInSeconds) => { - const date = new Date(timestampInSeconds * 1000); // 从秒转换为毫秒 + const date = new Date(timestampInSeconds * 1000); // 从秒转换为毫秒 - const year = date.getFullYear(); // 获取年份 - const month = ('0' + (date.getMonth() + 1)).slice(-2); // 获取月份,从0开始需要+1,并保证两位数 - const day = ('0' + date.getDate()).slice(-2); // 获取日期,并保证两位数 - const hours = ('0' + date.getHours()).slice(-2); // 获取小时,并保证两位数 - const minutes = ('0' + date.getMinutes()).slice(-2); // 获取分钟,并保证两位数 - const seconds = ('0' + date.getSeconds()).slice(-2); // 获取秒钟,并保证两位数 + const year = date.getFullYear(); // 获取年份 + const month = ('0' + (date.getMonth() + 1)).slice(-2); // 获取月份,从0开始需要+1,并保证两位数 + const day = ('0' + date.getDate()).slice(-2); // 获取日期,并保证两位数 + const hours = ('0' + date.getHours()).slice(-2); // 获取小时,并保证两位数 + const minutes = ('0' + date.getMinutes()).slice(-2); // 获取分钟,并保证两位数 + const seconds = ('0' + date.getSeconds()).slice(-2); // 获取秒钟,并保证两位数 - return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; // 格式化输出 + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; // 格式化输出 }; const LogsTable = () => { - const [isModalOpen, setIsModalOpen] = useState(false); - const [modalContent, setModalContent] = useState(''); - const columns = [ - { - title: '提交时间', - dataIndex: 'submit_time', - render: (text, record, index) => { - return ( -
- {renderTimestamp(text / 1000)} -
- ); - }, - }, - { - title: '渠道', - dataIndex: 'channel_id', - className: isAdmin() ? 'tableShow' : 'tableHiddle', - render: (text, record, index) => { - return ( + const [isModalOpen, setIsModalOpen] = useState(false); + const [modalContent, setModalContent] = useState(''); + const columns = [ + { + title: '提交时间', + dataIndex: 'submit_time', + render: (text, record, index) => { + return ( +
+ {renderTimestamp(text / 1000)} +
+ ); + } + }, + { + title: '渠道', + dataIndex: 'channel_id', + className: isAdmin() ? 'tableShow' : 'tableHiddle', + render: (text, record, index) => { + return ( -
- { - copyText(text); // 假设copyText是用于文本复制的函数 - }}> {text} -
+
+ { + copyText(text); // 假设copyText是用于文本复制的函数 + }}> {text} +
- ); - }, - }, - { - title: '类型', - dataIndex: 'action', - render: (text, record, index) => { - return ( -
- {renderType(text)} -
- ); - }, - }, - { - title: '任务ID', - dataIndex: 'mj_id', - render: (text, record, index) => { - return ( -
- {text} -
- ); - }, - }, - { - title: '提交结果', - dataIndex: 'code', - className: isAdmin() ? 'tableShow' : 'tableHiddle', - render: (text, record, index) => { - return ( -
- {renderCode(text)} -
- ); - }, - }, - { - title: '任务状态', - dataIndex: 'status', - className: isAdmin() ? 'tableShow' : 'tableHiddle', - render: (text, record, index) => { - return ( -
- {renderStatus(text)} -
- ); - }, - }, - { - title: '进度', - dataIndex: 'progress', - render: (text, record, index) => { - return ( -
- { - // 转换例如100%为数字100,如果text未定义,返回0 - - } -
- ); - }, - }, - { - title: '结果图片', - dataIndex: 'image_url', - render: (text, record, index) => { - if (!text) { - return '无'; - } - return ( - - ); - } - }, - { - title: 'Prompt', - dataIndex: 'prompt', - render: (text, record, index) => { - // 如果text未定义,返回替代文本,例如空字符串''或其他 - if (!text) { - return '无'; - } - - return ( - { - setModalContent(text); - setIsModalOpen(true); - }} - > - {text} - - ); - } - }, - { - title: 'PromptEn', - dataIndex: 'prompt_en', - render: (text, record, index) => { - // 如果text未定义,返回替代文本,例如空字符串''或其他 - if (!text) { - return '无'; - } - - return ( - { - setModalContent(text); - setIsModalOpen(true); - }} - > - {text} - - ); - } - }, - { - title: '失败原因', - dataIndex: 'fail_reason', - render: (text, record, index) => { - // 如果text未定义,返回替代文本,例如空字符串''或其他 - if (!text) { - return '无'; - } - - return ( - { - setModalContent(text); - setIsModalOpen(true); - }} - > - {text} - - ); + ); + } + }, + { + title: '类型', + dataIndex: 'action', + render: (text, record, index) => { + return ( +
+ {renderType(text)} +
+ ); + } + }, + { + title: '任务ID', + dataIndex: 'mj_id', + render: (text, record, index) => { + return ( +
+ {text} +
+ ); + } + }, + { + title: '提交结果', + dataIndex: 'code', + className: isAdmin() ? 'tableShow' : 'tableHiddle', + render: (text, record, index) => { + return ( +
+ {renderCode(text)} +
+ ); + } + }, + { + title: '任务状态', + dataIndex: 'status', + className: isAdmin() ? 'tableShow' : 'tableHiddle', + render: (text, record, index) => { + return ( +
+ {renderStatus(text)} +
+ ); + } + }, + { + title: '进度', + dataIndex: 'progress', + render: (text, record, index) => { + return ( +
+ { + // 转换例如100%为数字100,如果text未定义,返回0 + } +
+ ); + } + }, + { + title: '结果图片', + dataIndex: 'image_url', + render: (text, record, index) => { + if (!text) { + return '无'; + } + return ( + + ); + } + }, + { + title: 'Prompt', + dataIndex: 'prompt', + render: (text, record, index) => { + // 如果text未定义,返回替代文本,例如空字符串''或其他 + if (!text) { + return '无'; } - ]; - - const [logs, setLogs] = useState([]); - const [loading, setLoading] = useState(true); - const [activePage, setActivePage] = useState(1); - const [logCount, setLogCount] = useState(ITEMS_PER_PAGE); - const [logType, setLogType] = useState(0); - const isAdminUser = isAdmin(); - const [isModalOpenurl, setIsModalOpenurl] = useState(false); - - // 定义模态框图片URL的状态和更新函数 - const [modalImageUrl, setModalImageUrl] = useState(''); - let now = new Date(); - // 初始化start_timestamp为前一天 - const [inputs, setInputs] = useState({ - channel_id: '', - mj_id: '', - start_timestamp: timestamp2string(now.getTime() / 1000 - 2592000), - 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 - }); - - 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); - logs[i].key = '' + logs[i].id; + return ( + { + setModalContent(text); + setIsModalOpen(true); + }} + > + {text} + + ); + } + }, + { + title: 'PromptEn', + dataIndex: 'prompt_en', + render: (text, record, index) => { + // 如果text未定义,返回替代文本,例如空字符串''或其他 + if (!text) { + return '无'; } - // data.key = '' + data.id - setLogs(logs); - setLogCount(logs.length + ITEMS_PER_PAGE); - // console.log(logCount); + + return ( + { + setModalContent(text); + setIsModalOpen(true); + }} + > + {text} + + ); + } + }, + { + title: '失败原因', + dataIndex: 'fail_reason', + render: (text, record, index) => { + // 如果text未定义,返回替代文本,例如空字符串''或其他 + if (!text) { + return '无'; + } + + return ( + { + setModalContent(text); + setIsModalOpen(true); + }} + > + {text} + + ); + } } - const loadLogs = async (startIdx) => { - setLoading(true); + ]; - let url = ''; - let localStartTimestamp = Date.parse(start_timestamp); - let localEndTimestamp = Date.parse(end_timestamp); - if (isAdminUser) { - url = `/api/mj/?p=${startIdx}&channel_id=${channel_id}&mj_id=${mj_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`; - } else { - url = `/api/mj/self/?p=${startIdx}&mj_id=${mj_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`; - } - const res = await API.get(url); - const {success, message, data} = res.data; - if (success) { - if (startIdx === 0) { - setLogsFormat(data); - } else { - let newLogs = [...logs]; - newLogs.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data); - setLogsFormat(newLogs); - } - } else { - showError(message); - } - setLoading(false); - }; + const [logs, setLogs] = useState([]); + const [loading, setLoading] = useState(true); + const [activePage, setActivePage] = useState(1); + const [logCount, setLogCount] = useState(ITEMS_PER_PAGE); + const [logType, setLogType] = useState(0); + const isAdminUser = isAdmin(); + const [isModalOpenurl, setIsModalOpenurl] = useState(false); + const [showBanner, setShowBanner] = useState(false); - const pageData = logs.slice((activePage - 1) * ITEMS_PER_PAGE, activePage * ITEMS_PER_PAGE); + // 定义模态框图片URL的状态和更新函数 + const [modalImageUrl, setModalImageUrl] = useState(''); + let now = new Date(); + // 初始化start_timestamp为前一天 + const [inputs, setInputs] = useState({ + channel_id: '', + mj_id: '', + start_timestamp: timestamp2string(now.getTime() / 1000 - 2592000), + end_timestamp: timestamp2string(now.getTime() / 1000 + 3600) + }); + const { channel_id, mj_id, start_timestamp, end_timestamp } = inputs; - 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 => { - }); - } - }; + const [stat, setStat] = useState({ + quota: 0, + token: 0 + }); - const refresh = async () => { - // setLoading(true); - setActivePage(1); - await loadLogs(0); - }; + const handleInputChange = (value, name) => { + setInputs((inputs) => ({ ...inputs, [name]: value })); + }; - const copyText = async (text) => { - if (await copy(text)) { - showSuccess('已复制:' + text); - } else { - // setSearchKeyword(text); - Modal.error({title: '无法复制到剪贴板,请手动复制', content: text}); - } + + const setLogsFormat = (logs) => { + for (let i = 0; i < logs.length; i++) { + logs[i].timestamp2string = timestamp2string(logs[i].created_at); + logs[i].key = '' + logs[i].id; } + // data.key = '' + data.id + setLogs(logs); + setLogCount(logs.length + ITEMS_PER_PAGE); + // console.log(logCount); + }; - useEffect(() => { - refresh().then(); - }, [logType]); + const loadLogs = async (startIdx) => { + setLoading(true); + let url = ''; + let localStartTimestamp = Date.parse(start_timestamp); + let localEndTimestamp = Date.parse(end_timestamp); + if (isAdminUser) { + url = `/api/mj/?p=${startIdx}&channel_id=${channel_id}&mj_id=${mj_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`; + } else { + url = `/api/mj/self/?p=${startIdx}&mj_id=${mj_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`; + } + const res = await API.get(url); + const { success, message, data } = res.data; + if (success) { + if (startIdx === 0) { + setLogsFormat(data); + } else { + let newLogs = [...logs]; + newLogs.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data); + setLogsFormat(newLogs); + } + } else { + showError(message); + } + setLoading(false); + }; - return ( - <> + const pageData = logs.slice((activePage - 1) * ITEMS_PER_PAGE, activePage * ITEMS_PER_PAGE); - -
- <> - handleInputChange(value, 'channel_id')}/> - handleInputChange(value, 'mj_id')}/> - handleInputChange(value, 'start_timestamp')}/> - handleInputChange(value, 'end_timestamp')}/> + 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 => { + }); + } + }; - - - - - -
- setIsModalOpen(false)} - onCancel={() => setIsModalOpen(false)} - closable={null} - bodyStyle={{height: '400px', overflow: 'auto'}} // 设置模态框内容区域样式 - width={800} // 设置模态框宽度 - > -

{modalContent}

-
- setIsModalOpenurl(visible)} - /> + const refresh = async () => { + // setLoading(true); + setActivePage(1); + await loadLogs(0); + }; - - - ); + const copyText = async (text) => { + if (await copy(text)) { + showSuccess('已复制:' + text); + } else { + // setSearchKeyword(text); + Modal.error({ title: '无法复制到剪贴板,请手动复制', content: text }); + } + }; + + useEffect(() => { + refresh().then(); + }, [logType]); + + useEffect(() => { + const mjNotifyEnabled = localStorage.getItem('mj_notify_enabled'); + if (mjNotifyEnabled !== 'true') { + setShowBanner(true); + } + }, []); + + return ( + <> + + + {isAdminUser && showBanner ? : <> + } +
+ <> + handleInputChange(value, 'channel_id')} /> + handleInputChange(value, 'mj_id')} /> + handleInputChange(value, 'start_timestamp')} /> + handleInputChange(value, 'end_timestamp')} /> + + + + + + +
+ setIsModalOpen(false)} + onCancel={() => setIsModalOpen(false)} + closable={null} + bodyStyle={{ height: '400px', overflow: 'auto' }} // 设置模态框内容区域样式 + width={800} // 设置模态框宽度 + > +

{modalContent}

+
+ setIsModalOpenurl(visible)} + /> + + + + ); }; export default LogsTable; diff --git a/web/src/components/OperationSetting.js b/web/src/components/OperationSetting.js index 1a3a4be..874c552 100644 --- a/web/src/components/OperationSetting.js +++ b/web/src/components/OperationSetting.js @@ -1,453 +1,468 @@ -import React, {useEffect, useState} from 'react'; -import {Divider, Form, Grid, Header} from 'semantic-ui-react'; -import {API, showError, showSuccess, timestamp2string, verifyJSON} from '../helpers'; +import React, { useEffect, useState } from 'react'; +import { Divider, Form, Grid, Header } from 'semantic-ui-react'; +import { API, showError, showSuccess, timestamp2string, verifyJSON } from '../helpers'; const OperationSetting = () => { - let now = new Date(); - let [inputs, setInputs] = useState({ - QuotaForNewUser: 0, - QuotaForInviter: 0, - QuotaForInvitee: 0, - QuotaRemindThreshold: 0, - PreConsumedQuota: 0, - ModelRatio: '', - ModelPrice: '', - GroupRatio: '', - TopUpLink: '', - ChatLink: '', - ChatLink2: '', // 添加的新状态变量 - QuotaPerUnit: 0, - AutomaticDisableChannelEnabled: '', - AutomaticEnableChannelEnabled: '', - ChannelDisableThreshold: 0, - LogConsumeEnabled: '', - DisplayInCurrencyEnabled: '', - DisplayTokenStatEnabled: '', - DrawingEnabled: '', - DataExportEnabled: '', - DataExportDefaultTime: 'hour', - DataExportInterval: 5, - DefaultCollapseSidebar: '', // 默认折叠侧边栏 - RetryTimes: 0 + let now = new Date(); + let [inputs, setInputs] = useState({ + QuotaForNewUser: 0, + QuotaForInviter: 0, + QuotaForInvitee: 0, + QuotaRemindThreshold: 0, + PreConsumedQuota: 0, + ModelRatio: '', + ModelPrice: '', + GroupRatio: '', + TopUpLink: '', + ChatLink: '', + ChatLink2: '', // 添加的新状态变量 + QuotaPerUnit: 0, + AutomaticDisableChannelEnabled: '', + AutomaticEnableChannelEnabled: '', + ChannelDisableThreshold: 0, + LogConsumeEnabled: '', + DisplayInCurrencyEnabled: '', + DisplayTokenStatEnabled: '', + MjNotifyEnabled: '', + DrawingEnabled: '', + DataExportEnabled: '', + DataExportDefaultTime: 'hour', + DataExportInterval: 5, + DefaultCollapseSidebar: '', // 默认折叠侧边栏 + 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 + // 精确时间选项(小时,天,周) + const timeOptions = [ + { key: 'hour', text: '小时', value: 'hour' }, + { key: 'day', text: '天', value: 'day' }, + { key: 'week', text: '周', value: 'week' } + ]; + const getOptions = async () => { + const res = await API.get('/api/option/'); + const { success, message, data } = res.data; + if (success) { + let newInputs = {}; + data.forEach((item) => { + if (item.key === 'ModelRatio' || item.key === 'GroupRatio' || item.key === 'ModelPrice') { + item.value = JSON.stringify(JSON.parse(item.value), null, 2); + } + newInputs[item.key] = item.value; + }); + setInputs(newInputs); + setOriginInputs(newInputs); + } else { + showError(message); + } + }; + + useEffect(() => { + getOptions().then(); + }, []); + + const updateOption = async (key, value) => { + setLoading(true); + if (key.endsWith('Enabled')) { + value = inputs[key] === 'true' ? 'false' : 'true'; + } + if (key === 'DefaultCollapseSidebar') { + value = inputs[key] === 'true' ? 'false' : 'true'; + } + console.log(key, value); + const res = await API.put('/api/option/', { + key, + value }); - const [originInputs, setOriginInputs] = useState({}); - let [loading, setLoading] = useState(false); - 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'} - ]; - const getOptions = async () => { - const res = await API.get('/api/option/'); - const {success, message, data} = res.data; - if (success) { - let newInputs = {}; - data.forEach((item) => { - if (item.key === 'ModelRatio' || item.key === 'GroupRatio' || item.key === 'ModelPrice') { - item.value = JSON.stringify(JSON.parse(item.value), null, 2); - } - newInputs[item.key] = item.value; - }); - setInputs(newInputs); - setOriginInputs(newInputs); - } else { - showError(message); - } - }; + const { success, message } = res.data; + if (success) { + setInputs((inputs) => ({ ...inputs, [key]: value })); + } else { + showError(message); + } + setLoading(false); + }; - useEffect(() => { - getOptions().then(); - }, []); + const handleInputChange = async (e, { name, value }) => { + if (name.endsWith('Enabled') || name === 'DataExportInterval' || name === 'DataExportDefaultTime' || name === 'DefaultCollapseSidebar') { + if (name === 'DataExportDefaultTime') { + localStorage.setItem('data_export_default_time', value); + } else if (name === 'MjNotifyEnabled') { + localStorage.setItem('mj_notify_enabled', value); + } + await updateOption(name, value); + } else { + setInputs((inputs) => ({ ...inputs, [name]: value })); + } + }; - const updateOption = async (key, value) => { - setLoading(true); - if (key.endsWith('Enabled')) { - value = inputs[key] === 'true' ? 'false' : 'true'; + const submitConfig = async (group) => { + switch (group) { + case 'monitor': + if (originInputs['ChannelDisableThreshold'] !== inputs.ChannelDisableThreshold) { + await updateOption('ChannelDisableThreshold', inputs.ChannelDisableThreshold); } - if (key === 'DefaultCollapseSidebar') { - value = inputs[key] === 'true' ? 'false' : 'true'; + if (originInputs['QuotaRemindThreshold'] !== inputs.QuotaRemindThreshold) { + await updateOption('QuotaRemindThreshold', inputs.QuotaRemindThreshold); } - console.log(key, value) - const res = await API.put('/api/option/', { - key, - value - }); - const {success, message} = res.data; - if (success) { - setInputs((inputs) => ({...inputs, [key]: value})); - } else { - showError(message); - } - setLoading(false); - }; - - const handleInputChange = async (e, {name, value}) => { - if (name.endsWith('Enabled') || name === 'DataExportInterval' || name === 'DataExportDefaultTime' || name === 'DefaultCollapseSidebar') { - if (name === 'DataExportDefaultTime') { - localStorage.setItem('data_export_default_time', value); - } - await updateOption(name, value); - } else { - setInputs((inputs) => ({...inputs, [name]: value})); - } - }; - - const submitConfig = async (group) => { - switch (group) { - case 'monitor': - if (originInputs['ChannelDisableThreshold'] !== inputs.ChannelDisableThreshold) { - await updateOption('ChannelDisableThreshold', inputs.ChannelDisableThreshold); - } - if (originInputs['QuotaRemindThreshold'] !== inputs.QuotaRemindThreshold) { - await updateOption('QuotaRemindThreshold', inputs.QuotaRemindThreshold); - } - break; - case 'ratio': - if (originInputs['ModelRatio'] !== inputs.ModelRatio) { - if (!verifyJSON(inputs.ModelRatio)) { - showError('模型倍率不是合法的 JSON 字符串'); - return; - } - await updateOption('ModelRatio', inputs.ModelRatio); - } - if (originInputs['GroupRatio'] !== inputs.GroupRatio) { - if (!verifyJSON(inputs.GroupRatio)) { - showError('分组倍率不是合法的 JSON 字符串'); - return; - } - await updateOption('GroupRatio', inputs.GroupRatio); - } - if (originInputs['ModelPrice'] !== inputs.ModelPrice) { - if (!verifyJSON(inputs.ModelPrice)) { - showError('模型固定价格不是合法的 JSON 字符串'); - return; - } - await updateOption('ModelPrice', inputs.ModelPrice); - } - break; - case 'quota': - if (originInputs['QuotaForNewUser'] !== inputs.QuotaForNewUser) { - await updateOption('QuotaForNewUser', inputs.QuotaForNewUser); - } - if (originInputs['QuotaForInvitee'] !== inputs.QuotaForInvitee) { - await updateOption('QuotaForInvitee', inputs.QuotaForInvitee); - } - if (originInputs['QuotaForInviter'] !== inputs.QuotaForInviter) { - await updateOption('QuotaForInviter', inputs.QuotaForInviter); - } - if (originInputs['PreConsumedQuota'] !== inputs.PreConsumedQuota) { - await updateOption('PreConsumedQuota', inputs.PreConsumedQuota); - } - break; - case 'general': - if (originInputs['TopUpLink'] !== inputs.TopUpLink) { - await updateOption('TopUpLink', inputs.TopUpLink); - } - if (originInputs['ChatLink'] !== inputs.ChatLink) { - await updateOption('ChatLink', inputs.ChatLink); - } - if (originInputs['ChatLink2'] !== inputs.ChatLink2) { - await updateOption('ChatLink2', inputs.ChatLink2); - } - if (originInputs['QuotaPerUnit'] !== inputs.QuotaPerUnit) { - await updateOption('QuotaPerUnit', inputs.QuotaPerUnit); - } - if (originInputs['RetryTimes'] !== inputs.RetryTimes) { - await updateOption('RetryTimes', inputs.RetryTimes); - } - break; - } - }; - - const deleteHistoryLogs = async () => { - console.log(inputs); - const res = await API.delete(`/api/log/?target_timestamp=${Date.parse(historyTimestamp) / 1000}`); - const {success, message, data} = res.data; - if (success) { - showSuccess(`${data} 条日志已清理!`); + break; + case 'ratio': + if (originInputs['ModelRatio'] !== inputs.ModelRatio) { + if (!verifyJSON(inputs.ModelRatio)) { + showError('模型倍率不是合法的 JSON 字符串'); return; + } + await updateOption('ModelRatio', inputs.ModelRatio); } - showError('日志清理失败:' + message); - }; - return ( - - -
-
- 通用设置 -
- - - - - - - - + if (originInputs['GroupRatio'] !== inputs.GroupRatio) { + if (!verifyJSON(inputs.GroupRatio)) { + showError('分组倍率不是合法的 JSON 字符串'); + return; + } + await updateOption('GroupRatio', inputs.GroupRatio); + } + if (originInputs['ModelPrice'] !== inputs.ModelPrice) { + if (!verifyJSON(inputs.ModelPrice)) { + showError('模型固定价格不是合法的 JSON 字符串'); + return; + } + await updateOption('ModelPrice', inputs.ModelPrice); + } + break; + case 'quota': + if (originInputs['QuotaForNewUser'] !== inputs.QuotaForNewUser) { + await updateOption('QuotaForNewUser', inputs.QuotaForNewUser); + } + if (originInputs['QuotaForInvitee'] !== inputs.QuotaForInvitee) { + await updateOption('QuotaForInvitee', inputs.QuotaForInvitee); + } + if (originInputs['QuotaForInviter'] !== inputs.QuotaForInviter) { + await updateOption('QuotaForInviter', inputs.QuotaForInviter); + } + if (originInputs['PreConsumedQuota'] !== inputs.PreConsumedQuota) { + await updateOption('PreConsumedQuota', inputs.PreConsumedQuota); + } + break; + case 'general': + if (originInputs['TopUpLink'] !== inputs.TopUpLink) { + await updateOption('TopUpLink', inputs.TopUpLink); + } + if (originInputs['ChatLink'] !== inputs.ChatLink) { + await updateOption('ChatLink', inputs.ChatLink); + } + if (originInputs['ChatLink2'] !== inputs.ChatLink2) { + await updateOption('ChatLink2', inputs.ChatLink2); + } + if (originInputs['QuotaPerUnit'] !== inputs.QuotaPerUnit) { + await updateOption('QuotaPerUnit', inputs.QuotaPerUnit); + } + if (originInputs['RetryTimes'] !== inputs.RetryTimes) { + await updateOption('RetryTimes', inputs.RetryTimes); + } + break; + } + }; - - - - - - { - submitConfig('general').then(); - }}>保存通用设置 -
- 日志设置 -
- - - - - { - setHistoryTimestamp(value); - }}/> - - { - deleteHistoryLogs().then(); - }}>清理历史日志 - -
- 数据看板 -
- - - - - - -
- 监控设置 -
- - - - - - - - - { - submitConfig('monitor').then(); - }}>保存监控设置 - -
- 额度设置 -
- - - - - - - { - submitConfig('quota').then(); - }}>保存额度设置 - -
- 倍率设置 -
- - - - - - - - - - { - submitConfig('ratio').then(); - }}>保存倍率设置 - -
-
- ) - ; + const deleteHistoryLogs = async () => { + console.log(inputs); + const res = await API.delete(`/api/log/?target_timestamp=${Date.parse(historyTimestamp) / 1000}`); + const { success, message, data } = res.data; + if (success) { + showSuccess(`${data} 条日志已清理!`); + return; + } + showError('日志清理失败:' + message); + }; + return ( + + +
+
+ 通用设置 +
+ + + + + + + + + + + + + { + submitConfig('general').then(); + }}>保存通用设置 + +
+ 绘图设置 +
+ + + + + +
+ 日志设置 +
+ + + + + { + setHistoryTimestamp(value); + }} /> + + { + deleteHistoryLogs().then(); + }}>清理历史日志 + +
+ 数据看板 +
+ + + + + + +
+ 监控设置 +
+ + + + + + + + + { + submitConfig('monitor').then(); + }}>保存监控设置 + +
+ 额度设置 +
+ + + + + + + { + submitConfig('quota').then(); + }}>保存额度设置 + +
+ 倍率设置 +
+ + + + + + + + + + { + submitConfig('ratio').then(); + }}>保存倍率设置 + +
+
+ ) + ; }; export default OperationSetting; diff --git a/web/src/components/OtherSetting.js b/web/src/components/OtherSetting.js index 878c2b0..014492d 100644 --- a/web/src/components/OtherSetting.js +++ b/web/src/components/OtherSetting.js @@ -1,5 +1,5 @@ import React, { useEffect, useRef, useState } from 'react'; -import { Col, Row , Form, Button, Banner } from '@douyinfe/semi-ui'; +import { Banner, Button, Col, Form, Row } from '@douyinfe/semi-ui'; import { API, showError, showSuccess } from '../helpers'; import { marked } from 'marked'; @@ -57,8 +57,8 @@ const OtherSetting = () => { await updateOption('Notice', inputs.Notice); showSuccess('公告已更新'); } catch (error) { - console.error("公告更新失败", error); - showError("公告更新失败") + console.error('公告更新失败', error); + showError('公告更新失败'); } finally { setLoadingInput((loadingInput) => ({ ...loadingInput, Notice: false })); } @@ -72,8 +72,8 @@ const OtherSetting = () => { await updateOption('SystemName', inputs.SystemName); showSuccess('系统名称已更新'); } catch (error) { - console.error("系统名称更新失败", error); - showError("系统名称更新失败") + console.error('系统名称更新失败', error); + showError('系统名称更新失败'); } finally { setLoadingInput((loadingInput) => ({ ...loadingInput, SystemName: false })); } @@ -86,8 +86,8 @@ const OtherSetting = () => { await updateOption('Logo', inputs.Logo); showSuccess('Logo 已更新'); } catch (error) { - console.error("Logo 更新失败", error); - showError("Logo 更新失败") + console.error('Logo 更新失败', error); + showError('Logo 更新失败'); } finally { setLoadingInput((loadingInput) => ({ ...loadingInput, Logo: false })); } @@ -99,8 +99,8 @@ const OtherSetting = () => { await updateOption(key, inputs[key]); showSuccess('首页内容已更新'); } catch (error) { - console.error("首页内容更新失败", error); - showError("首页内容更新失败") + console.error('首页内容更新失败', error); + showError('首页内容更新失败'); } finally { setLoadingInput((loadingInput) => ({ ...loadingInput, HomePageContent: false })); } @@ -112,8 +112,8 @@ const OtherSetting = () => { await updateOption('About', inputs.About); showSuccess('关于内容已更新'); } catch (error) { - console.error("关于内容更新失败", error); - showError("关于内容更新失败"); + console.error('关于内容更新失败', error); + showError('关于内容更新失败'); } finally { setLoadingInput((loadingInput) => ({ ...loadingInput, About: false })); } @@ -125,16 +125,14 @@ const OtherSetting = () => { await updateOption('Footer', inputs.Footer); showSuccess('页脚内容已更新'); } catch (error) { - console.error("页脚内容更新失败", error); - showError("页脚内容更新失败"); + console.error('页脚内容更新失败', error); + showError('页脚内容更新失败'); } finally { setLoadingInput((loadingInput) => ({ ...loadingInput, Footer: false })); } }; - - const openGitHubRelease = () => { window.location = 'https://github.com/songquanpeng/one-api/releases/latest'; @@ -173,16 +171,17 @@ const OtherSetting = () => { } }; - useEffect( () => { + useEffect(() => { getOptions(); }, []); return ( - +
{/* 通用设置 */} - formAPISettingGeneral.current = formAPI} style={{marginBottom: 15}}> + formAPISettingGeneral.current = formAPI} + style={{ marginBottom: 15 }}> { onChange={handleInputChange} style={{ fontFamily: 'JetBrains Mono, Consolas' }} autosize={{ minRows: 6, maxRows: 12 }} - /> + /> {/* 个性化设置 */} - formAPIPersonalization.current = formAPI} style={{marginBottom: 15}}> + formAPIPersonalization.current = formAPI} + style={{ marginBottom: 15 }}> + /> + /> { onChange={handleInputChange} style={{ fontFamily: 'JetBrains Mono, Consolas' }} autosize={{ minRows: 6, maxRows: 12 }} - /> - + /> + { onChange={handleInputChange} style={{ fontFamily: 'JetBrains Mono, Consolas' }} autosize={{ minRows: 6, maxRows: 12 }} - /> + /> {/* */} { type="info" description="移除 One API 的版权标识必须首先获得授权,项目维护需要花费大量精力,如果本项目对你有意义,请主动支持本项目。" closeIcon={null} - style={{ marginTop: 15 }} + style={{ marginTop: 15 }} /> + /> @@ -270,7 +271,7 @@ const OtherSetting = () => { {/* />*/} {/* */} {/**/} - + ); }; diff --git a/web/src/components/PasswordResetConfirm.js b/web/src/components/PasswordResetConfirm.js index d82ae89..071837a 100644 --- a/web/src/components/PasswordResetConfirm.js +++ b/web/src/components/PasswordResetConfirm.js @@ -1,12 +1,12 @@ import React, { useEffect, useState } from 'react'; import { Button, Form, Grid, Header, Image, Segment } from 'semantic-ui-react'; -import { API, copy, showError, showInfo, showNotice, showSuccess } from '../helpers'; +import { API, copy, showError, showNotice } from '../helpers'; 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 }); }, []); @@ -37,7 +37,7 @@ const PasswordResetConfirm = () => { setDisableButton(false); setCountdown(30); } - return () => clearInterval(countdownInterval); + return () => clearInterval(countdownInterval); }, [disableButton, countdown]); async function handleSubmit(e) { @@ -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) { @@ -59,44 +59,44 @@ const PasswordResetConfirm = () => { } setLoading(false); } - + return ( - + -
- 密码重置确认 +
+ 密码重置确认
-
+ {newPassword && ( { - e.target.select(); - navigator.clipboard.writeText(newPassword); - showNotice(`密码已复制到剪贴板:${newPassword}`); - }} - /> + fluid + icon="lock" + iconPosition="left" + placeholder="新密码" + name="newPassword" + value={newPassword} + readOnly + onClick={(e) => { + e.target.select(); + navigator.clipboard.writeText(newPassword); + showNotice(`密码已复制到剪贴板:${newPassword}`); + }} + /> )} - - {renderQuota(userState?.user?.aff_history_quota)} - {userState?.user?.aff_count} - - - - - 个人信息 -
- 邮箱 -
-
- -
-
- -
-
-
-
- 微信 -
-
- -
-
- -
-
-
-
- GitHub -
-
- -
-
- -
-
-
-
- LINUX DO -
-
- -
-
- -
-
-
+ + + {renderQuota(userState?.user?.aff_history_quota)} + {userState?.user?.aff_count} + + +
+ + 个人信息 +
+ 邮箱 +
+
+ +
+
+ +
+
+
+
+ 微信 +
+
+ +
+
+ +
+
+
+
+ GitHub +
+
+ +
+
+ +
+
+
+
+ LINUX DO +
+
+ +
+
+ +
+
+
-
- Telegram -
-
- -
-
- {status.telegram_oauth ? - userState.user.telegram_id !== '' ? - : - : - } -
-
-
+
+ Telegram +
+
+ +
+
+ {status.telegram_oauth ? + userState.user.telegram_id !== '' ? + : + : + } +
+
+
-
- - - - - +
+ + + + + - {systemToken && ( - - )} - { - status.wechat_login && ( - - ) - } - setShowWeChatBindModal(false)} - // onOpen={() => setShowWeChatBindModal(true)} - visible={showWeChatBindModal} - size={'mini'} - > - -
-

- 微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效) -

-
- handleInputChange('wechat_verification_code', v)} - /> - -
-
- - setShowEmailBindModal(false)} - // onOpen={() => setShowEmailBindModal(true)} - onOk={bindEmail} - visible={showEmailBindModal} - size={'small'} - centered={true} - maskClosable={false} + {systemToken && ( + + )} + { + status.wechat_login && ( + -
-
- handleInputChange('email_verification_code', value)} - /> -
- {turnstileEnabled ? ( - { - setTurnstileToken(token); - }} - /> - ) : ( - <> - )} - - setShowAccountDeleteModal(false)} - visible={showAccountDeleteModal} - size={'small'} - centered={true} - onOk={deleteAccount} - > -
- -
-
- handleInputChange('self_account_deletion_confirmation', value)} - /> - {turnstileEnabled ? ( - { - setTurnstileToken(token); - }} - /> - ) : ( - <> - )} -
-
- setShowChangePasswordModal(false)} - visible={showChangePasswordModal} - size={'small'} - centered={true} - onOk={changePassword} - > -
- handleInputChange('set_new_password', value)} - /> - handleInputChange('set_new_password_confirmation', value)} - /> - {turnstileEnabled ? ( - { - setTurnstileToken(token); - }} - /> - ) : ( - <> - )} -
-
- + 绑定微信账号 + + ) + } + setShowWeChatBindModal(false)} + // onOpen={() => setShowWeChatBindModal(true)} + visible={showWeChatBindModal} + size={'mini'} + > + +
+

+ 微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效) +

+
+ handleInputChange('wechat_verification_code', v)} + /> + +
+ +
+ setShowEmailBindModal(false)} + // onOpen={() => setShowEmailBindModal(true)} + onOk={bindEmail} + visible={showEmailBindModal} + size={'small'} + centered={true} + maskClosable={false} + > + 绑定邮箱地址 +
+ handleInputChange('email', value)} + name="email" + type="email" + /> + +
+
+ handleInputChange('email_verification_code', value)} + /> +
+ {turnstileEnabled ? ( + { + setTurnstileToken(token); + }} + /> + ) : ( + <> + )} +
+ setShowAccountDeleteModal(false)} + visible={showAccountDeleteModal} + size={'small'} + centered={true} + onOk={deleteAccount} + > +
+ +
+
+ handleInputChange('self_account_deletion_confirmation', value)} + /> + {turnstileEnabled ? ( + { + setTurnstileToken(token); + }} + /> + ) : ( + <> + )} +
+
+ setShowChangePasswordModal(false)} + visible={showChangePasswordModal} + size={'small'} + centered={true} + onOk={changePassword} + > +
+ handleInputChange('set_new_password', value)} + /> + handleInputChange('set_new_password_confirmation', value)} + /> + {turnstileEnabled ? ( + { + setTurnstileToken(token); + }} + /> + ) : ( + <> + )} +
+
+ - - - - ); + + + + ); }; export default PersonalSetting; diff --git a/web/src/components/PrivateRoute.js b/web/src/components/PrivateRoute.js index f7cc724..9ef826c 100644 --- a/web/src/components/PrivateRoute.js +++ b/web/src/components/PrivateRoute.js @@ -5,7 +5,7 @@ import { history } from '../helpers'; function PrivateRoute({ children }) { if (!localStorage.getItem('user')) { - return ; + return ; } return children; } diff --git a/web/src/components/RedemptionsTable.js b/web/src/components/RedemptionsTable.js index 8d3814d..8c9c96a 100644 --- a/web/src/components/RedemptionsTable.js +++ b/web/src/components/RedemptionsTable.js @@ -1,406 +1,406 @@ -import React, {useEffect, useState} from 'react'; -import {API, copy, showError, showInfo, showSuccess, showWarning, timestamp2string} from '../helpers'; +import React, { useEffect, useState } from 'react'; +import { API, copy, showError, showSuccess, timestamp2string } from '../helpers'; -import {ITEMS_PER_PAGE} from '../constants'; -import {renderQuota} from '../helpers/render'; -import {Button, Modal, Popconfirm, Popover, Table, Tag, Form} from "@douyinfe/semi-ui"; -import EditRedemption from "../pages/Redemption/EditRedemption"; +import { ITEMS_PER_PAGE } from '../constants'; +import { renderQuota } from '../helpers/render'; +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 未使用; - case 2: - return 已禁用 ; - case 3: - return 已使用 ; - default: - return 未知状态 ; - } + switch (status) { + case 1: + return 未使用; + case 2: + return 已禁用 ; + case 3: + return 已使用 ; + default: + return 未知状态 ; + } } const RedemptionsTable = () => { - const columns = [ - { - title: 'ID', - dataIndex: 'id', - }, - { - title: '名称', - dataIndex: 'name', - }, - { - title: '状态', - dataIndex: 'status', - key: 'status', - render: (text, record, index) => { - return ( -
- {renderStatus(text)} -
- ); - }, - }, - { - title: '额度', - dataIndex: 'quota', - render: (text, record, index) => { - return ( -
- {renderQuota(parseInt(text))} -
- ); - }, - }, - { - title: '创建时间', - dataIndex: 'created_time', - render: (text, record, index) => { - return ( -
- {renderTimestamp(text)} -
- ); - }, - }, - { - title: '兑换人ID', - dataIndex: 'used_user_id', - render: (text, record, index) => { - return ( -
- {text === 0 ? '无' : text} -
- ); - }, - }, - { - title: '', - dataIndex: 'operate', - render: (text, record, index) => ( -
- - - - - { - manageRedemption(record.id, 'delete', record).then( - () => { - removeRecord(record.key); - } - ) - }} - > - - - { - record.status === 1 ? - : - - } - -
- ), - }, - ]; - - const [redemptions, setRedemptions] = useState([]); - const [loading, setLoading] = useState(true); - const [activePage, setActivePage] = useState(1); - const [searchKeyword, setSearchKeyword] = useState(''); - const [searching, setSearching] = useState(false); - const [tokenCount, setTokenCount] = useState(ITEMS_PER_PAGE); - const [selectedKeys, setSelectedKeys] = useState([]); - const [editingRedemption, setEditingRedemption] = useState({ - id: undefined, - }); - const [showEdit, setShowEdit] = useState(false); - - const closeEdit = () => { - setShowEdit(false); - } - - // const setCount = (data) => { - // if (data.length >= (activePage) * ITEMS_PER_PAGE) { - // setTokenCount(data.length + 1); - // } else { - // setTokenCount(data.length); - // } - // } - - const setRedemptionFormat = (redeptions) => { - // for (let i = 0; i < redeptions.length; i++) { - // redeptions[i].key = '' + redeptions[i].id; - // } - // data.key = '' + data.id - setRedemptions(redeptions); - if (redeptions.length >= (activePage) * ITEMS_PER_PAGE) { - setTokenCount(redeptions.length + 1); - } else { - setTokenCount(redeptions.length); - } - } - - const loadRedemptions = async (startIdx) => { - const res = await API.get(`/api/redemption/?p=${startIdx}`); - const {success, message, data} = res.data; - if (success) { - if (startIdx === 0) { - setRedemptionFormat(data); - } else { - let newRedemptions = redemptions; - newRedemptions.push(...data); - setRedemptionFormat(newRedemptions); + const columns = [ + { + title: 'ID', + dataIndex: 'id' + }, + { + title: '名称', + dataIndex: 'name' + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + render: (text, record, index) => { + return ( +
+ {renderStatus(text)} +
+ ); + } + }, + { + title: '额度', + dataIndex: 'quota', + render: (text, record, index) => { + return ( +
+ {renderQuota(parseInt(text))} +
+ ); + } + }, + { + title: '创建时间', + dataIndex: 'created_time', + render: (text, record, index) => { + return ( +
+ {renderTimestamp(text)} +
+ ); + } + }, + { + title: '兑换人ID', + dataIndex: 'used_user_id', + render: (text, record, index) => { + return ( +
+ {text === 0 ? '无' : text} +
+ ); + } + }, + { + title: '', + dataIndex: 'operate', + render: (text, record, index) => ( +
+ { - let newDataSource = [...redemptions]; - if (key != null) { - let idx = newDataSource.findIndex(data => data.key === key); - - if (idx > -1) { - newDataSource.splice(idx, 1); - setRedemptions(newDataSource); - } - } - }; - - const copyText = async (text) => { - if (await copy(text)) { - showSuccess('已复制到剪贴板!'); - } else { - // setSearchKeyword(text); - Modal.error({title: '无法复制到剪贴板,请手动复制', content: text}); - } - } - - const onPaginationChange = (e, {activePage}) => { - (async () => { - if (activePage === Math.ceil(redemptions.length / ITEMS_PER_PAGE) + 1) { - // In this case we have to load more data and then append them. - await loadRedemptions(activePage - 1); - } - setActivePage(activePage); - })(); - }; - - useEffect(() => { - loadRedemptions(0) - .then() - .catch((reason) => { - showError(reason); - }); - }, []); - - const refresh = async () => { - await loadRedemptions(activePage - 1); - }; - - const manageRedemption = async (id, action, record) => { - let data = {id}; - let res; - switch (action) { - case 'delete': - res = await API.delete(`/api/redemption/${id}/`); - break; - case 'enable': - data.status = 1; - res = await API.put('/api/redemption/?status_only=true', data); - break; - case 'disable': - data.status = 2; - res = await API.put('/api/redemption/?status_only=true', data); - break; - } - const {success, message} = res.data; - if (success) { - showSuccess('操作成功完成!'); - let redemption = res.data.data; - let newRedemptions = [...redemptions]; - // let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx; - if (action === 'delete') { - - } else { - record.status = redemption.status; - } - setRedemptions(newRedemptions); - } else { - showError(message); - } - }; - - const searchRedemptions = async () => { - if (searchKeyword === '') { - // if keyword is blank, load files instead. - await loadRedemptions(0); - setActivePage(1); - return; - } - setSearching(true); - const res = await API.get(`/api/redemption/search?keyword=${searchKeyword}`); - const {success, message, data} = res.data; - if (success) { - setRedemptions(data); - setActivePage(1); - } else { - showError(message); - } - setSearching(false); - }; - - const handleKeywordChange = async (value) => { - setSearchKeyword(value.trim()); - }; - - const sortRedemption = (key) => { - if (redemptions.length === 0) return; - setLoading(true); - let sortedRedemptions = [...redemptions]; - sortedRedemptions.sort((a, b) => { - return ('' + a[key]).localeCompare(b[key]); - }); - if (sortedRedemptions[0].id === redemptions[0].id) { - sortedRedemptions.reverse(); - } - setRedemptions(sortedRedemptions); - setLoading(false); - }; - - 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 => { - }); - } - }; - - let pageData = redemptions.slice((activePage - 1) * ITEMS_PER_PAGE, activePage * ITEMS_PER_PAGE); - const rowSelection = { - 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)', - }, - }; - } else { - return {}; - } - }; - - return ( - <> - - - - - -
`第 ${page.currentStart} - ${page.currentEnd} 条,共 ${redemptions.length} 条`, - // onPageSizeChange: (size) => { - // setPageSize(size); - // setActivePage(1); - // }, - onPageChange: handlePageChange, - }} loading={loading} rowSelection={rowSelection} onRow={handleRow}> -
- + + + { + manageRedemption(record.id, 'delete', record).then( () => { - setEditingRedemption({ - id: undefined, - }); - setShowEdit(true); + removeRecord(record.key); } - }>添加兑换码 - + + { + record.status === 1 ? + - - ); + }>禁用 : + + } + + + ) + } + ]; + + const [redemptions, setRedemptions] = useState([]); + const [loading, setLoading] = useState(true); + const [activePage, setActivePage] = useState(1); + const [searchKeyword, setSearchKeyword] = useState(''); + const [searching, setSearching] = useState(false); + const [tokenCount, setTokenCount] = useState(ITEMS_PER_PAGE); + const [selectedKeys, setSelectedKeys] = useState([]); + const [editingRedemption, setEditingRedemption] = useState({ + id: undefined + }); + const [showEdit, setShowEdit] = useState(false); + + const closeEdit = () => { + setShowEdit(false); + }; + + // const setCount = (data) => { + // if (data.length >= (activePage) * ITEMS_PER_PAGE) { + // setTokenCount(data.length + 1); + // } else { + // setTokenCount(data.length); + // } + // } + + const setRedemptionFormat = (redeptions) => { + // for (let i = 0; i < redeptions.length; i++) { + // redeptions[i].key = '' + redeptions[i].id; + // } + // data.key = '' + data.id + setRedemptions(redeptions); + if (redeptions.length >= (activePage) * ITEMS_PER_PAGE) { + setTokenCount(redeptions.length + 1); + } else { + setTokenCount(redeptions.length); + } + }; + + const loadRedemptions = async (startIdx) => { + const res = await API.get(`/api/redemption/?p=${startIdx}`); + const { success, message, data } = res.data; + if (success) { + if (startIdx === 0) { + setRedemptionFormat(data); + } else { + let newRedemptions = redemptions; + newRedemptions.push(...data); + setRedemptionFormat(newRedemptions); + } + } else { + showError(message); + } + setLoading(false); + }; + + const removeRecord = key => { + let newDataSource = [...redemptions]; + if (key != null) { + let idx = newDataSource.findIndex(data => data.key === key); + + if (idx > -1) { + newDataSource.splice(idx, 1); + setRedemptions(newDataSource); + } + } + }; + + const copyText = async (text) => { + if (await copy(text)) { + showSuccess('已复制到剪贴板!'); + } else { + // setSearchKeyword(text); + Modal.error({ title: '无法复制到剪贴板,请手动复制', content: text }); + } + }; + + const onPaginationChange = (e, { activePage }) => { + (async () => { + if (activePage === Math.ceil(redemptions.length / ITEMS_PER_PAGE) + 1) { + // In this case we have to load more data and then append them. + await loadRedemptions(activePage - 1); + } + setActivePage(activePage); + })(); + }; + + useEffect(() => { + loadRedemptions(0) + .then() + .catch((reason) => { + showError(reason); + }); + }, []); + + const refresh = async () => { + await loadRedemptions(activePage - 1); + }; + + const manageRedemption = async (id, action, record) => { + let data = { id }; + let res; + switch (action) { + case 'delete': + res = await API.delete(`/api/redemption/${id}/`); + break; + case 'enable': + data.status = 1; + res = await API.put('/api/redemption/?status_only=true', data); + break; + case 'disable': + data.status = 2; + res = await API.put('/api/redemption/?status_only=true', data); + break; + } + const { success, message } = res.data; + if (success) { + showSuccess('操作成功完成!'); + let redemption = res.data.data; + let newRedemptions = [...redemptions]; + // let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx; + if (action === 'delete') { + + } else { + record.status = redemption.status; + } + setRedemptions(newRedemptions); + } else { + showError(message); + } + }; + + const searchRedemptions = async () => { + if (searchKeyword === '') { + // if keyword is blank, load files instead. + await loadRedemptions(0); + setActivePage(1); + return; + } + setSearching(true); + const res = await API.get(`/api/redemption/search?keyword=${searchKeyword}`); + const { success, message, data } = res.data; + if (success) { + setRedemptions(data); + setActivePage(1); + } else { + showError(message); + } + setSearching(false); + }; + + const handleKeywordChange = async (value) => { + setSearchKeyword(value.trim()); + }; + + const sortRedemption = (key) => { + if (redemptions.length === 0) return; + setLoading(true); + let sortedRedemptions = [...redemptions]; + sortedRedemptions.sort((a, b) => { + return ('' + a[key]).localeCompare(b[key]); + }); + if (sortedRedemptions[0].id === redemptions[0].id) { + sortedRedemptions.reverse(); + } + setRedemptions(sortedRedemptions); + setLoading(false); + }; + + 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 => { + }); + } + }; + + let pageData = redemptions.slice((activePage - 1) * ITEMS_PER_PAGE, activePage * ITEMS_PER_PAGE); + const rowSelection = { + 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)' + } + }; + } else { + return {}; + } + }; + + return ( + <> + +
+ + + + `第 ${page.currentStart} - ${page.currentEnd} 条,共 ${redemptions.length} 条`, + // onPageSizeChange: (size) => { + // setPageSize(size); + // setActivePage(1); + // }, + onPageChange: handlePageChange + }} loading={loading} rowSelection={rowSelection} onRow={handleRow}> +
+ + + + ); }; export default RedemptionsTable; diff --git a/web/src/components/RegisterForm.js b/web/src/components/RegisterForm.js index f91d6da..1f26b63 100644 --- a/web/src/components/RegisterForm.js +++ b/web/src/components/RegisterForm.js @@ -98,49 +98,49 @@ const RegisterForm = () => { }; return ( - + -
+
新用户注册
-
+ {showEmailVerification ? ( <> 获取验证码 @@ -149,11 +149,11 @@ const RegisterForm = () => { /> ) : ( @@ -170,9 +170,9 @@ const RegisterForm = () => { <> )} - - - - } - - - - - - - - - - - - -
- 配置邮箱域名白名单 - 用以防止恶意用户利用临时邮箱批量注册 -
- - - - - - { - submitNewRestrictedDomain(); - }}>填入 - } - onKeyDown={(e) => { - if (e.key === 'Enter') { - submitNewRestrictedDomain(); - } - }} - autoComplete='new-password' - placeholder='输入新的允许的邮箱域名' - value={restrictedDomainInput} - onChange={(e, { value }) => { - setRestrictedDomainInput(value); - }} - /> - - 保存邮箱域名白名单设置 - -
- 配置 SMTP - 用以支持系统的邮件发送 -
- - - - - - - - - - 保存 SMTP 设置 - -
- 配置 GitHub OAuth App - - 用以支持通过 GitHub 进行登录注册, - - 点击此处 - - 管理你的 GitHub OAuth App - -
- - Homepage URL 填 {inputs.ServerAddress} - ,Authorization callback URL 填{' '} - {`${inputs.ServerAddress}/oauth/github`} - - - - - - - 保存 GitHub OAuth 设置 - - -
- 配置 LINUX DO Oauth - - 用以支持通过 LINUX DO 进行登录注册, - - 点击此处 - - 管理你的 LINUX DO OAuth - -
- - Homepage URL 填 {inputs.ServerAddress} - ,Authorization callback URL 填{' '} - {`${inputs.ServerAddress}/oauth/linuxdo`} - - - - - - - 保存 LINUX DO OAuth 设置 - - -
- 配置 WeChat Server - - 用以支持通过微信进行登录注册, - - 点击此处 - - 了解 WeChat Server - -
- - - - - - - 保存 WeChat Server 设置 - - -
配置 Telegram 登录
- - - - - - 保存 Telegram 登录设置 - - -
- 配置 Turnstile - - 用以支持用户校验, - - 点击此处 - - 管理你的 Turnstile Sites,推荐选择 Invisible Widget Type - -
- - - - - - 保存 Turnstile 设置 - - - - - ); + const updateOption = async (key, value) => { + setLoading(true); + switch (key) { + case 'PasswordLoginEnabled': + case 'PasswordRegisterEnabled': + case 'EmailVerificationEnabled': + case 'GitHubOAuthEnabled': + case 'LinuxDoOAuthEnabled': + case 'WeChatAuthEnabled': + case 'TelegramOAuthEnabled': + case 'TurnstileCheckEnabled': + case 'EmailDomainRestrictionEnabled': + case 'RegisterEnabled': + value = inputs[key] === 'true' ? 'false' : 'true'; + break; + default: + break; + } + const res = await API.put('/api/option/', { + key, + value + }); + const { success, message } = res.data; + if (success) { + if (key === 'EmailDomainWhitelist') { + value = value.split(','); + } + if (key === 'Price') { + value = parseFloat(value); + } + setInputs((inputs) => ({ + ...inputs, [key]: value + })); + } else { + showError(message); + } + setLoading(false); + }; + + const handleInputChange = async (e, { name, value }) => { + if (name === 'PasswordLoginEnabled' && inputs[name] === 'true') { + // block disabling password login + setShowPasswordWarningModal(true); + return; + } + if ( + name === 'Notice' || + name.startsWith('SMTP') || + name === 'ServerAddress' || + name === 'EpayId' || + name === 'EpayKey' || + name === 'Price' || + name === 'PayAddress' || + name === 'GitHubClientId' || + name === 'GitHubClientSecret' || + name === 'LinuxDoClientId' || + name === 'LinuxDoClientSecret' || + name === 'WeChatServerAddress' || + name === 'WeChatServerToken' || + name === 'WeChatAccountQRCodeImageURL' || + name === 'TurnstileSiteKey' || + name === 'TurnstileSecretKey' || + name === 'EmailDomainWhitelist' || + name === 'TopupGroupRatio' || + name === 'TelegramBotToken' || + name === 'TelegramBotName' + ) { + setInputs((inputs) => ({ ...inputs, [name]: value })); + } else { + await updateOption(name, value); + } + }; + + const submitServerAddress = async () => { + let ServerAddress = removeTrailingSlash(inputs.ServerAddress); + await updateOption('ServerAddress', ServerAddress); + }; + + const submitPayAddress = async () => { + if (inputs.ServerAddress === '') { + showError('请先填写服务器地址'); + return; + } + if (originInputs['TopupGroupRatio'] !== inputs.TopupGroupRatio) { + if (!verifyJSON(inputs.TopupGroupRatio)) { + showError('充值分组倍率不是合法的 JSON 字符串'); + return; + } + await updateOption('TopupGroupRatio', inputs.TopupGroupRatio); + } + let PayAddress = removeTrailingSlash(inputs.PayAddress); + await updateOption('PayAddress', PayAddress); + if (inputs.EpayId !== '') { + await updateOption('EpayId', inputs.EpayId); + } + if (inputs.EpayKey !== '') { + await updateOption('EpayKey', inputs.EpayKey); + } + await updateOption('Price', '' + inputs.Price); + }; + + const submitSMTP = async () => { + if (originInputs['SMTPServer'] !== inputs.SMTPServer) { + await updateOption('SMTPServer', inputs.SMTPServer); + } + if (originInputs['SMTPAccount'] !== inputs.SMTPAccount) { + await updateOption('SMTPAccount', inputs.SMTPAccount); + } + if (originInputs['SMTPFrom'] !== inputs.SMTPFrom) { + await updateOption('SMTPFrom', inputs.SMTPFrom); + } + if ( + originInputs['SMTPPort'] !== inputs.SMTPPort && + inputs.SMTPPort !== '' + ) { + await updateOption('SMTPPort', inputs.SMTPPort); + } + if ( + originInputs['SMTPToken'] !== inputs.SMTPToken && + inputs.SMTPToken !== '' + ) { + await updateOption('SMTPToken', inputs.SMTPToken); + } + }; + + + const submitEmailDomainWhitelist = async () => { + if ( + originInputs['EmailDomainWhitelist'] !== inputs.EmailDomainWhitelist.join(',') && + inputs.SMTPToken !== '' + ) { + await updateOption('EmailDomainWhitelist', inputs.EmailDomainWhitelist.join(',')); + } + }; + + const submitWeChat = async () => { + if (originInputs['WeChatServerAddress'] !== inputs.WeChatServerAddress) { + await updateOption( + 'WeChatServerAddress', + removeTrailingSlash(inputs.WeChatServerAddress) + ); + } + if ( + originInputs['WeChatAccountQRCodeImageURL'] !== + inputs.WeChatAccountQRCodeImageURL + ) { + await updateOption( + 'WeChatAccountQRCodeImageURL', + inputs.WeChatAccountQRCodeImageURL + ); + } + if ( + originInputs['WeChatServerToken'] !== inputs.WeChatServerToken && + inputs.WeChatServerToken !== '' + ) { + await updateOption('WeChatServerToken', inputs.WeChatServerToken); + } + }; + + const submitGitHubOAuth = async () => { + if (originInputs['GitHubClientId'] !== inputs.GitHubClientId) { + await updateOption('GitHubClientId', inputs.GitHubClientId); + } + if ( + originInputs['GitHubClientSecret'] !== inputs.GitHubClientSecret && + inputs.GitHubClientSecret !== '' + ) { + await updateOption('GitHubClientSecret', inputs.GitHubClientSecret); + } + }; + + const submitLinuxDoOAuth = async () => { + if (originInputs['LinuxDoClientId'] !== inputs.LinuxDoClientId) { + await updateOption('LinuxDoClientId', inputs.LinuxDoClientId); + } + if ( + originInputs['LinuxDoClientSecret'] !== inputs.LinuxDoClientSecret && + inputs.LinuxDoClientSecret !== '' + ) { + await updateOption('LinuxDoClientSecret', inputs.LinuxDoClientSecret); + } + }; + + const submitTelegramSettings = async () => { + // await updateOption('TelegramOAuthEnabled', inputs.TelegramOAuthEnabled); + await updateOption('TelegramBotToken', inputs.TelegramBotToken); + await updateOption('TelegramBotName', inputs.TelegramBotName); + }; + + const submitTurnstile = async () => { + if (originInputs['TurnstileSiteKey'] !== inputs.TurnstileSiteKey) { + await updateOption('TurnstileSiteKey', inputs.TurnstileSiteKey); + } + if ( + originInputs['TurnstileSecretKey'] !== inputs.TurnstileSecretKey && + inputs.TurnstileSecretKey !== '' + ) { + await updateOption('TurnstileSecretKey', inputs.TurnstileSecretKey); + } + }; + + const submitNewRestrictedDomain = () => { + const localDomainList = inputs.EmailDomainWhitelist; + if (restrictedDomainInput !== '' && !localDomainList.includes(restrictedDomainInput)) { + setRestrictedDomainInput(''); + setInputs({ + ...inputs, + EmailDomainWhitelist: [...localDomainList, restrictedDomainInput] + }); + setEmailDomainWhitelist([...EmailDomainWhitelist, { + key: restrictedDomainInput, + text: restrictedDomainInput, + value: restrictedDomainInput + }]); + } + }; + + return ( + + +
+
通用设置
+ + + + + 更新服务器地址 + + +
支付设置(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!)
+ + + + + + + + + + + + + + + + 更新支付设置 + + +
配置登录注册
+ + + { + showPasswordWarningModal && + setShowPasswordWarningModal(false)} + size={'tiny'} + style={{ maxWidth: '450px' }} + > + 警告 + +

取消密码登录将导致所有未绑定其他登录方式的用户(包括管理员)无法通过密码登录,确认取消?

+
+ + + + +
+ } + + + + + + +
+ + + + + +
+ 配置邮箱域名白名单 + 用以防止恶意用户利用临时邮箱批量注册 +
+ + + + + + { + submitNewRestrictedDomain(); + }}>填入 + } + onKeyDown={(e) => { + if (e.key === 'Enter') { + submitNewRestrictedDomain(); + } + }} + autoComplete="new-password" + placeholder="输入新的允许的邮箱域名" + value={restrictedDomainInput} + onChange={(e, { value }) => { + setRestrictedDomainInput(value); + }} + /> + + 保存邮箱域名白名单设置 + +
+ 配置 SMTP + 用以支持系统的邮件发送 +
+ + + + + + + + + + 保存 SMTP 设置 + +
+ 配置 GitHub OAuth App + + 用以支持通过 GitHub 进行登录注册, + + 点击此处 + + 管理你的 GitHub OAuth App + +
+ + Homepage URL 填 {inputs.ServerAddress} + ,Authorization callback URL 填{' '} + {`${inputs.ServerAddress}/oauth/github`} + + + + + + + 保存 GitHub OAuth 设置 + + +
+ 配置 LINUX DO Oauth + + 用以支持通过 LINUX DO 进行登录注册, + + 点击此处 + + 管理你的 LINUX DO OAuth + +
+ + Homepage URL 填 {inputs.ServerAddress} + ,Authorization callback URL 填{' '} + {`${inputs.ServerAddress}/oauth/linuxdo`} + + + + + + + 保存 LINUX DO OAuth 设置 + + +
+ 配置 WeChat Server + + 用以支持通过微信进行登录注册, + + 点击此处 + + 了解 WeChat Server + +
+ + + + + + + 保存 WeChat Server 设置 + + +
配置 Telegram 登录
+ + + + + + 保存 Telegram 登录设置 + + +
+ 配置 Turnstile + + 用以支持用户校验, + + 点击此处 + + 管理你的 Turnstile Sites,推荐选择 Invisible Widget Type + +
+ + + + + + 保存 Turnstile 设置 + + +
+
+ ); }; export default SystemSetting; diff --git a/web/src/components/TokensTable.js b/web/src/components/TokensTable.js index fdd1329..5901bfd 100644 --- a/web/src/components/TokensTable.js +++ b/web/src/components/TokensTable.js @@ -1,562 +1,586 @@ -import React, {useEffect, useState} from 'react'; -import {API, copy, isAdmin, showError, showSuccess, showWarning, timestamp2string} from '../helpers'; +import React, { useEffect, useState } from 'react'; +import { API, copy, showError, showSuccess, timestamp2string } from '../helpers'; -import {ITEMS_PER_PAGE} from '../constants'; -import {renderQuota, stringToColor} from '../helpers/render'; -import { - Avatar, - Tag, - Table, - Button, - Popover, - Form, - Modal, - Popconfirm, - SplitButtonGroup, - Dropdown -} from "@douyinfe/semi-ui"; +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 { IconTreeTriangleDown } from '@douyinfe/semi-icons'; +import EditToken from '../pages/Token/EditToken'; -import { - IconTreeTriangleDown, -} from '@douyinfe/semi-icons'; -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: 'next', text: 'ChatGPT Next Web', value: 'next' }, + { key: 'ama', text: 'ChatGPT Web & Midjourney', value: 'ama' }, + { 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: 'ama', text: 'ChatGPT Web & Midjourney', value: 'ama' }, + { 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 已启用:限制模型; - } else { - return 已启用; - } - case 2: - return 已禁用 ; - case 3: - return 已过期 ; - case 4: - return 已耗尽 ; - default: - return 未知状态 ; - } + switch (status) { + case 1: + if (model_limits_enabled) { + return 已启用:限制模型; + } else { + return 已启用; + } + case 2: + return 已禁用 ; + case 3: + return 已过期 ; + case 4: + return 已耗尽 ; + default: + return 未知状态 ; + } } const TokensTable = () => { - const link_menu = [ - {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: () => {onOpenLink('next-mj')}}, - {node: 'item', key: 'opencat', name: 'OpenCat', value: 'opencat'}, - ]; + const link_menu = [ + { + 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: () => { + onOpenLink('next-mj'); + } + }, + { node: 'item', key: 'opencat', name: 'OpenCat', value: 'opencat' } + ]; - const columns = [ - { - title: '名称', - dataIndex: 'name', - }, - { - title: '状态', - dataIndex: 'status', - key: 'status', - render: (text, record, index) => { - return ( -
- {renderStatus(text, record.model_limits_enabled)} -
- ); - }, - }, - { - title: '已用额度', - dataIndex: 'used_quota', - render: (text, record, index) => { - return ( -
- {renderQuota(parseInt(text))} -
- ); - }, - }, - { - title: '剩余额度', - dataIndex: 'remain_quota', - render: (text, record, index) => { - return ( -
- {record.unlimited_quota ? 无限制 : {renderQuota(parseInt(text))}} -
- ); - }, - }, - { - title: '创建时间', - dataIndex: 'created_time', - render: (text, record, index) => { - return ( -
- {renderTimestamp(text)} -
- ); - }, - }, - { - title: '过期时间', - dataIndex: 'expired_time', - render: (text, record, index) => { - return ( -
- {record.expired_time === -1 ? "永不过期" : renderTimestamp(text)} -
- ); - }, - }, - { - title: '', - dataIndex: 'operate', - render: (text, record, index) => ( -
- - - - - - - {onOpenLink('next', record.key)}}, - {node: 'item', key: 'next-mj', disabled: !localStorage.getItem('chat_link2'), name: 'ChatGPT Web & Midjourney', onClick: () => {onOpenLink('next-mj', record.key)}}, - {node: 'item', key: 'ama', name: 'AMA 问天(BotGem)', onClick: () => {onOpenLink('ama', record.key)}}, - {node: 'item', key: 'opencat', name: 'OpenCat', onClick: () => {onOpenLink('opencat', record.key)}}, - ] - } - > - - - - { - manageToken(record.id, 'delete', record).then( - () => { - removeRecord(record.key); - } - ) - }} - > - - - { - record.status === 1 ? - : - - } - -
- ), - }, - ]; - - const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE); - const [showEdit, setShowEdit] = useState(false); - const [tokens, setTokens] = useState([]); - const [selectedKeys, setSelectedKeys] = useState([]); - const [tokenCount, setTokenCount] = useState(pageSize); - const [loading, setLoading] = useState(true); - const [activePage, setActivePage] = useState(1); - const [searchKeyword, setSearchKeyword] = useState(''); - const [searchToken, setSearchToken] = useState(''); - const [searching, setSearching] = useState(false); - const [showTopUpModal, setShowTopUpModal] = useState(false); - const [targetTokenIdx, setTargetTokenIdx] = useState(0); - const [editingToken, setEditingToken] = useState({ - id: undefined, - }); - - const closeEdit = () => { - setShowEdit(false); - setTimeout(() => { - setEditingToken({ - id: undefined, - }); - }, 500); - } - - const setTokensFormat = (tokens) => { - setTokens(tokens); - if (tokens.length >= pageSize) { - setTokenCount(tokens.length + pageSize); - } else { - setTokenCount(tokens.length); - } - } - - 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}`); - const {success, message, data} = res.data; - if (success) { - if (startIdx === 0) { - setTokensFormat(data); - } else { - let newTokens = [...tokens]; - newTokens.splice(startIdx * pageSize, data.length, ...data); - setTokensFormat(newTokens); + const columns = [ + { + title: '名称', + dataIndex: 'name' + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + render: (text, record, index) => { + return ( +
+ {renderStatus(text, record.model_limits_enabled)} +
+ ); + } + }, + { + title: '已用额度', + dataIndex: 'used_quota', + render: (text, record, index) => { + return ( +
+ {renderQuota(parseInt(text))} +
+ ); + } + }, + { + title: '剩余额度', + dataIndex: 'remain_quota', + render: (text, record, index) => { + return ( +
+ {record.unlimited_quota ? 无限制 : + {renderQuota(parseInt(text))}} +
+ ); + } + }, + { + title: '创建时间', + dataIndex: 'created_time', + render: (text, record, index) => { + return ( +
+ {renderTimestamp(text)} +
+ ); + } + }, + { + title: '过期时间', + dataIndex: 'expired_time', + render: (text, record, index) => { + return ( +
+ {record.expired_time === -1 ? '永不过期' : renderTimestamp(text)} +
+ ); + } + }, + { + title: '', + dataIndex: 'operate', + render: (text, record, index) => ( +
+ { - (async () => { - if (activePage === Math.ceil(tokens.length / pageSize) + 1) { - // In this case we have to load more data and then append them. - await loadTokens(activePage - 1); - } - setActivePage(activePage); - })(); - }; - - const refresh = async () => { - await loadTokens(activePage - 1); - }; - - const onCopy = async (type, key) => { - let status = localStorage.getItem('status'); - let serverAddress = ''; - if (status) { - status = JSON.parse(status); - serverAddress = status.server_address; - } - if (serverAddress === '') { - serverAddress = window.location.origin; - } - let encodedServerAddress = encodeURIComponent(serverAddress); - const nextLink = localStorage.getItem('chat_link'); - const mjLink = localStorage.getItem('chat_link2'); - let nextUrl; - - if (nextLink) { - nextUrl = nextLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; - } else { - nextUrl = `https://chat.oneapi.pro/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; - } - - let url; - switch (type) { - case 'ama': - url = mjLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; - break; - case 'opencat': - url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`; - break; - case 'next': - url = nextUrl; - break; - default: - url = `sk-${key}`; - } - // if (await copy(url)) { - // showSuccess('已复制到剪贴板!'); - // } else { - // showWarning('无法复制到剪贴板,请手动复制,已将令牌填入搜索框。'); - // setSearchKeyword(url); - // } - }; - - const copyText = async (text) => { - if (await copy(text)) { - showSuccess('已复制到剪贴板!'); - } else { - // setSearchKeyword(text); - Modal.error({ title: '无法复制到剪贴板,请手动复制', content: text }); - } - } - - const onOpenLink = async (type, key) => { - let status = localStorage.getItem('status'); - let serverAddress = ''; - if (status) { - status = JSON.parse(status); - serverAddress = status.server_address; - } - if (serverAddress === '') { - serverAddress = window.location.origin; - } - let encodedServerAddress = encodeURIComponent(serverAddress); - const chatLink = localStorage.getItem('chat_link'); - const mjLink = localStorage.getItem('chat_link2'); - let defaultUrl; - - if (chatLink) { - defaultUrl = chatLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; - } - let url; - switch (type) { - case 'ama': - url = `ama://set-api-key?server=${encodedServerAddress}&key=sk-${key}`; - break; - case 'opencat': - url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`; - break; - case 'next-mj': - url = mjLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; - break; - default: - if (!chatLink) { - showError('管理员未设置聊天链接') - return; + style={{ padding: 20 }} + position="top" + > + + + + + + { + onOpenLink('next', record.key); + } + }, + { + node: 'item', + key: 'next-mj', + disabled: !localStorage.getItem('chat_link2'), + name: 'ChatGPT Web & Midjourney', + onClick: () => { + onOpenLink('next-mj', record.key); + } + }, + { + node: 'item', key: 'ama', name: 'AMA 问天(BotGem)', onClick: () => { + onOpenLink('ama', record.key); + } + }, + { + node: 'item', key: 'opencat', name: 'OpenCat', onClick: () => { + onOpenLink('opencat', record.key); + } } - url = defaultUrl; - } - - window.open(url, '_blank'); - } - - useEffect(() => { - loadTokens(0) - .then() - .catch((reason) => { - showError(reason); - }); - }, [pageSize]); - - const removeRecord = key => { - let newDataSource = [...tokens]; - if (key != null) { - let idx = newDataSource.findIndex(data => data.key === key); - - if (idx > -1) { - newDataSource.splice(idx, 1); - setTokensFormat(newDataSource); + ] } - } - }; - - const manageToken = async (id, action, record) => { - setLoading(true); - let data = {id}; - let res; - switch (action) { - case 'delete': - res = await API.delete(`/api/token/${id}/`); - break; - case 'enable': - data.status = 1; - res = await API.put('/api/token/?status_only=true', data); - break; - case 'disable': - data.status = 2; - res = await API.put('/api/token/?status_only=true', data); - break; - } - const {success, message} = res.data; - if (success) { - showSuccess('操作成功完成!'); - let token = res.data.data; - let newTokens = [...tokens]; - // let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx; - if (action === 'delete') { - - } else { - record.status = token.status; - // newTokens[realIdx].status = token.status; - } - setTokensFormat(newTokens); - } else { - showError(message); - } - setLoading(false); - }; - - const searchTokens = async () => { - if (searchKeyword === '' && searchToken === '') { - // if keyword is blank, load files instead. - await loadTokens(0); - setActivePage(1); - return; - } - setSearching(true); - const res = await API.get(`/api/token/search?keyword=${searchKeyword}&token=${searchToken}`); - const {success, message, data} = res.data; - if (success) { - setTokensFormat(data); - setActivePage(1); - } else { - showError(message); - } - setSearching(false); - }; - - const handleKeywordChange = async (value) => { - setSearchKeyword(value.trim()); - }; - - const handleSearchTokenChange = async (value) => { - setSearchToken(value.trim()); - }; - - const sortToken = (key) => { - if (tokens.length === 0) return; - setLoading(true); - let sortedTokens = [...tokens]; - sortedTokens.sort((a, b) => { - return ('' + a[key]).localeCompare(b[key]); - }); - if (sortedTokens[0].id === tokens[0].id) { - sortedTokens.reverse(); - } - setTokens(sortedTokens); - setLoading(false); - }; - - - 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 => { - }); - } - }; - - const rowSelection = { - 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)', - }, - }; - } else { - return {}; - } - }; - - return ( - <> - -
- - - - - - `第 ${page.currentStart} - ${page.currentEnd} 条,共 ${tokens.length} 条`, - onPageSizeChange: (size) => { - setPageSize(size); - setActivePage(1); - }, - onPageChange: handlePageChange, - }} loading={loading} rowSelection={rowSelection} onRow={handleRow}> -
- +
+
+ { + manageToken(record.id, 'delete', record).then( () => { - setEditingToken({ - id: undefined, - }); - setShowEdit(true); + removeRecord(record.key); } - }>添加令牌 - + + { + record.status === 1 ? + - - ); + }>禁用 : + + } + +
+ ) + } + ]; + + const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE); + const [showEdit, setShowEdit] = useState(false); + const [tokens, setTokens] = useState([]); + const [selectedKeys, setSelectedKeys] = useState([]); + const [tokenCount, setTokenCount] = useState(pageSize); + const [loading, setLoading] = useState(true); + const [activePage, setActivePage] = useState(1); + const [searchKeyword, setSearchKeyword] = useState(''); + const [searchToken, setSearchToken] = useState(''); + const [searching, setSearching] = useState(false); + const [showTopUpModal, setShowTopUpModal] = useState(false); + const [targetTokenIdx, setTargetTokenIdx] = useState(0); + const [editingToken, setEditingToken] = useState({ + id: undefined + }); + + const closeEdit = () => { + setShowEdit(false); + setTimeout(() => { + setEditingToken({ + id: undefined + }); + }, 500); + }; + + const setTokensFormat = (tokens) => { + setTokens(tokens); + if (tokens.length >= pageSize) { + setTokenCount(tokens.length + pageSize); + } else { + setTokenCount(tokens.length); + } + }; + + 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}`); + const { success, message, data } = res.data; + if (success) { + if (startIdx === 0) { + setTokensFormat(data); + } else { + let newTokens = [...tokens]; + newTokens.splice(startIdx * pageSize, data.length, ...data); + setTokensFormat(newTokens); + } + } else { + showError(message); + } + setLoading(false); + }; + + const onPaginationChange = (e, { activePage }) => { + (async () => { + if (activePage === Math.ceil(tokens.length / pageSize) + 1) { + // In this case we have to load more data and then append them. + await loadTokens(activePage - 1); + } + setActivePage(activePage); + })(); + }; + + const refresh = async () => { + await loadTokens(activePage - 1); + }; + + const onCopy = async (type, key) => { + let status = localStorage.getItem('status'); + let serverAddress = ''; + if (status) { + status = JSON.parse(status); + serverAddress = status.server_address; + } + if (serverAddress === '') { + serverAddress = window.location.origin; + } + let encodedServerAddress = encodeURIComponent(serverAddress); + const nextLink = localStorage.getItem('chat_link'); + const mjLink = localStorage.getItem('chat_link2'); + let nextUrl; + + if (nextLink) { + nextUrl = nextLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; + } else { + nextUrl = `https://chat.oneapi.pro/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; + } + + let url; + switch (type) { + case 'ama': + url = mjLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; + break; + case 'opencat': + url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`; + break; + case 'next': + url = nextUrl; + break; + default: + url = `sk-${key}`; + } + // if (await copy(url)) { + // showSuccess('已复制到剪贴板!'); + // } else { + // showWarning('无法复制到剪贴板,请手动复制,已将令牌填入搜索框。'); + // setSearchKeyword(url); + // } + }; + + const copyText = async (text) => { + if (await copy(text)) { + showSuccess('已复制到剪贴板!'); + } else { + // setSearchKeyword(text); + Modal.error({ title: '无法复制到剪贴板,请手动复制', content: text }); + } + }; + + const onOpenLink = async (type, key) => { + let status = localStorage.getItem('status'); + let serverAddress = ''; + if (status) { + status = JSON.parse(status); + serverAddress = status.server_address; + } + if (serverAddress === '') { + serverAddress = window.location.origin; + } + let encodedServerAddress = encodeURIComponent(serverAddress); + const chatLink = localStorage.getItem('chat_link'); + const mjLink = localStorage.getItem('chat_link2'); + let defaultUrl; + + if (chatLink) { + defaultUrl = chatLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; + } + let url; + switch (type) { + case 'ama': + url = `ama://set-api-key?server=${encodedServerAddress}&key=sk-${key}`; + break; + case 'opencat': + url = `opencat://team/join?domain=${encodedServerAddress}&token=sk-${key}`; + break; + case 'next-mj': + url = mjLink + `/#/?settings={"key":"sk-${key}","url":"${serverAddress}"}`; + break; + default: + if (!chatLink) { + showError('管理员未设置聊天链接'); + return; + } + url = defaultUrl; + } + + window.open(url, '_blank'); + }; + + useEffect(() => { + loadTokens(0) + .then() + .catch((reason) => { + showError(reason); + }); + }, [pageSize]); + + const removeRecord = key => { + let newDataSource = [...tokens]; + if (key != null) { + let idx = newDataSource.findIndex(data => data.key === key); + + if (idx > -1) { + newDataSource.splice(idx, 1); + setTokensFormat(newDataSource); + } + } + }; + + const manageToken = async (id, action, record) => { + setLoading(true); + let data = { id }; + let res; + switch (action) { + case 'delete': + res = await API.delete(`/api/token/${id}/`); + break; + case 'enable': + data.status = 1; + res = await API.put('/api/token/?status_only=true', data); + break; + case 'disable': + data.status = 2; + res = await API.put('/api/token/?status_only=true', data); + break; + } + const { success, message } = res.data; + if (success) { + showSuccess('操作成功完成!'); + let token = res.data.data; + let newTokens = [...tokens]; + // let realIdx = (activePage - 1) * ITEMS_PER_PAGE + idx; + if (action === 'delete') { + + } else { + record.status = token.status; + // newTokens[realIdx].status = token.status; + } + setTokensFormat(newTokens); + } else { + showError(message); + } + setLoading(false); + }; + + const searchTokens = async () => { + if (searchKeyword === '' && searchToken === '') { + // if keyword is blank, load files instead. + await loadTokens(0); + setActivePage(1); + return; + } + setSearching(true); + const res = await API.get(`/api/token/search?keyword=${searchKeyword}&token=${searchToken}`); + const { success, message, data } = res.data; + if (success) { + setTokensFormat(data); + setActivePage(1); + } else { + showError(message); + } + setSearching(false); + }; + + const handleKeywordChange = async (value) => { + setSearchKeyword(value.trim()); + }; + + const handleSearchTokenChange = async (value) => { + setSearchToken(value.trim()); + }; + + const sortToken = (key) => { + if (tokens.length === 0) return; + setLoading(true); + let sortedTokens = [...tokens]; + sortedTokens.sort((a, b) => { + return ('' + a[key]).localeCompare(b[key]); + }); + if (sortedTokens[0].id === tokens[0].id) { + sortedTokens.reverse(); + } + setTokens(sortedTokens); + setLoading(false); + }; + + + 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 => { + }); + } + }; + + const rowSelection = { + 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)' + } + }; + } else { + return {}; + } + }; + + return ( + <> + +
+ + + + + + `第 ${page.currentStart} - ${page.currentEnd} 条,共 ${tokens.length} 条`, + onPageSizeChange: (size) => { + setPageSize(size); + setActivePage(1); + }, + onPageChange: handlePageChange + }} loading={loading} rowSelection={rowSelection} onRow={handleRow}> +
+ + + + ); }; export default TokensTable; diff --git a/web/src/components/UsersTable.js b/web/src/components/UsersTable.js index ce8e594..7757afa 100644 --- a/web/src/components/UsersTable.js +++ b/web/src/components/UsersTable.js @@ -1,337 +1,338 @@ -import React, {useEffect, useState} from 'react'; -import {API, isAdmin, showError, showSuccess} from '../helpers'; -import {Button, Modal, Popconfirm, Popover, Table, Tag, Form, Tooltip, Space} from "@douyinfe/semi-ui"; -import {ITEMS_PER_PAGE} from '../constants'; -import {renderGroup, renderNumber, renderQuota, renderText, stringToColor} from '../helpers/render'; -import AddUser from "../pages/User/AddUser"; -import EditUser from "../pages/User/EditUser"; +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 { ITEMS_PER_PAGE } from '../constants'; +import { renderGroup, renderNumber, renderQuota } from '../helpers/render'; +import AddUser from '../pages/User/AddUser'; +import EditUser from '../pages/User/EditUser'; function renderRole(role) { - switch (role) { - case 1: - return 普通用户; - case 10: - return 管理员; - case 100: - return 超级管理员; - default: - return 未知身份; - } + switch (role) { + case 1: + return 普通用户; + case 10: + return 管理员; + case 100: + return 超级管理员; + default: + return 未知身份; + } } const UsersTable = () => { - const columns = [{ - title: 'ID', dataIndex: 'id', - }, { - title: '用户名', dataIndex: 'username', - }, { - title: '分组', dataIndex: 'group', render: (text, record, index) => { - return (
- {renderGroup(text)} -
); - }, - }, { - title: '统计信息', dataIndex: 'info', render: (text, record, index) => { - return (
- - - {renderQuota(record.quota)} - - - {renderQuota(record.used_quota)} - - - {renderNumber(record.request_count)} - - -
); - } - }, { - title: '邀请信息', dataIndex: 'invite', render: (text, record, index) => { - return (
- - - {renderNumber(record.aff_count)} - - - {renderQuota(record.aff_history_quota)} - - - {record.inviter_id === 0 ? : - {record.inviter_id}} - - -
); - } - }, { - title: '角色', dataIndex: 'role', render: (text, record, index) => { - return (
- {renderRole(text)} -
); - }, - }, { - title: '状态', dataIndex: 'status', render: (text, record, index) => { - return (
- {record.DeletedAt !== null? 已注销 : renderStatus(text)} -
); - }, - }, { - title: '', dataIndex: 'operate', render: (text, record, index) => (
- { - record.DeletedAt !== null ? <>: - <> - { - manageUser(record.username, 'promote', record) - }} - > - - - { - manageUser(record.username, 'demote', record) - }} - > - - - {record.status === 1 ? - : - } - - - - } + const columns = [{ + title: 'ID', dataIndex: 'id' + }, { + title: '用户名', dataIndex: 'username' + }, { + title: '分组', dataIndex: 'group', render: (text, record, index) => { + return (
+ {renderGroup(text)} +
); + } + }, { + title: '统计信息', dataIndex: 'info', render: (text, record, index) => { + return (
+ + + {renderQuota(record.quota)} + + + {renderQuota(record.used_quota)} + + + {renderNumber(record.request_count)} + + +
); + } + }, { + title: '邀请信息', dataIndex: 'invite', render: (text, record, index) => { + return (
+ + + {renderNumber(record.aff_count)} + + + {renderQuota(record.aff_history_quota)} + + + {record.inviter_id === 0 ? : + {record.inviter_id}} + + +
); + } + }, { + title: '角色', dataIndex: 'role', render: (text, record, index) => { + return (
+ {renderRole(text)} +
); + } + }, { + title: '状态', dataIndex: 'status', render: (text, record, index) => { + return (
+ {record.DeletedAt !== null ? 已注销 : renderStatus(text)} +
); + } + }, { + title: '', dataIndex: 'operate', render: (text, record, index) => (
+ { + record.DeletedAt !== null ? <> : + <> { - manageUser(record.username, 'delete', record).then(() => { - removeRecord(record.id); - }) - }} + title="确定?" + okType={'warning'} + onConfirm={() => { + manageUser(record.username, 'promote', record); + }} > - + -
), - },]; + { + manageUser(record.username, 'demote', record); + }} + > + + + {record.status === 1 ? + : + } + + - const [users, setUsers] = useState([]); - const [loading, setLoading] = useState(true); - const [activePage, setActivePage] = useState(1); - const [searchKeyword, setSearchKeyword] = useState(''); - const [searching, setSearching] = useState(false); - const [userCount, setUserCount] = useState(ITEMS_PER_PAGE); - const [showAddUser, setShowAddUser] = useState(false); - const [showEditUser, setShowEditUser] = useState(false); - const [editingUser, setEditingUser] = useState({ - id: undefined, + } + { + manageUser(record.username, 'delete', record).then(() => { + removeRecord(record.id); + }); + }} + > + + +
) + }]; + + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(true); + const [activePage, setActivePage] = useState(1); + const [searchKeyword, setSearchKeyword] = useState(''); + const [searching, setSearching] = useState(false); + const [userCount, setUserCount] = useState(ITEMS_PER_PAGE); + const [showAddUser, setShowAddUser] = useState(false); + const [showEditUser, setShowEditUser] = useState(false); + const [editingUser, setEditingUser] = useState({ + id: undefined + }); + + const setCount = (data) => { + if (data.length >= (activePage) * ITEMS_PER_PAGE) { + setUserCount(data.length + 1); + } else { + setUserCount(data.length); + } + }; + + const removeRecord = key => { + console.log(key); + let newDataSource = [...users]; + if (key != null) { + let idx = newDataSource.findIndex(data => data.id === key); + + if (idx > -1) { + newDataSource.splice(idx, 1); + setUsers(newDataSource); + } + } + }; + + const loadUsers = async (startIdx) => { + const res = await API.get(`/api/user/?p=${startIdx}`); + const { success, message, data } = res.data; + if (success) { + if (startIdx === 0) { + setUsers(data); + setCount(data); + } else { + let newUsers = users; + newUsers.push(...data); + setUsers(newUsers); + setCount(newUsers); + } + } else { + showError(message); + } + setLoading(false); + }; + + const onPaginationChange = (e, { activePage }) => { + (async () => { + if (activePage === Math.ceil(users.length / ITEMS_PER_PAGE) + 1) { + // In this case we have to load more data and then append them. + await loadUsers(activePage - 1); + } + setActivePage(activePage); + })(); + }; + + useEffect(() => { + loadUsers(0) + .then() + .catch((reason) => { + showError(reason); + }); + }, []); + + const manageUser = async (username, action, record) => { + const res = await API.post('/api/user/manage', { + username, action }); + const { success, message } = res.data; + if (success) { + showSuccess('操作成功完成!'); + let user = res.data.data; + let newUsers = [...users]; + if (action === 'delete') { - const setCount = (data) => { - if (data.length >= (activePage) * ITEMS_PER_PAGE) { - setUserCount(data.length + 1); - } else { - setUserCount(data.length); - } + } else { + record.status = user.status; + record.role = user.role; + } + setUsers(newUsers); + } else { + showError(message); } + }; - const removeRecord = key => { - console.log(key); - let newDataSource = [...users]; - if (key != null) { - let idx = newDataSource.findIndex(data => data.id === key); - - if (idx > -1) { - newDataSource.splice(idx, 1); - setUsers(newDataSource); - } - } - }; - - const loadUsers = async (startIdx) => { - const res = await API.get(`/api/user/?p=${startIdx}`); - const {success, message, data} = res.data; - if (success) { - if (startIdx === 0) { - setUsers(data); - setCount(data); - } else { - let newUsers = users; - newUsers.push(...data); - setUsers(newUsers); - setCount(newUsers); - } - } else { - showError(message); - } - setLoading(false); - }; - - const onPaginationChange = (e, {activePage}) => { - (async () => { - if (activePage === Math.ceil(users.length / ITEMS_PER_PAGE) + 1) { - // In this case we have to load more data and then append them. - await loadUsers(activePage - 1); - } - setActivePage(activePage); - })(); - }; - - useEffect(() => { - loadUsers(0) - .then() - .catch((reason) => { - showError(reason); - }); - }, []); - - const manageUser = async (username, action, record) => { - const res = await API.post('/api/user/manage', { - username, action - }); - const {success, message} = res.data; - if (success) { - showSuccess('操作成功完成!'); - let user = res.data.data; - let newUsers = [...users]; - if (action === 'delete') { - - } else { - record.status = user.status; - record.role = user.role; - } - setUsers(newUsers); - } else { - showError(message); - } - }; - - const renderStatus = (status) => { - switch (status) { - case 1: - return 已激活; - case 2: - return ( - 已封禁 - ); - default: - return ( - 未知状态 - ); - } - }; - - const searchUsers = async () => { - if (searchKeyword === '') { - // if keyword is blank, load files instead. - await loadUsers(0); - setActivePage(1); - return; - } - setSearching(true); - const res = await API.get(`/api/user/search?keyword=${searchKeyword}`); - const {success, message, data} = res.data; - if (success) { - setUsers(data); - setActivePage(1); - } else { - showError(message); - } - setSearching(false); - }; - - const handleKeywordChange = async (value) => { - setSearchKeyword(value.trim()); - }; - - const sortUser = (key) => { - if (users.length === 0) return; - setLoading(true); - let sortedUsers = [...users]; - sortedUsers.sort((a, b) => { - return ('' + a[key]).localeCompare(b[key]); - }); - if (sortedUsers[0].id === users[0].id) { - sortedUsers.reverse(); - } - setUsers(sortedUsers); - setLoading(false); - }; - - 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 => { - }); - } - }; - - const pageData = users.slice((activePage - 1) * ITEMS_PER_PAGE, activePage * ITEMS_PER_PAGE); - - const closeAddUser = () => { - setShowAddUser(false); + const renderStatus = (status) => { + switch (status) { + case 1: + return 已激活; + case 2: + return ( + 已封禁 + ); + default: + return ( + 未知状态 + ); } + }; - const closeEditUser = () => { - setShowEditUser(false); - setEditingUser({ - id: undefined, - }); + const searchUsers = async () => { + if (searchKeyword === '') { + // if keyword is blank, load files instead. + await loadUsers(0); + setActivePage(1); + return; } + setSearching(true); + const res = await API.get(`/api/user/search?keyword=${searchKeyword}`); + const { success, message, data } = res.data; + if (success) { + setUsers(data); + setActivePage(1); + } else { + showError(message); + } + setSearching(false); + }; - const refresh = async () => { - if (searchKeyword === '') { - await loadUsers(activePage - 1); - } else { - await searchUsers(); + const handleKeywordChange = async (value) => { + setSearchKeyword(value.trim()); + }; + + const sortUser = (key) => { + if (users.length === 0) return; + setLoading(true); + let sortedUsers = [...users]; + sortedUsers.sort((a, b) => { + return ('' + a[key]).localeCompare(b[key]); + }); + if (sortedUsers[0].id === users[0].id) { + sortedUsers.reverse(); + } + setUsers(sortedUsers); + setLoading(false); + }; + + 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 => { + }); + } + }; + + const pageData = users.slice((activePage - 1) * ITEMS_PER_PAGE, activePage * ITEMS_PER_PAGE); + + const closeAddUser = () => { + setShowAddUser(false); + }; + + const closeEditUser = () => { + setShowEditUser(false); + setEditingUser({ + id: undefined + }); + }; + + const refresh = async () => { + if (searchKeyword === '') { + await loadUsers(activePage - 1); + } else { + await searchUsers(); + } + }; + + return ( + <> + + +
+ handleKeywordChange(value)} + /> + + + +
- - - ); + }>添加用户 + + ); }; export default UsersTable; diff --git a/web/src/components/WeChatIcon.js b/web/src/components/WeChatIcon.js index 8d7f6dd..22210d9 100644 --- a/web/src/components/WeChatIcon.js +++ b/web/src/components/WeChatIcon.js @@ -3,14 +3,14 @@ import { Icon } from '@douyinfe/semi-ui'; const WeChatIcon = () => { function CustomIcon() { - return + return + 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"> + 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"> ; } diff --git a/web/src/pages/Redemption/EditRedemption.js b/web/src/pages/Redemption/EditRedemption.js index 54aa47d..be62fb2 100644 --- a/web/src/pages/Redemption/EditRedemption.js +++ b/web/src/pages/Redemption/EditRedemption.js @@ -1,17 +1,17 @@ import React, { useEffect, useState } from 'react'; -import { useParams, useNavigate } from 'react-router-dom'; -import {API, downloadTextAsFile, isMobile, showError, showSuccess} from '../../helpers'; -import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render'; -import {SideSheet, Space, Spin, Button, Input, Typography, AutoComplete, Modal} from "@douyinfe/semi-ui"; -import Title from "@douyinfe/semi-ui/lib/es/typography/title"; -import {Divider} from "semantic-ui-react"; +import { useNavigate, useParams } from 'react-router-dom'; +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 Title from '@douyinfe/semi-ui/lib/es/typography/title'; +import { Divider } from 'semantic-ui-react'; const EditRedemption = (props) => { const isEdit = props.editingRedemption.id !== undefined; const [loading, setLoading] = useState(isEdit); const params = useParams(); - const navigate = useNavigate() + const navigate = useNavigate(); const originInputs = { name: '', quota: 100000, @@ -22,8 +22,8 @@ const EditRedemption = (props) => { const handleCancel = () => { props.handleClose(); - } - + }; + const handleInputChange = (name, value) => { setInputs((inputs) => ({ ...inputs, [name]: value })); }; @@ -43,9 +43,9 @@ const EditRedemption = (props) => { useEffect(() => { if (isEdit) { loadRedemption().then( - () => { - // console.log(inputs); - } + () => { + // console.log(inputs); + } ); } else { setInputs(originInputs); @@ -82,21 +82,21 @@ const EditRedemption = (props) => { showError(message); } if (!isEdit && data) { - let text = ""; + let text = ''; for (let i = 0; i < data.length; i++) { - text += data[i] + "\n"; + text += data[i] + '\n'; } // downloadTextAsFile(text, `${inputs.name}.txt`); Modal.confirm({ title: '兑换码创建成功', content: ( -
-

兑换码创建成功,是否下载兑换码?

-

兑换码将以文本文件的形式下载,文件名为兑换码的名称。

-
+
+

兑换码创建成功,是否下载兑换码?

+

兑换码将以文本文件的形式下载,文件名为兑换码的名称。

+
), onOk: () => { - downloadTextAsFile(text, `${inputs.name}.txt`); + downloadTextAsFile(text, `${inputs.name}.txt`); } }); } @@ -106,71 +106,71 @@ const EditRedemption = (props) => { return ( <> {isEdit ? '更新兑换码信息' : '创建新的兑换码'}} - headerStyle={{borderBottom: '1px solid var(--semi-color-border)'}} - bodyStyle={{borderBottom: '1px solid var(--semi-color-border)'}} - visible={props.visiable} - footer={ -
- - - - -
- } - closeIcon={null} - onCancel={() => handleCancel()} - width={isMobile() ? '100%' : 600} + placement={isEdit ? 'right' : 'left'} + title={{isEdit ? '更新兑换码信息' : '创建新的兑换码'}} + headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }} + bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }} + visible={props.visiable} + footer={ +
+ + + + +
+ } + closeIcon={null} + onCancel={() => handleCancel()} + width={isMobile() ? '100%' : 600} > handleInputChange('name', value)} - value={name} - autoComplete='new-password' - required={!isEdit} + style={{ marginTop: 20 }} + label="名称" + name="name" + placeholder={'请输入名称'} + onChange={value => handleInputChange('name', value)} + value={name} + autoComplete="new-password" + required={!isEdit} /> - +
{`额度${renderQuotaWithPrompt(quota)}`}
handleInputChange('quota', value)} - value={quota} - autoComplete='new-password' - type='number' - position={'bottom'} - data={[ - {value: 500000, label: '1$'}, - {value: 5000000, label: '10$'}, - {value: 25000000, label: '50$'}, - {value: 50000000, label: '100$'}, - {value: 250000000, label: '500$'}, - {value: 500000000, label: '1000$'}, - ]} + style={{ marginTop: 8 }} + name="quota" + placeholder={'请输入额度'} + onChange={(value) => handleInputChange('quota', value)} + value={quota} + autoComplete="new-password" + type="number" + position={'bottom'} + data={[ + { value: 500000, label: '1$' }, + { value: 5000000, label: '10$' }, + { value: 25000000, label: '50$' }, + { value: 50000000, label: '100$' }, + { value: 250000000, label: '500$' }, + { value: 500000000, label: '1000$' } + ]} /> { - !isEdit && <> - - 生成数量 - handleInputChange('count', value)} - value={count} - autoComplete='new-password' - type='number' - /> - + !isEdit && <> + + 生成数量 + handleInputChange('count', value)} + value={count} + autoComplete="new-password" + type="number" + /> + }
diff --git a/web/src/pages/Token/EditToken.js b/web/src/pages/Token/EditToken.js index d7fd909..0fc9c6b 100644 --- a/web/src/pages/Token/EditToken.js +++ b/web/src/pages/Token/EditToken.js @@ -1,352 +1,351 @@ -import React, {useEffect, useRef, useState} from 'react'; -import {useParams, useNavigate} from 'react-router-dom'; -import {API, isMobile, showError, showSuccess, timestamp2string} from '../../helpers'; -import {renderQuota, renderQuotaWithPrompt} from '../../helpers/render'; +import React, { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { API, isMobile, showError, showSuccess, timestamp2string } from '../../helpers'; +import { renderQuotaWithPrompt } from '../../helpers/render'; import { - Layout, - SideSheet, + AutoComplete, + Banner, Button, + Checkbox, + DatePicker, + Input, + Select, + SideSheet, Space, Spin, - Banner, - Input, - DatePicker, - AutoComplete, - Typography, - Checkbox, Select -} from "@douyinfe/semi-ui"; -import Title from "@douyinfe/semi-ui/lib/es/typography/title"; -import {Divider} from "semantic-ui-react"; + Typography +} from '@douyinfe/semi-ui'; +import Title from '@douyinfe/semi-ui/lib/es/typography/title'; +import { Divider } from 'semantic-ui-react'; const EditToken = (props) => { - const [isEdit, setIsEdit] = useState(false); - const [loading, setLoading] = useState(isEdit); - const originInputs = { - name: '', - remain_quota: isEdit ? 0 : 500000, - expired_time: -1, - unlimited_quota: false, - model_limits_enabled: false, - model_limits: [], - }; - const [inputs, setInputs] = useState(originInputs); - 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(); - const handleInputChange = (name, value) => { - setInputs((inputs) => ({...inputs, [name]: value})); - }; - const handleCancel = () => { + const [isEdit, setIsEdit] = useState(false); + const [loading, setLoading] = useState(isEdit); + const originInputs = { + name: '', + remain_quota: isEdit ? 0 : 500000, + expired_time: -1, + unlimited_quota: false, + model_limits_enabled: false, + model_limits: [] + }; + const [inputs, setInputs] = useState(originInputs); + 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(); + const handleInputChange = (name, value) => { + setInputs((inputs) => ({ ...inputs, [name]: value })); + }; + const handleCancel = () => { + props.handleClose(); + }; + const setExpiredTime = (month, day, hour, minute) => { + let now = new Date(); + let timestamp = now.getTime() / 1000; + let seconds = month * 30 * 24 * 60 * 60; + seconds += day * 24 * 60 * 60; + seconds += hour * 60 * 60; + seconds += minute * 60; + if (seconds !== 0) { + timestamp += seconds; + setInputs({ ...inputs, expired_time: timestamp2string(timestamp) }); + } else { + setInputs({ ...inputs, expired_time: -1 }); + } + }; + + const setUnlimitedQuota = () => { + setInputs({ ...inputs, unlimited_quota: !unlimited_quota }); + }; + + const loadModels = async () => { + let res = await API.get(`/api/user/models`); + const { success, message, data } = res.data; + if (success) { + let localModelOptions = data.map((model) => ({ + label: model, + value: model + })); + setModels(localModelOptions); + } else { + showError(message); + } + }; + + const loadToken = async () => { + setLoading(true); + let res = await API.get(`/api/token/${props.editingToken.id}`); + const { success, message, data } = res.data; + if (success) { + if (data.expired_time !== -1) { + data.expired_time = timestamp2string(data.expired_time); + } + if (data.model_limits !== '') { + data.model_limits = data.model_limits.split(','); + } else { + data.model_limits = []; + } + setInputs(data); + } else { + showError(message); + } + setLoading(false); + }; + useEffect(() => { + setIsEdit(props.editingToken.id !== undefined); + }, [props.editingToken.id]); + + useEffect(() => { + if (!isEdit) { + setInputs(originInputs); + } else { + loadToken().then( + () => { + // console.log(inputs); + } + ); + } + loadModels(); + }, [isEdit]); + + // 新增 state 变量 tokenCount 来记录用户想要创建的令牌数量,默认为 1 + const [tokenCount, setTokenCount] = useState(1); + + // 新增处理 tokenCount 变化的函数 + const handleTokenCountChange = (value) => { + // 确保用户输入的是正整数 + const count = parseInt(value, 10); + if (!isNaN(count) && count > 0) { + setTokenCount(count); + } + }; + + // 生成一个随机的四位字母数字字符串 + const generateRandomSuffix = () => { + const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + let result = ''; + for (let i = 0; i < 6; i++) { + result += characters.charAt(Math.floor(Math.random() * characters.length)); + } + return result; + }; + + const submit = async () => { + setLoading(true); + if (isEdit) { + // 编辑令牌的逻辑保持不变 + let localInputs = { ...inputs }; + localInputs.remain_quota = parseInt(localInputs.remain_quota); + if (localInputs.expired_time !== -1) { + let time = Date.parse(localInputs.expired_time); + if (isNaN(time)) { + showError('过期时间格式错误!'); + setLoading(false); + return; + } + 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) }); + const { success, message } = res.data; + if (success) { + showSuccess('令牌更新成功!'); + props.refresh(); props.handleClose(); - } - const setExpiredTime = (month, day, hour, minute) => { - let now = new Date(); - let timestamp = now.getTime() / 1000; - let seconds = month * 30 * 24 * 60 * 60; - seconds += day * 24 * 60 * 60; - seconds += hour * 60 * 60; - seconds += minute * 60; - if (seconds !== 0) { - timestamp += seconds; - setInputs({...inputs, expired_time: timestamp2string(timestamp)}); - } else { - setInputs({...inputs, expired_time: -1}); + } else { + showError(message); + } + } else { + // 处理新增多个令牌的情况 + let successCount = 0; // 记录成功创建的令牌数量 + for (let i = 0; i < tokenCount; i++) { + let localInputs = { ...inputs }; + if (i !== 0) { + // 如果用户想要创建多个令牌,则给每个令牌一个序号后缀 + localInputs.name = `${inputs.name}-${generateRandomSuffix()}`; } - }; + localInputs.remain_quota = parseInt(localInputs.remain_quota); - const setUnlimitedQuota = () => { - setInputs({...inputs, unlimited_quota: !unlimited_quota}); - }; + if (localInputs.expired_time !== -1) { + let time = Date.parse(localInputs.expired_time); + if (isNaN(time)) { + showError('过期时间格式错误!'); + setLoading(false); + break; + } + localInputs.expired_time = Math.ceil(time / 1000); + } + localInputs.model_limits = localInputs.model_limits.join(','); + let res = await API.post(`/api/token/`, localInputs); + const { success, message } = res.data; - const loadModels = async () => { - let res = await API.get(`/api/user/models`); - const {success, message, data} = res.data; if (success) { - let localModelOptions = data.map((model) => ({ - label: model, - value: model - })); - setModels(localModelOptions); + successCount++; } else { - showError(message); + showError(message); + break; // 如果创建失败,终止循环 } + } + + if (successCount > 0) { + showSuccess(`${successCount}个令牌创建成功,请在列表页面点击复制获取令牌!`); + props.refresh(); + props.handleClose(); + } } + setLoading(false); + setInputs(originInputs); // 重置表单 + setTokenCount(1); // 重置数量为默认值 + }; - const loadToken = async () => { - setLoading(true); - let res = await API.get(`/api/token/${props.editingToken.id}`); - const {success, message, data} = res.data; - if (success) { - if (data.expired_time !== -1) { - data.expired_time = timestamp2string(data.expired_time); - } - if (data.model_limits !== '') { - data.model_limits = data.model_limits.split(','); - } else { - data.model_limits = []; - } - setInputs(data); - } else { - showError(message); + + return ( + <> + {isEdit ? '更新令牌信息' : '创建新的令牌'}} + headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }} + bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }} + visible={props.visiable} + footer={ +
+ + + + +
} - setLoading(false); - }; - useEffect(() => { - setIsEdit(props.editingToken.id !== undefined); - }, [props.editingToken.id]); + closeIcon={null} + onCancel={() => handleCancel()} + width={isMobile() ? '100%' : 600} + > + + handleInputChange('name', value)} + value={name} + autoComplete="new-password" + required={!isEdit} + /> + + handleInputChange('expired_time', value)} + value={expired_time} + autoComplete="new-password" + type="dateTime" + /> +
+ + + + + + +
- useEffect(() => { - if (!isEdit) { - setInputs(originInputs); - } else { - loadToken().then( - () => { - // console.log(inputs); - } - ); - } - loadModels(); - }, [isEdit]); + + +
+ {`额度${renderQuotaWithPrompt(remain_quota)}`} +
+ handleInputChange('remain_quota', value)} + value={remain_quota} + autoComplete="new-password" + type="number" + // position={'top'} + data={[ + { value: 500000, label: '1$' }, + { value: 5000000, label: '10$' }, + { value: 25000000, label: '50$' }, + { value: 50000000, label: '100$' }, + { value: 250000000, label: '500$' }, + { value: 500000000, label: '1000$' } + ]} + disabled={unlimited_quota} + /> - // 新增 state 变量 tokenCount 来记录用户想要创建的令牌数量,默认为 1 - const [tokenCount, setTokenCount] = useState(1); + {!isEdit && ( + <> +
+ 新建数量 +
+ handleTokenCountChange(value)} + onSelect={(value) => handleTokenCountChange(value)} + value={tokenCount.toString()} + autoComplete="off" + type="number" + data={[ + { value: 10, label: '10个' }, + { value: 20, label: '20个' }, + { value: 30, label: '30个' }, + { value: 100, label: '100个' } + ]} + disabled={unlimited_quota} + /> + + )} - // 新增处理 tokenCount 变化的函数 - const handleTokenCountChange = (value) => { - // 确保用户输入的是正整数 - const count = parseInt(value, 10); - if (!isNaN(count) && count > 0) { - setTokenCount(count); - } - }; +
+ +
+ +
+ + handleInputChange('model_limits_enabled', e.target.checked)} + > + + 启用模型限制(非必要,不建议启用) + +
- // 生成一个随机的四位字母数字字符串 - const generateRandomSuffix = () => { - const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - let result = ''; - for (let i = 0; i < 6; i++) { - result += characters.charAt(Math.floor(Math.random() * characters.length)); - } - return result; - }; - - const submit = async () => { - setLoading(true); - if (isEdit) { - // 编辑令牌的逻辑保持不变 - let localInputs = {...inputs}; - localInputs.remain_quota = parseInt(localInputs.remain_quota); - if (localInputs.expired_time !== -1) { - let time = Date.parse(localInputs.expired_time); - if (isNaN(time)) { - showError('过期时间格式错误!'); - setLoading(false); - return; - } - 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)}); - const {success, message} = res.data; - if (success) { - showSuccess('令牌更新成功!'); - props.refresh(); - props.handleClose(); - } else { - showError(message); - } - } else { - // 处理新增多个令牌的情况 - let successCount = 0; // 记录成功创建的令牌数量 - for (let i = 0; i < tokenCount; i++) { - let localInputs = {...inputs}; - if (i !== 0) { - // 如果用户想要创建多个令牌,则给每个令牌一个序号后缀 - localInputs.name = `${inputs.name}-${generateRandomSuffix()}`; - } - localInputs.remain_quota = parseInt(localInputs.remain_quota); - - if (localInputs.expired_time !== -1) { - let time = Date.parse(localInputs.expired_time); - if (isNaN(time)) { - showError('过期时间格式错误!'); - setLoading(false); - break; - } - localInputs.expired_time = Math.ceil(time / 1000); - } - localInputs.model_limits = localInputs.model_limits.join(','); - let res = await API.post(`/api/token/`, localInputs); - const {success, message} = res.data; - - if (success) { - successCount++; - } else { - showError(message); - break; // 如果创建失败,终止循环 - } - } - - if (successCount > 0) { - showSuccess(`${successCount}个令牌创建成功,请在列表页面点击复制获取令牌!`); - props.refresh(); - props.handleClose(); - } - } - setLoading(false); - setInputs(originInputs); // 重置表单 - setTokenCount(1); // 重置数量为默认值 - }; - - - - return ( - <> - {isEdit ? '更新令牌信息' : '创建新的令牌'}} - headerStyle={{borderBottom: '1px solid var(--semi-color-border)'}} - bodyStyle={{borderBottom: '1px solid var(--semi-color-border)'}} - visible={props.visiable} - footer={ -
- - - - -
- } - closeIcon={null} - onCancel={() => handleCancel()} - width={isMobile() ? '100%' : 600} - > - - handleInputChange('name', value)} - value={name} - autoComplete='new-password' - required={!isEdit} - /> - - handleInputChange('expired_time', value)} - value={expired_time} - autoComplete='new-password' - type='dateTime' - /> -
- - - - - - -
- - - -
- {`额度${renderQuotaWithPrompt(remain_quota)}`} -
- handleInputChange('remain_quota', value)} - value={remain_quota} - autoComplete='new-password' - type='number' - // position={'top'} - data={[ - {value: 500000, label: '1$'}, - {value: 5000000, label: '10$'}, - {value: 25000000, label: '50$'}, - {value: 50000000, label: '100$'}, - {value: 250000000, label: '500$'}, - {value: 500000000, label: '1000$'}, - ]} - disabled={unlimited_quota} - /> - - {!isEdit && ( - <> -
- 新建数量 -
- handleTokenCountChange(value)} - onSelect={(value) => handleTokenCountChange(value)} - value={tokenCount.toString()} - autoComplete='off' - type='number' - data={[ - { value: 10, label: '10个' }, - { value: 20, label: '20个' }, - { value: 30, label: '30个' }, - { value: 100, label: '100个' }, - ]} - disabled={unlimited_quota} - /> - - )} - -
- -
- -
- - handleInputChange('model_limits_enabled', e.target.checked)} - > - - 启用模型限制(非必要,不建议启用) - -
- - { + handleInputChange('model_limits', value); + }} + value={inputs.model_limits} + autoComplete="new-password" + optionList={models} + disabled={!model_limits_enabled} + /> +
+
+ + ); }; export default EditToken; diff --git a/web/src/pages/User/AddUser.js b/web/src/pages/User/AddUser.js index c69f9bf..7ebdd33 100644 --- a/web/src/pages/User/AddUser.js +++ b/web/src/pages/User/AddUser.js @@ -1,98 +1,98 @@ -import React, {useState} from 'react'; -import {API, isMobile, showError, showSuccess} from '../../helpers'; -import Title from "@douyinfe/semi-ui/lib/es/typography/title"; -import {Button, SideSheet, Space, Input, Spin} from "@douyinfe/semi-ui"; +import React, { useState } from 'react'; +import { API, isMobile, showError, showSuccess } from '../../helpers'; +import Title from '@douyinfe/semi-ui/lib/es/typography/title'; +import { Button, Input, SideSheet, Space, Spin } from '@douyinfe/semi-ui'; const AddUser = (props) => { - const originInputs = { - username: '', - display_name: '', - password: '', - }; - const [inputs, setInputs] = useState(originInputs); - const [loading, setLoading] = useState(false); - const {username, display_name, password} = inputs; + const originInputs = { + username: '', + display_name: '', + password: '' + }; + const [inputs, setInputs] = useState(originInputs); + const [loading, setLoading] = useState(false); + const { username, display_name, password } = inputs; - const handleInputChange = (name, value) => { - setInputs((inputs) => ({...inputs, [name]: value})); - }; + const handleInputChange = (name, value) => { + setInputs((inputs) => ({ ...inputs, [name]: value })); + }; - const submit = async () => { - setLoading(true); - if (inputs.username === '' || inputs.password === '') return; - const res = await API.post(`/api/user/`, inputs); - const {success, message} = res.data; - if (success) { - showSuccess('用户账户创建成功!'); - setInputs(originInputs); - props.refresh(); - props.handleClose(); - } else { - showError(message); - } - setLoading(false); - }; - - const handleCancel = () => { - props.handleClose(); + const submit = async () => { + setLoading(true); + if (inputs.username === '' || inputs.password === '') return; + const res = await API.post(`/api/user/`, inputs); + const { success, message } = res.data; + if (success) { + showSuccess('用户账户创建成功!'); + setInputs(originInputs); + props.refresh(); + props.handleClose(); + } else { + showError(message); } + setLoading(false); + }; - return ( - <> - {'添加用户'}} - headerStyle={{borderBottom: '1px solid var(--semi-color-border)'}} - bodyStyle={{borderBottom: '1px solid var(--semi-color-border)'}} - visible={props.visible} - footer={ -
- - - - -
- } - closeIcon={null} - onCancel={() => handleCancel()} - width={isMobile() ? '100%' : 600} - > - - handleInputChange('username', value)} - value={username} - autoComplete="off" - /> - handleInputChange('display_name', value)} - value={display_name} - /> - handleInputChange('password', value)} - value={password} - autoComplete="off" - /> - -
- - ); + const handleCancel = () => { + props.handleClose(); + }; + + return ( + <> + {'添加用户'}} + headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }} + bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }} + visible={props.visible} + footer={ +
+ + + + +
+ } + closeIcon={null} + onCancel={() => handleCancel()} + width={isMobile() ? '100%' : 600} + > + + handleInputChange('username', value)} + value={username} + autoComplete="off" + /> + handleInputChange('display_name', value)} + value={display_name} + /> + handleInputChange('password', value)} + value={password} + autoComplete="off" + /> + +
+ + ); }; export default AddUser; diff --git a/web/src/pages/User/EditUser.js b/web/src/pages/User/EditUser.js index 08b1ede..17e2416 100644 --- a/web/src/pages/User/EditUser.js +++ b/web/src/pages/User/EditUser.js @@ -1,9 +1,9 @@ import React, { useEffect, useState } from 'react'; -import { useParams, useNavigate } from 'react-router-dom'; +import { useNavigate } from 'react-router-dom'; import { API, isMobile, showError, showSuccess } from '../../helpers'; -import { renderQuota, renderQuotaWithPrompt } from '../../helpers/render'; -import Title from "@douyinfe/semi-ui/lib/es/typography/title"; -import { SideSheet, Space, Button, Spin, Input, Typography, Select, Divider } from "@douyinfe/semi-ui"; +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'; const EditUser = (props) => { const userId = props.editingUser.id; @@ -30,7 +30,7 @@ const EditUser = (props) => { let res = await API.get(`/api/group/`); setGroupOptions(res.data.data.map((group) => ({ label: group, - value: group, + value: group }))); } catch (error) { showError(error.message); @@ -39,7 +39,7 @@ const EditUser = (props) => { const navigate = useNavigate(); const handleCancel = () => { props.handleClose(); - } + }; const loadUser = async () => { setLoading(true); let res = undefined; @@ -99,8 +99,8 @@ const EditUser = (props) => { footer={
- - + +
} @@ -113,35 +113,35 @@ const EditUser = (props) => { 用户名 handleInputChange('username', value)} value={username} - autoComplete='new-password' + autoComplete="new-password" />
密码
handleInputChange('password', value)} value={password} - autoComplete='new-password' + autoComplete="new-password" />
显示名称
handleInputChange('display_name', value)} value={display_name} - autoComplete='new-password' + autoComplete="new-password" /> { userId && <> @@ -150,7 +150,7 @@ const EditUser = (props) => { handleInputChange('quota', value)} value={quota} type={'number'} - autoComplete='new-password' + autoComplete="new-password" /> } @@ -179,10 +179,10 @@ const EditUser = (props) => { 已绑定的 GitHub 账户
@@ -199,30 +199,30 @@ const EditUser = (props) => { 已绑定的微信账户
已绑定的 Telegram 账户
已绑定的邮箱账户