Merge branch 'dev'

This commit is contained in:
RockYang 2024-04-07 08:03:14 +08:00
commit c383367e9f
51 changed files with 2081 additions and 564 deletions

View File

@ -1,20 +1,30 @@
# 更新日志
## 4.0.2
* 功能新增:支持前端菜单可以配置
* 功能优化:手机端支持免登录预览功能
* 功能优化:在登录和注册界面标题显示软件版本号
* 功能优化MJ 绘画支持 --sref 和 --cref 图片一致性参数
* 功能优化:使用 leveldb 解决 SD 绘图进度图片预览问题
* Bug修复解决因为图片上传使用相对路径而导致融图失败的问题。
* 功能新增:手机端支持 Stable-Diffusion 绘画
* 功能新增:管理后台登录页面增加行为验证码,防止爆破
## v4.0.1
* 功能重构:重构 Stable-Diffusion 绘画实现,使用 SDAPI 替换之前的 websocket 接口SDAPI 兼容各种 stable-diffusion 发行版,稳定性更强一些
* 功能优化:使用 [midjouney-proxy](https://github.com/novicezk/midjourney-proxy) 项目替换内置的原生 MidJourney API兼容 MJ-Plus 中转
* 功能重构:重构 Stable-Diffusion 绘画实现,使用 SDAPI 替换之前的 websocket 接口SDAPI 兼容各种 stable-diffusion
发行版,稳定性更强一些
* 功能优化:使用 [midjouney-proxy](https://github.com/novicezk/midjourney-proxy) 项目替换内置的原生 MidJourney API兼容
MJ-Plus 中转
* 功能新增:用户算力消费日志增加统计功能,统计一段时间内用户消费的算力
* Bug修复修复 iphone 手机无法通过图形验证码的Bug使用滑动验证码替换
* Bug修复修复手机端 MidJourney 绘画页面滚动条无法滚动的Bug
## v4.0.0
非兼容版本重大重构引入算力概念将系统中所有的能力AI对话MJ绘画SD绘画DALL绘画全部使用算力来兑换。
只要你的算力值余额不为0你就可以进行任何操作。比如一次 GPT3.5 对话消耗1个单位算力一次 GPT4 对话消耗10个算力。一次 MJ 对话消耗15个算力...
只要你的算力值余额不为0你就可以进行任何操作。比如一次 GPT3.5 对话消耗1个单位算力一次 GPT4 对话消耗10个算力。一次 MJ
对话消耗15个算力...
* 功能重构:重构整体系统,全部采用算力来进行结算
* 功能优化SD 绘画页面采用 websocket 替换 http 轮询机制,节省带宽
@ -29,6 +39,7 @@
* 功能新增管理后台新增7日内新增用户和新增订单统计
## v3.2.7
* 功能重构:采用 Vant 重构移动页面,新增 MidJourney 功能
* 功能优化:优化 PC 端 MidJourney 页面布局,新增融图和换脸功能
* Bug修复修复 issue [
@ -43,6 +54,7 @@
* 功能新增:后台管理新怎对话查看和检索功能
## v3.2.6
* 功能优化:恢复关闭注册系统配置项,管理员可以在后台关闭用户注册,只允许内部添加账号
* 功能优化:兼用旧版本微信收款消息解析
* 功能优化:优化订单扫码支付状态轮询功能,当关闭二维码时取消轮询,节约网络资源
@ -56,16 +68,18 @@
* 功能优化:给所有的 websocket 连接加上心跳,解决 "close 1006 (abnormal closure): unexpected EOF" Bug
* 功能新增:新增短信宝短信平台发送平台集成
## v3.2.5
* 功能新增:**重磅更新!!!** 新增 MidJourney-Plus API 支持,一秒配置,开箱即用,高效稳定。
* 功能新增:**重磅更新!!!** 新增 GPT4-ALL 和 GPTs 模型支持,你只需花几块钱,可以丝滑享受 ChatGPT-Plus 会员的所有功能,无需再订阅 Plus 账号了!!!
* 功能新增:**重磅更新!!!** 新增 GPT4-ALL 和 GPTs 模型支持,你只需花几块钱,可以丝滑享受 ChatGPT-Plus 会员的所有功能,无需再订阅
Plus 账号了!!!
* 功能优化:增强 markdown 图片和引用块解析。
* 功能新增:新增用户文件管理,目前一支持上传文件跟 GPT 进行多态对话。
* 功能优化function call 兼用中转 API。
* Bug修复修复部分已知的 Bug。
## v3.2.4.1
* 功能新增:新增 PayJs 支付通道
* Bug修复紧急修复后台添加用户失败问题
* Bug修复紧急修复使用中转 API-KEY 无法绘图的问题

View File

@ -91,7 +91,7 @@ KEY。
![](https://ai.r9it.com/docs/images/env/admin_api_keys.png)
另外,如果您目前还没有 OpenAI 的 API KEY的推荐您去 https://gpt.bemore.lol 购买,**无需魔法,高速稳定,且价格还远低于 OpenAI
另外,如果您目前还没有 OpenAI 的 API KEY的推荐您去 https://api.chat-plus.net 购买,**无需魔法,高速稳定,且价格还远低于 OpenAI
官方**。
## 使用须知

View File

@ -25,6 +25,8 @@ type MjTask struct {
Type TaskType `json:"type"`
UserId int `json:"user_id"`
Prompt string `json:"prompt,omitempty"`
NegPrompt string `json:"neg_prompt,omitempty"`
Params string `json:"full_prompt"`
Index int `json:"index,omitempty"`
MessageId string `json:"message_id,omitempty"`
MessageHash string `json:"message_hash,omitempty"`
@ -43,7 +45,7 @@ type SdTask struct {
type SdTaskParams struct {
TaskId string `json:"task_id"`
Prompt string `json:"prompt"` // 提示词
NegativePrompt string `json:"negative_prompt"` // 反向提示词
NegPrompt string `json:"neg_prompt"` // 反向提示词
Steps int `json:"steps"` // 迭代步数默认20
Sampler string `json:"sampler"` // 采样器
FaceFix bool `json:"face_fix"` // 面部修复

View File

@ -28,10 +28,12 @@ require github.com/xxl-job/xxl-job-executor-go v1.2.0
require (
github.com/mojocn/base64Captcha v1.3.1
github.com/shopspring/decimal v1.3.1
github.com/syndtr/goleveldb v1.0.0
)
require (
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect
golang.org/x/image v0.0.0-20190501045829-6d32002ffd75 // indirect
)

View File

@ -27,6 +27,7 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/eatmoreapple/openwechat v1.2.1 h1:ez4oqF/Y2NSEX/DbPV8lvj7JlfkYqvieeo4awx5lzfU=
github.com/eatmoreapple/openwechat v1.2.1/go.mod h1:61HOzTyvLobGdgWhL68jfGNwTJEv0mhQ1miCXQrvWU8=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
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=
@ -67,8 +68,11 @@ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF0
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pOkfl+p/TAqKOfFu+7KPlMVpok/w=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@ -83,6 +87,7 @@ github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/imroc/req/v3 v3.37.2 h1:vEemuA0cq9zJ6lhe+mSRhsZm951bT0CdiSH47+KTn6I=
github.com/imroc/req/v3 v3.37.2/go.mod h1:DECzjVIrj6jcUr5n6e+z0ygmCO93rx4Jy0RjOEe1YCI=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
@ -133,9 +138,12 @@ github.com/mojocn/base64Captcha v1.3.1/go.mod h1:wAQCKEc5bDujxKRmbT6/vTnTt5CjStQ
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo/v2 v2.10.0 h1:sfUl4qgLdvkChZrWCYndY2EAu9BRIw1YphNAzy1VNWs=
github.com/onsi/ginkgo/v2 v2.10.0/go.mod h1:UDQOh5wbQUlMnkLfVaIUMtQ1Vus92oM+P2JX1aulgcE=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.27.7 h1:fVih9JD6ogIiHUN6ePK7HJidyEDpWGVB5mzM7cWNXoU=
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b h1:FfH+VrHHk6Lxt9HdVS0PXzSXFyS2NbZKXv33FYPol0A=
github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b/go.mod h1:AC62GU6hc0BrNm+9RK9VSiwa/EUe1bkIeFORAMcHvJU=
@ -193,6 +201,8 @@ github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/uber/jaeger-client-go v2.30.0+incompatible h1:D6wyKGCecFaSRUpo8lCVbaOOb6ThwMmTEbhRwtKR97o=
@ -235,6 +245,7 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
@ -243,11 +254,13 @@ golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -294,12 +307,15 @@ gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@ -478,7 +478,7 @@ func (h *ChatHandler) doRequest(ctx context.Context, req types.ApiRequest, platf
request = request.WithContext(ctx)
request.Header.Set("Content-Type", "application/json")
var proxyURL string
if apiKey.ProxyURL != "" { // 使用代理
if len(apiKey.ProxyURL) > 5 { // 使用代理
proxy, _ := url.Parse(apiKey.ProxyURL)
client = &http.Client{
Transport: &http.Transport{

View File

@ -220,7 +220,7 @@ func (h *FunctionHandler) Dall3(c *gin.Context) {
var res imgRes
var errRes ErrRes
var request *req.Request
if apiKey.ProxyURL != "" {
if len(apiKey.ProxyURL) > 5 {
request = req.C().SetProxyURL(apiKey.ProxyURL).R()
} else {
request = req.C().R()

View File

@ -98,7 +98,10 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
ImgArr []string `json:"img_arr"`
Tile bool `json:"tile"`
Quality float32 `json:"quality"`
Weight float32 `json:"weight"`
Iw float32 `json:"iw"`
CRef string `json:"cref"` //生成角色一致的图像
SRef string `json:"sref"` //生成风格一致的图像
Cw int `json:"cw"` // 参考程度
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
@ -108,41 +111,57 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
return
}
var prompt = data.Prompt
if data.Rate != "" && !strings.Contains(prompt, "--ar") {
prompt += " --ar " + data.Rate
var params = ""
if data.Rate != "" && !strings.Contains(params, "--ar") {
params += " --ar " + data.Rate
}
if data.Seed > 0 && !strings.Contains(prompt, "--seed") {
prompt += fmt.Sprintf(" --seed %d", data.Seed)
if data.Seed > 0 && !strings.Contains(params, "--seed") {
params += fmt.Sprintf(" --seed %d", data.Seed)
}
if data.Stylize > 0 && !strings.Contains(prompt, "--s") && !strings.Contains(prompt, "--stylize") {
prompt += fmt.Sprintf(" --s %d", data.Stylize)
if data.Stylize > 0 && !strings.Contains(params, "--s") && !strings.Contains(params, "--stylize") {
params += fmt.Sprintf(" --s %d", data.Stylize)
}
if data.Chaos > 0 && !strings.Contains(prompt, "--c") && !strings.Contains(prompt, "--chaos") {
prompt += fmt.Sprintf(" --c %d", data.Chaos)
if data.Chaos > 0 && !strings.Contains(params, "--c") && !strings.Contains(params, "--chaos") {
params += fmt.Sprintf(" --c %d", data.Chaos)
}
if data.Weight > 0 {
prompt += fmt.Sprintf(" --iw %f", data.Weight)
if len(data.ImgArr) > 0 && data.Iw > 0 {
params += fmt.Sprintf(" --iw %f", data.Iw)
}
if data.Raw {
prompt += " --style raw"
params += " --style raw"
}
if data.Quality > 0 {
prompt += fmt.Sprintf(" --q %.2f", data.Quality)
}
if data.NegPrompt != "" {
prompt += fmt.Sprintf(" --no %s", data.NegPrompt)
params += fmt.Sprintf(" --q %.2f", data.Quality)
}
if data.Tile {
prompt += " --tile "
params += " --tile "
}
if data.Model != "" && !strings.Contains(prompt, "--v") && !strings.Contains(prompt, "--niji") {
prompt += fmt.Sprintf(" %s", data.Model)
if data.CRef != "" {
params += fmt.Sprintf(" --cref %s", data.CRef)
if data.Cw > 0 {
params += fmt.Sprintf(" --cw %d", data.Cw)
} else {
params += " --cw 100"
}
}
if data.SRef != "" {
params += fmt.Sprintf(" --sref %s", data.CRef)
}
if data.Model != "" && !strings.Contains(params, "--v") && !strings.Contains(params, "--niji") {
params += fmt.Sprintf(" %s", data.Model)
}
// 处理融图和换脸的提示词
if data.TaskType == types.TaskSwapFace.String() || data.TaskType == types.TaskBlend.String() {
prompt = fmt.Sprintf("%s:%s", data.TaskType, strings.Join(data.ImgArr, ","))
params = fmt.Sprintf("%s:%s", data.TaskType, strings.Join(data.ImgArr, ","))
}
// 如果本地图片上传的是相对地址,处理成绝对地址
for k, v := range data.ImgArr {
if !strings.HasPrefix(v, "http") {
data.ImgArr[k] = fmt.Sprintf("http://localhost:5678/%s", strings.TrimLeft(v, "/"))
}
}
idValue, _ := c.Get(types.LoginUserID)
@ -158,7 +177,7 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
UserId: userId,
TaskId: taskId,
Progress: 0,
Prompt: prompt,
Prompt: fmt.Sprintf("%s %s", data.Prompt, params),
Power: h.App.SysConfig.MjPower,
CreatedAt: time.Now(),
}
@ -181,7 +200,9 @@ func (h *MidJourneyHandler) Image(c *gin.Context) {
TaskId: taskId,
SessionId: data.SessionId,
Type: types.TaskType(data.TaskType),
Prompt: prompt,
Prompt: data.Prompt,
NegPrompt: data.NegPrompt,
Params: params,
UserId: userId,
ImgArr: data.ImgArr,
})

View File

@ -424,29 +424,23 @@ func (h *PaymentHandler) notify(orderNo string, tradeNo string) error {
var opt string
var power int
if user.Vip { // 已经是 VIP 用户
if remark.Days > 0 { // 只延期 VIP不增加调用次数
if remark.Days > 0 { // VIP 充值
if user.ExpiredTime >= time.Now().Unix() {
user.ExpiredTime = time.Unix(user.ExpiredTime, 0).AddDate(0, 0, remark.Days).Unix()
opt = "VIP充值VIP 没到期,只延期不增加算力"
} else {
user.ExpiredTime = time.Now().AddDate(0, 0, remark.Days).Unix()
user.Power += h.App.SysConfig.VipMonthPower
power = h.App.SysConfig.VipMonthPower
opt = "VIP充值"
}
user.Vip = true
} else { // 充值点卡,直接增加次数即可
user.Power += remark.Power
opt = "点卡充值"
power = remark.Power
}
} else { // 非 VIP 用户
if remark.Days > 0 { // vip 套餐days > 0, power == 0
user.ExpiredTime = time.Now().AddDate(0, 0, remark.Days).Unix()
user.Power += h.App.SysConfig.VipMonthPower
user.Vip = true
opt = "VIP充值"
power = h.App.SysConfig.VipMonthPower
} else { //点卡days == 0, calls > 0
user.Power += remark.Power
opt = "点卡充值"
power = remark.Power
}
}
// 更新用户信息
res = h.DB.Updates(&user)
if res.Error != nil {

View File

@ -6,11 +6,11 @@ import (
"chatplus/service"
"chatplus/service/oss"
"chatplus/service/sd"
"chatplus/store"
"chatplus/store/model"
"chatplus/store/vo"
"chatplus/utils"
"chatplus/utils/resp"
"encoding/base64"
"fmt"
"net/http"
"time"
@ -28,13 +28,15 @@ type SdJobHandler struct {
pool *sd.ServicePool
uploader *oss.UploaderManager
snowflake *service.Snowflake
leveldb *store.LevelDB
}
func NewSdJobHandler(app *core.AppServer, db *gorm.DB, pool *sd.ServicePool, manager *oss.UploaderManager, snowflake *service.Snowflake) *SdJobHandler {
func NewSdJobHandler(app *core.AppServer, db *gorm.DB, pool *sd.ServicePool, manager *oss.UploaderManager, snowflake *service.Snowflake, levelDB *store.LevelDB) *SdJobHandler {
return &SdJobHandler{
pool: pool,
uploader: manager,
snowflake: snowflake,
leveldb: levelDB,
BaseHandler: BaseHandler{
App: app,
DB: db,
@ -127,7 +129,7 @@ func (h *SdJobHandler) Image(c *gin.Context) {
params := types.SdTaskParams{
TaskId: taskId,
Prompt: data.Prompt,
NegativePrompt: data.NegativePrompt,
NegPrompt: data.NegPrompt,
Steps: data.Steps,
Sampler: data.Sampler,
FaceFix: data.FaceFix,
@ -257,10 +259,10 @@ func (h *SdJobHandler) getData(finish bool, userId uint, page int, pageSize int,
}
if item.Progress < 100 {
// 正在运行中任务使用代理访问图片
image, err := utils.DownloadImage(item.ImgURL, "")
// 从 leveldb 中获取图片预览数据
imageData, err := h.leveldb.Get(item.TaskId)
if err == nil {
job.ImgURL = "data:image/png;base64," + base64.StdEncoding.EncodeToString(image)
job.ImgURL = "data:image/png;base64," + string(imageData)
}
}
jobs = append(jobs, job)

View File

@ -53,6 +53,10 @@ func (l *AppLifecycle) OnStop(context.Context) error {
return nil
}
func NewAppLifeCycle() *AppLifecycle {
return &AppLifecycle{}
}
func main() {
configFile := os.Getenv("CONFIG_FILE")
if configFile == "" {
@ -92,6 +96,7 @@ func main() {
fx.Provide(store.NewGormConfig),
fx.Provide(store.NewMysql),
fx.Provide(store.NewRedisClient),
fx.Provide(store.NewLevelDB),
fx.Provide(func() embed.FS {
return xdbFS
@ -292,7 +297,7 @@ func main() {
group.POST("save", h.Save)
group.GET("list", h.List)
group.POST("set", h.Set)
group.POST("remove", h.Remove)
group.GET("remove", h.Remove)
}),
fx.Invoke(func(s *core.AppServer, h *admin.UserHandler) {
group := s.Engine.Group("/api/admin/user/")
@ -432,11 +437,14 @@ func main() {
group.GET("list", h.List)
}),
fx.Invoke(func(s *core.AppServer, db *gorm.DB) {
go func() {
err := s.Run(db)
if err != nil {
log.Fatal(err)
}
}()
}),
fx.Provide(NewAppLifeCycle),
// 注册生命周期回调函数
fx.Invoke(func(lifecycle fx.Lifecycle, lc *AppLifecycle) {
lifecycle.Append(fx.Hook{

View File

@ -8,6 +8,7 @@ import (
"fmt"
"github.com/imroc/req/v3"
"io"
"time"
"github.com/gin-gonic/gin"
)
@ -16,17 +17,26 @@ import (
type PlusClient struct {
Config types.MjPlusConfig
apiURL string
client *req.Client
}
func NewPlusClient(config types.MjPlusConfig) *PlusClient {
return &PlusClient{Config: config, apiURL: config.ApiURL}
return &PlusClient{
Config: config,
apiURL: config.ApiURL,
client: req.C().SetTimeout(time.Minute).SetUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"),
}
}
func (c *PlusClient) Imagine(task types.MjTask) (ImageRes, error) {
apiURL := fmt.Sprintf("%s/mj-%s/mj/submit/imagine", c.apiURL, c.Config.Mode)
prompt := fmt.Sprintf("%s %s", task.Prompt, task.Params)
if task.NegPrompt != "" {
prompt += fmt.Sprintf(" --no %s", task.NegPrompt)
}
body := ImageReq{
BotType: "MID_JOURNEY",
Prompt: task.Prompt,
Prompt: prompt,
Base64Array: make([]string, 0),
}
// 生成图片 Base64 编码
@ -42,7 +52,7 @@ func (c *PlusClient) Imagine(task types.MjTask) (ImageRes, error) {
logger.Info("API URL: ", apiURL)
var res ImageRes
var errRes ErrRes
r, err := req.C().R().
r, err := c.client.R().
SetHeader("Authorization", "Bearer "+c.Config.ApiKey).
SetBody(body).
SetSuccessResult(&res).
@ -81,7 +91,7 @@ func (c *PlusClient) Blend(task types.MjTask) (ImageRes, error) {
}
var res ImageRes
var errRes ErrRes
r, err := req.C().R().
r, err := c.client.R().
SetHeader("Authorization", "Bearer "+c.Config.ApiKey).
SetBody(body).
SetSuccessResult(&res).
@ -130,7 +140,7 @@ func (c *PlusClient) SwapFace(task types.MjTask) (ImageRes, error) {
}
var res ImageRes
var errRes ErrRes
r, err := req.C().R().
r, err := c.client.SetTimeout(time.Minute).R().
SetHeader("Authorization", "Bearer "+c.Config.ApiKey).
SetBody(body).
SetSuccessResult(&res).
@ -156,7 +166,7 @@ func (c *PlusClient) Upscale(task types.MjTask) (ImageRes, error) {
apiURL := fmt.Sprintf("%s/mj/submit/action", c.apiURL)
var res ImageRes
var errRes ErrRes
r, err := req.C().R().
r, err := c.client.R().
SetHeader("Authorization", "Bearer "+c.Config.ApiKey).
SetBody(body).
SetSuccessResult(&res).
@ -202,7 +212,7 @@ func (c *PlusClient) Variation(task types.MjTask) (ImageRes, error) {
func (c *PlusClient) QueryTask(taskId string) (QueryRes, error) {
apiURL := fmt.Sprintf("%s/mj/task/%s/fetch", c.apiURL, taskId)
var res QueryRes
r, err := req.C().R().SetHeader("Authorization", "Bearer "+c.Config.ApiKey).
r, err := c.client.R().SetHeader("Authorization", "Bearer "+c.Config.ApiKey).
SetSuccessResult(&res).
Get(apiURL)

View File

@ -22,8 +22,12 @@ func NewProxyClient(config types.MjProxyConfig) *ProxyClient {
func (c *ProxyClient) Imagine(task types.MjTask) (ImageRes, error) {
apiURL := fmt.Sprintf("%s/mj/submit/imagine", c.apiURL)
prompt := fmt.Sprintf("%s %s", task.Prompt, task.Params)
if task.NegPrompt != "" {
prompt += fmt.Sprintf(" --no %s", task.NegPrompt)
}
body := ImageReq{
Prompt: task.Prompt,
Prompt: prompt,
Base64Array: make([]string, 0),
}
// 生成图片 Base64 编码
@ -46,8 +50,6 @@ func (c *ProxyClient) Imagine(task types.MjTask) (ImageRes, error) {
SetErrorResult(&errRes).
Post(apiURL)
if err != nil {
all, err := io.ReadAll(r.Body)
logger.Info(string(all))
return ImageRes{}, fmt.Errorf("请求 API %s 出错:%v", apiURL, err)
}

View File

@ -67,8 +67,8 @@ func (s *Service) Run() {
continue
}
// 如果是 mj-proxy 则自动翻译提示词
if utils.HasChinese(task.Prompt) && strings.HasPrefix(s.Name, "mj-proxy-service") {
// translate prompt
if utils.HasChinese(task.Prompt) {
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.Prompt))
if err == nil {
task.Prompt = content
@ -76,6 +76,15 @@ func (s *Service) Run() {
logger.Warnf("error with translate prompt: %v", err)
}
}
// translate negative prompt
if task.NegPrompt != "" && utils.HasChinese(task.NegPrompt) {
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.NegPrompt))
if err == nil {
task.NegPrompt = content
} else {
logger.Warnf("error with translate prompt: %v", err)
}
}
var job model.MidJourneyJob
tx := s.db.Where("id = ?", task.Id).First(&job)

View File

@ -20,7 +20,7 @@ type ServicePool struct {
Clients *types.LMap[uint, *types.WsClient] // UserId => Client
}
func NewServicePool(db *gorm.DB, redisCli *redis.Client, manager *oss.UploaderManager, appConfig *types.AppConfig) *ServicePool {
func NewServicePool(db *gorm.DB, redisCli *redis.Client, manager *oss.UploaderManager, appConfig *types.AppConfig, levelDB *store.LevelDB) *ServicePool {
services := make([]*Service, 0)
taskQueue := store.NewRedisQueue("StableDiffusion_Task_Queue", redisCli)
notifyQueue := store.NewRedisQueue("StableDiffusion_Queue", redisCli)
@ -32,7 +32,7 @@ func NewServicePool(db *gorm.DB, redisCli *redis.Client, manager *oss.UploaderMa
// create sd service
name := fmt.Sprintf("StableDifffusion Service-%s", config.Model)
service := NewService(name, config, taskQueue, notifyQueue, db, manager)
service := NewService(name, config, taskQueue, notifyQueue, db, manager, levelDB)
// run sd service
go func() {
service.Run()

View File

@ -24,9 +24,10 @@ type Service struct {
db *gorm.DB
uploadManager *oss.UploaderManager
name string // service name
leveldb *store.LevelDB
}
func NewService(name string, config types.StableDiffusionConfig, taskQueue *store.RedisQueue, notifyQueue *store.RedisQueue, db *gorm.DB, manager *oss.UploaderManager) *Service {
func NewService(name string, config types.StableDiffusionConfig, taskQueue *store.RedisQueue, notifyQueue *store.RedisQueue, db *gorm.DB, manager *oss.UploaderManager, levelDB *store.LevelDB) *Service {
config.ApiURL = strings.TrimRight(config.ApiURL, "/")
return &Service{
name: name,
@ -35,6 +36,7 @@ func NewService(name string, config types.StableDiffusionConfig, taskQueue *stor
taskQueue: taskQueue,
notifyQueue: notifyQueue,
db: db,
leveldb: levelDB,
uploadManager: manager,
}
}
@ -47,11 +49,24 @@ func (s *Service) Run() {
logger.Errorf("taking task with error: %v", err)
continue
}
// 翻译提示词
// translate prompt
if utils.HasChinese(task.Params.Prompt) {
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.RewritePromptTemplate, task.Params.Prompt))
if err == nil {
task.Params.Prompt = content
} else {
logger.Warnf("error with translate prompt: %v", err)
}
}
// translate negative prompt
if task.Params.NegPrompt != "" && utils.HasChinese(task.Params.NegPrompt) {
content, err := utils.OpenAIRequest(s.db, fmt.Sprintf(service.TranslatePromptTemplate, task.Params.NegPrompt))
if err == nil {
task.Params.NegPrompt = content
} else {
logger.Warnf("error with translate prompt: %v", err)
}
}
@ -108,7 +123,7 @@ type TaskProgressResp struct {
func (s *Service) Txt2Img(task types.SdTask) error {
body := Txt2ImgReq{
Prompt: task.Params.Prompt,
NegativePrompt: task.Params.NegativePrompt,
NegativePrompt: task.Params.NegPrompt,
Steps: task.Params.Steps,
CfgScale: task.Params.CfgScale,
Width: task.Params.Width,
@ -167,15 +182,20 @@ func (s *Service) Txt2Img(task types.SdTask) error {
}
s.db.Model(&model.SdJob{Id: uint(task.Id)}).UpdateColumn("progress", 100)
s.notifyQueue.RPush(task.UserId)
// 从 leveldb 中删除预览图片数据
_ = s.leveldb.Delete(task.Params.TaskId)
return nil
default:
err, resp := s.checkTaskProgress()
// 更新任务进度
if err == nil && resp.Progress > 0 {
logger.Debugf("Check task progress: %+v", resp.Progress)
s.db.Model(&model.SdJob{Id: uint(task.Id)}).UpdateColumn("progress", int(resp.Progress*100))
// 发送更新状态信号
s.notifyQueue.RPush(task.UserId)
// 保存预览图片数据
if resp.CurrentImage != "" {
_ = s.leveldb.Put(task.Params.TaskId, resp.CurrentImage)
}
}
time.Sleep(time.Second)
}

110
api/store/leveldb.go Normal file
View File

@ -0,0 +1,110 @@
package store
import (
"encoding/json"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/util"
)
type LevelDB struct {
driver *leveldb.DB
}
func NewLevelDB() (*LevelDB, error) {
db, err := leveldb.OpenFile("data", nil)
if err != nil {
return nil, err
}
return &LevelDB{
driver: db,
}, nil
}
func (db *LevelDB) Put(key string, value interface{}) error {
var byteData []byte
if v, ok := value.(string); ok {
byteData = []byte(v)
} else {
b, err := json.Marshal(value)
if err != nil {
return err
}
byteData = b
}
return db.driver.Put([]byte(key), byteData, nil)
}
func (db *LevelDB) Get(key string) ([]byte, error) {
bytes, err := db.driver.Get([]byte(key), nil)
if err != nil {
return nil, err
}
return bytes, nil
}
func (db *LevelDB) Search(prefix string) []string {
var items = make([]string, 0)
iter := db.driver.NewIterator(util.BytesPrefix([]byte(prefix)), nil)
defer iter.Release()
for iter.Next() {
items = append(items, string(iter.Value()))
}
return items
}
type PageVo struct {
Items []string
Page int
PageSize int
Total int
TotalPage int
}
func (db *LevelDB) SearchPage(prefix string, page int, pageSize int) *PageVo {
var items = make([]string, 0)
iter := db.driver.NewIterator(util.BytesPrefix([]byte(prefix)), nil)
defer iter.Release()
res := &PageVo{Page: page, PageSize: pageSize}
// 计算数据总数和总页数
total := 0
for iter.Next() {
total++
}
res.TotalPage = (total + pageSize - 1) / pageSize
res.Total = total
// 计算目标页码的起始和结束位置
start := (page - 1) * pageSize
if start > total {
return nil
}
end := start + pageSize
if end > total {
end = total
}
// 跳转到目标页码的起始位置
count := 0
for iter.Next() {
if count >= start {
items = append(items, string(iter.Value()))
}
count++
}
iter.Release()
res.Items = items
return res
}
func (db *LevelDB) Delete(key string) error {
return db.driver.Delete([]byte(key), nil)
}
// Close release resources
func (db *LevelDB) Close() error {
return db.driver.Close()
}

View File

@ -1,5 +1,12 @@
package main
func main() {
import (
"fmt"
"reflect"
)
func main() {
text := 1
bytes := reflect.ValueOf(text).Bytes()
fmt.Println(bytes)
}

View File

@ -61,7 +61,7 @@ func OpenAIRequest(db *gorm.DB, prompt string) (string, error) {
var response apiRes
var errRes apiErrRes
client := req.C()
if apiKey.ProxyURL != "" {
if len(apiKey.ProxyURL) > 5 {
client.SetProxyURL(apiKey.ApiURL)
}
r, err := client.R().SetHeader("Content-Type", "application/json").

View File

@ -15,8 +15,8 @@ server {
# ssl_prefer_server_ciphers on;
# 日志地址
access_log /var/log/access.log;
error_log /var/log/error.log;
access_log /var/log/nginx/access.log;
error_log /var/log/nginx/error.log;
index index.html;
root /var/www/app/dist; # 这里改成前端静态页面的地址

View File

@ -72,6 +72,7 @@ services:
- ./conf/config.toml:/var/www/app/config.toml
- ./logs/app:/var/www/app/logs
- ./static:/var/www/app/static
- ./data/leveldb:/var/www/app/data
# 前端应用
chatgpt-plus-web:

View File

@ -6,3 +6,4 @@ VUE_APP_ADMIN_USER=admin
VUE_APP_ADMIN_PASS=admin123
VUE_APP_KEY_PREFIX=ChatPLUS_DEV_
VUE_APP_TITLE="Geek-AI 创作系统"
VUE_APP_VERSION=v4.0.2

View File

@ -2,3 +2,4 @@ VUE_APP_API_HOST=
VUE_APP_WS_HOST=
VUE_APP_KEY_PREFIX=ChatPLUS_
VUE_APP_TITLE="Geek-AI 创作系统"
VUE_APP_VERSION=v4.0.2

Binary file not shown.

Before

Width:  |  Height:  |  Size: 741 B

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -182,6 +182,11 @@
.page-mj .inner .el-form .el-slider {
width: 180px;
}
.page-mj .inner .el-form .uploader-icon {
font-size: 24px;
position: relative;
top: 3px;
}
.page-mj .inner .task-list-box {
width: 100%;
padding: 10px;
@ -275,6 +280,13 @@
width: 20px;
height: 20px;
}
.page-mj .inner .task-list-box .task-list-inner .el-row.text-info {
width: 100%;
padding: 10px 0;
}
.page-mj .inner .task-list-box .task-list-inner .el-row.text-info .el-tag {
margin-right: 10px;
}
.page-mj .inner .task-list-box .task-list-inner .submit-btn {
display: flex;
margin: 20px 0;
@ -282,12 +294,6 @@
.page-mj .inner .task-list-box .task-list-inner .submit-btn .el-button {
width: 200px;
}
.page-mj .inner .task-list-box .task-list-inner .submit-btn .text-info {
width: 100%;
display: flex;
justify-content: right;
align-items: center;
}
.page-mj .inner .task-list-box .task-list-inner .job-list-box .running-job-list .job-item {
width: 100%;
padding: 2px;

View File

@ -225,6 +225,12 @@
.el-input, .el-slider {
width 180px
}
.uploader-icon {
font-size 24px
position relative
top 3px
}
}
@import "task-list.styl"

View File

@ -53,6 +53,12 @@
padding-top: 5px;
padding-bottom: 5px;
}
.page-sd .inner .sd-box .sd-params .text-info {
padding: 10px;
}
.page-sd .inner .sd-box .sd-params .text-info .el-tag {
margin-right: 10px;
}
.page-sd .inner .sd-box .submit-btn {
padding: 10px 15px 0 15px;
text-align: center;
@ -159,6 +165,13 @@
width: 20px;
height: 20px;
}
.page-sd .inner .task-list-box .task-list-inner .el-row.text-info {
width: 100%;
padding: 10px 0;
}
.page-sd .inner .task-list-box .task-list-inner .el-row.text-info .el-tag {
margin-right: 10px;
}
.page-sd .inner .task-list-box .task-list-inner .submit-btn {
display: flex;
margin: 20px 0;
@ -166,12 +179,6 @@
.page-sd .inner .task-list-box .task-list-inner .submit-btn .el-button {
width: 200px;
}
.page-sd .inner .task-list-box .task-list-inner .submit-btn .text-info {
width: 100%;
display: flex;
justify-content: right;
align-items: center;
}
.page-sd .inner .task-list-box .task-list-inner .job-list-box .running-job-list .job-item {
width: 100%;
padding: 2px;

View File

@ -65,6 +65,14 @@
padding-top 5px
padding-bottom 5px
}
.text-info {
padding 10px
.el-tag {
margin-right 10px
}
}
}
.submit-btn {

View File

@ -1,8 +1,8 @@
.mobile-mj .content .text-line {
.mobile-mj .text-line {
padding: 6px;
font-size: 14px;
}
.mobile-mj .content .text-line .van-row .van-col .rate {
.mobile-mj .text-line .van-row .van-col .rate {
display: flex;
justify-content: center;
background-color: #f5f5f5;
@ -11,17 +11,17 @@
border-radius: 5px;
flex-flow: column;
}
.mobile-mj .content .text-line .van-row .van-col .rate .icon {
.mobile-mj .text-line .van-row .van-col .rate .icon {
text-align: center;
}
.mobile-mj .content .text-line .van-row .van-col .rate .icon .van-image {
.mobile-mj .text-line .van-row .van-col .rate .icon .van-image {
max-width: 20px;
}
.mobile-mj .content .text-line .van-row .van-col .rate .text {
.mobile-mj .text-line .van-row .van-col .rate .text {
text-align: center;
color: #555;
}
.mobile-mj .content .text-line .van-row .van-col .model {
.mobile-mj .text-line .van-row .van-col .model {
display: flex;
justify-content: center;
background-color: #f5f5f5;
@ -30,40 +30,45 @@
border-radius: 5px;
flex-flow: column;
}
.mobile-mj .content .text-line .van-row .van-col .model .icon {
.mobile-mj .text-line .van-row .van-col .model .icon {
text-align: center;
}
.mobile-mj .content .text-line .van-row .van-col .model .icon .van-image {
.mobile-mj .text-line .van-row .van-col .model .icon .van-image {
width: 100%;
height: 50px;
}
.mobile-mj .content .text-line .van-row .van-col .model .text {
.mobile-mj .text-line .van-row .van-col .model .text {
text-align: center;
color: #555;
}
.mobile-mj .content .text-line .van-row .van-col .active {
.mobile-mj .text-line .van-row .van-col .active {
background-color: #e5e5e5;
}
.mobile-mj .content .text-line .van-button {
.mobile-mj .text-line .van-button {
position: relative;
}
.mobile-mj .content .text-line .van-button .van-tag {
.mobile-mj .text-line .van-button .van-tag {
position: absolute;
right: 20px;
}
.mobile-mj .content .text-line .align-right {
.mobile-mj .text-line .align-right {
display: flex;
justify-content: right;
}
.mobile-mj .content .running-job-list .van-grid .van-grid-item .van-grid-item__content {
.mobile-mj .tip-text {
padding: 10px;
line-height: 1.5;
color: #c1c1c1;
}
.mobile-mj .running-job-list .van-grid .van-grid-item .van-grid-item__content {
padding: 0;
position: relative;
}
.mobile-mj .content .running-job-list .van-grid .van-grid-item .van-grid-item__content .van-image,
.mobile-mj .content .running-job-list .van-grid .van-grid-item .van-grid-item__content .task-in-queue {
.mobile-mj .running-job-list .van-grid .van-grid-item .van-grid-item__content .van-image,
.mobile-mj .running-job-list .van-grid .van-grid-item .van-grid-item__content .task-in-queue {
min-height: 100px;
}
.mobile-mj .content .running-job-list .van-grid .van-grid-item .van-grid-item__content .progress {
.mobile-mj .running-job-list .van-grid .van-grid-item .van-grid-item__content .progress {
display: flex;
justify-content: center;
align-items: center;
@ -74,36 +79,36 @@
left: 0;
top: 0;
}
.mobile-mj .content .running-job-list .van-grid .van-grid-item .van-grid-item__content .progress .van-circle__text {
.mobile-mj .running-job-list .van-grid .van-grid-item .van-grid-item__content .progress .van-circle__text {
color: #fff;
}
.mobile-mj .content .running-job-list .van-grid .van-grid-item .van-grid-item__content .task-in-queue {
.mobile-mj .running-job-list .van-grid .van-grid-item .van-grid-item__content .task-in-queue {
display: flex;
flex-flow: column;
justify-content: center;
color: #c1c1c1;
}
.mobile-mj .content .running-job-list .van-grid .van-grid-item .van-grid-item__content .task-in-queue .icon {
.mobile-mj .running-job-list .van-grid .van-grid-item .van-grid-item__content .task-in-queue .icon {
text-align: center;
}
.mobile-mj .content .running-job-list .van-grid .van-grid-item .van-grid-item__content .task-in-queue .icon .iconfont {
.mobile-mj .running-job-list .van-grid .van-grid-item .van-grid-item__content .task-in-queue .icon .iconfont {
font-size: 24px;
}
.mobile-mj .content .running-job-list .van-grid .van-grid-item .van-grid-item__content .task-in-queue .text {
.mobile-mj .running-job-list .van-grid .van-grid-item .van-grid-item__content .task-in-queue .text {
font-size: 14px;
margin-top: 5px;
}
.mobile-mj .content .finish-job-list .van-grid .van-grid-item .van-grid-item__content {
.mobile-mj .finish-job-list .van-grid .van-grid-item .van-grid-item__content {
padding: 0;
}
.mobile-mj .content .finish-job-list .van-grid .van-grid-item .van-grid-item__content .job-item {
.mobile-mj .finish-job-list .van-grid .van-grid-item .van-grid-item__content .job-item {
overflow: hidden;
border-radius: 6px;
position: relative;
height: 100%;
width: 100%;
}
.mobile-mj .content .finish-job-list .van-grid .van-grid-item .van-grid-item__content .job-item .opt .opt-btn {
.mobile-mj .finish-job-list .van-grid .van-grid-item .van-grid-item__content .job-item .opt .opt-btn {
padding: 2px 0;
text-align: center;
border-radius: 5px;
@ -115,20 +120,20 @@
font-size: 14px;
width: 100%;
}
.mobile-mj .content .finish-job-list .van-grid .van-grid-item .van-grid-item__content .job-item .van-image {
.mobile-mj .finish-job-list .van-grid .van-grid-item .van-grid-item__content .job-item .van-image {
width: 100%;
height: 200px;
}
.mobile-mj .content .finish-job-list .van-grid .van-grid-item .van-grid-item__content .job-item .upscale {
.mobile-mj .finish-job-list .van-grid .van-grid-item .van-grid-item__content .job-item .upscale {
height: 260px;
width: 100%;
}
.mobile-mj .content .finish-job-list .van-grid .van-grid-item .van-grid-item__content .job-item .remove {
.mobile-mj .finish-job-list .van-grid .van-grid-item .van-grid-item__content .job-item .remove {
position: absolute;
right: 5px;
top: 5px;
}
.mobile-mj .content .finish-job-list .van-grid .van-grid-item .van-grid-item__content .job-item .remove .el-button {
.mobile-mj .finish-job-list .van-grid .van-grid-item .van-grid-item__content .job-item .remove .el-button {
margin-left: 5px;
height: auto;
padding: 5px;

View File

@ -1,5 +1,4 @@
.mobile-mj {
.content {
.text-line {
padding 6px
font-size 14px
@ -74,6 +73,13 @@
}
}
.tip-text {
padding 10px
line-height 1.5
color #c1c1c1
}
.running-job-list {
.van-grid {
.van-grid-item {
@ -186,4 +192,3 @@
}
}
}
}

View File

@ -0,0 +1,133 @@
.mobile-sd .text-line {
padding: 0 6px;
font-size: 14px;
}
.mobile-sd .text-line .van-row {
width: 100%;
}
.mobile-sd .text-line .van-row .van-col .rate {
display: flex;
justify-content: center;
background-color: #f5f5f5;
padding: 5px 10px;
margin: 5px 0;
border-radius: 5px;
flex-flow: column;
}
.mobile-sd .text-line .van-row .van-col .rate .icon {
text-align: center;
}
.mobile-sd .text-line .van-row .van-col .rate .icon .van-image {
max-width: 20px;
}
.mobile-sd .text-line .van-row .van-col .rate .text {
text-align: center;
color: #555;
}
.mobile-sd .text-line .van-row .van-col .el-input__inner {
text-align: center;
}
.mobile-sd .text-line .van-row .van-col .model {
display: flex;
justify-content: center;
background-color: #f5f5f5;
padding: 6px;
margin: 5px 0;
border-radius: 5px;
flex-flow: column;
}
.mobile-sd .text-line .van-row .van-col .model .icon {
text-align: center;
}
.mobile-sd .text-line .van-row .van-col .model .icon .van-image {
width: 100%;
height: 50px;
}
.mobile-sd .text-line .van-row .van-col .model .text {
text-align: center;
color: #555;
}
.mobile-sd .text-line .van-row .van-col .active {
background-color: #e5e5e5;
}
.mobile-sd .text-line .van-button {
position: relative;
}
.mobile-sd .text-line .van-button .van-tag {
position: absolute;
right: 20px;
}
.mobile-sd .text-line .align-right {
display: flex;
justify-content: right;
}
.mobile-sd .pt-6 {
padding: 10px;
}
.mobile-sd .tip-text {
padding: 10px;
line-height: 1.5;
color: #c1c1c1;
}
.mobile-sd .running-job-list .van-grid .van-grid-item .van-grid-item__content {
padding: 0;
position: relative;
}
.mobile-sd .running-job-list .van-grid .van-grid-item .van-grid-item__content .van-image,
.mobile-sd .running-job-list .van-grid .van-grid-item .van-grid-item__content .task-in-queue {
min-height: 100px;
}
.mobile-sd .running-job-list .van-grid .van-grid-item .van-grid-item__content .progress {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
background: rgba(50,50,50,0.5);
position: absolute;
left: 0;
top: 0;
}
.mobile-sd .running-job-list .van-grid .van-grid-item .van-grid-item__content .progress .van-circle__text {
color: #fff;
}
.mobile-sd .running-job-list .van-grid .van-grid-item .van-grid-item__content .task-in-queue {
display: flex;
flex-flow: column;
justify-content: center;
color: #c1c1c1;
}
.mobile-sd .running-job-list .van-grid .van-grid-item .van-grid-item__content .task-in-queue .icon {
text-align: center;
}
.mobile-sd .running-job-list .van-grid .van-grid-item .van-grid-item__content .task-in-queue .icon .iconfont {
font-size: 24px;
}
.mobile-sd .running-job-list .van-grid .van-grid-item .van-grid-item__content .task-in-queue .text {
font-size: 14px;
margin-top: 5px;
}
.mobile-sd .finish-job-list .van-grid .van-grid-item .van-grid-item__content {
padding: 0;
}
.mobile-sd .finish-job-list .van-grid .van-grid-item .van-grid-item__content .job-item {
overflow: hidden;
border-radius: 6px;
position: relative;
height: 100%;
width: 100%;
}
.mobile-sd .finish-job-list .van-grid .van-grid-item .van-grid-item__content .job-item .van-image {
width: 100%;
height: 200px;
}
.mobile-sd .finish-job-list .van-grid .van-grid-item .van-grid-item__content .job-item .remove {
position: absolute;
right: 5px;
top: 5px;
}
.mobile-sd .finish-job-list .van-grid .van-grid-item .van-grid-item__content .job-item .remove .el-button {
margin-left: 5px;
height: auto;
padding: 5px;
}

View File

@ -0,0 +1,184 @@
.mobile-sd {
.text-line {
padding 0 6px
font-size 14px
.van-row {
width 100%
.van-col {
.rate {
display: flex;
justify-content center
background-color #f5f5f5
padding 5px 10px
margin 5px 0
border-radius 5px
flex-flow column
.icon {
text-align center
.van-image {
max-width 20px
}
}
.text {
text-align center
color #555555
}
}
.el-input__inner {
text-align center
}
.model {
display: flex;
justify-content center
background-color #f5f5f5
padding 6px
margin 5px 0
border-radius 5px
flex-flow column
.icon {
text-align center
.van-image {
width 100%
height 50px
}
}
.text {
text-align center
color #555555
}
}
.active {
background-color #e5e5e5
}
}
}
.van-button {
position relative
.van-tag {
position absolute
right 20px
}
}
.align-right {
display flex
justify-content right
}
}
.pt-6 {
padding 10px
}
.tip-text {
padding 10px
line-height 1.5
color #c1c1c1
}
.running-job-list {
.van-grid {
.van-grid-item {
.van-grid-item__content {
padding 0
position relative
.van-image, .task-in-queue {
min-height 100px
}
.progress {
display flex
justify-content center
align-items center
width 100%
height 100%
background rgba(50, 50, 50, 0.5)
position absolute
left 0
top 0
.van-circle__text {
color #ffffff
}
}
// end progress
.task-in-queue {
display flex
flex-flow column
justify-content center
color #c1c1c1
.icon {
text-align center
.iconfont {
font-size 24px
}
}
.text {
font-size 14px
margin-top 5px
}
}
}
}
}
}
//end running jobs
.finish-job-list {
.van-grid {
.van-grid-item {
.van-grid-item__content {
padding 0
.job-item {
overflow hidden
border-radius 6px
position relative
height 100%
width 100%
.van-image {
width 100%
height 200px
}
.remove {
position absolute
right 5px
top 5px
.el-button {
margin-left 5px
height auto
padding 5px
}
}
}
}
}
}
}
}

View File

@ -116,6 +116,15 @@
}
}
.el-row.text-info {
width 100%
padding 10px 0
.el-tag {
margin-right 10px
}
}
//
.submit-btn {
@ -125,13 +134,6 @@
.el-button {
width 200px
}
.text-info {
width 100%
display flex
justify-content right
align-items center
}
}

View File

@ -120,6 +120,10 @@ const removeFile = (file) => {
const insertURL = (url) => {
show.value = false
//
if (url.indexOf("http") === -1) {
url = location.protocol + "//" + location.host + url
}
emits('selected', url)
}
</script>

View File

@ -2,7 +2,9 @@
<div class="container">
<div class="footer">
Powered by {{ author }} @
<el-link type="primary" href="https://github.com/yangjian102621/chatgpt-plus" target="_blank">{{ title }}
<el-link type="primary" href="https://github.com/yangjian102621/chatgpt-plus" target="_blank">
{{ title }} -
{{ version }}
</el-link>
</div>
</div>
@ -12,6 +14,7 @@
import {ref} from "vue";
const title = ref(process.env.VUE_APP_TITLE)
const version = ref(process.env.VUE_APP_VERSION)
const author = ref('极客学长')
</script>

View File

@ -1,7 +1,7 @@
<template>
<div class="welcome">
<div class="container">
<h1 class="title">{{ title }}</h1>
<h1 class="title">{{ title }}-{{ version }}</h1>
<el-row :gutter="20">
<el-col :span="8">
@ -60,6 +60,7 @@ import {httpGet} from "@/utils/http";
import {ElMessage} from "element-plus";
const title = ref(process.env.VUE_APP_TITLE)
const version = ref(process.env.VUE_APP_VERSION)
const samples = ref([
"用小学生都能听懂的术语解释什么是量子纠缠",

View File

@ -2,7 +2,7 @@
<div class="sidebar">
<div class="logo">
<el-image :src="logo"/>
<span class="text" v-show="!sidebar.collapse">{{ title }}</span>
<span class="text" v-show="!sidebar.collapse">{{ title }} - {{ version }}</span>
</div>
<el-menu
@ -60,6 +60,7 @@ import {ElMessage} from "element-plus";
const title = ref('Chat-Plus-Admin')
const logo = ref('/images/logo.png')
const version = ref(process.env.VUE_APP_VERSION)
//
httpGet('/api/admin/config/get?key=system').then(res => {

View File

@ -181,7 +181,7 @@ const routes = [
{
name: 'mobile',
path: '/mobile',
meta: {title: 'ChatPlus-智能助手V3'},
meta: {title: 'Geek-AI v4.0'},
component: () => import('@/views/mobile/Home.vue'),
redirect: '/mobile/chat',
children: [
@ -191,9 +191,9 @@ const routes = [
component: () => import('@/views/mobile/ChatList.vue'),
},
{
path: '/mobile/mj',
name: 'mobile-mj',
component: () => import('@/views/mobile/ImageMj.vue'),
path: '/mobile/image',
name: 'mobile-image',
component: () => import('@/views/mobile/Image.vue'),
},
{
path: '/mobile/profile',

View File

@ -103,7 +103,13 @@
:key="item.id"
:label="item.name"
:value="item.id"
/>
>
<span>{{ item.name }}</span>
<el-tag style="margin-left: 5px; position: relative; top:-2px" type="info" size="small">{{
item.power
}}算力
</el-tag>
</el-option>
</el-select>
<el-button type="primary" @click="newChat">
<el-icon>
@ -445,6 +451,12 @@ const newChat = () => {
showLoginDialog.value = true
return;
}
const role = getRoleById(roleId.value)
if (role.key === 'gpt') {
showHello.value = true
} else {
showHello.value = false
}
//
if (newChatItem.value !== null && newChatItem.value['role_id'] === roles.value[0]['role_id']) {
return;
@ -479,9 +491,15 @@ const changeChat = (chat) => {
}
const loadChat = function (chat) {
if (!isLogin.value) {
showLoginDialog.value = true
return;
}
if (activeChat.value['chat_id'] === chat.chat_id) {
return;
}
activeChat.value = chat
newChatItem.value = null;
roleId.value = chat.role_id;

View File

@ -6,12 +6,9 @@
<div class="divider"></div>
</div>
<ul class="nav-items">
<li v-for="item in navs" :key="item.path">
<!-- <el-tooltip effect="light" :content="item.title" placement="right">-->
<!-- -->
<!-- </el-tooltip>-->
<a @click="changeNav(item)" :class="item.path === curPath ? 'active' : ''">
<el-image :src="item.icon" :width="20"/>
<li v-for="item in navs" :key="item.url">
<a @click="changeNav(item)" :class="item.url === curPath ? 'active' : ''">
<el-image :src="item.icon" style="width: 30px;height: 30px"/>
</a>
<div :class="item.url === curPath ? 'title active' : 'title'">{{ item.name }}</div>
</li>
@ -69,7 +66,7 @@ onMounted(() => {
.navigator {
display flex
flex-flow column
width 70px
width 60px
padding 10px 6px
border-right: 1px solid #3c3c3c
background-color: #25272D
@ -79,6 +76,10 @@ onMounted(() => {
flex-flow column
align-items center
.el-image {
width 50px
height 50px
}
.divider {
border-bottom 1px solid #4A4A4A
@ -89,15 +90,13 @@ onMounted(() => {
.nav-items {
margin-top: 20px;
padding-left: 10px;
padding-right: 10px;
padding 0 5px
li {
margin-bottom 15px
a {
color #DADBDC
background-color #40444A
border-radius 10px
width 48px
height 48px
@ -117,6 +116,7 @@ onMounted(() => {
a:hover, a.active {
color #47fff1
background-color #0F7A71
}
.title {

View File

@ -168,7 +168,48 @@
<div class="extra-params">
<el-form>
<el-tabs v-model="activeName" class="title-tabs" @tabChange="tabChange">
<el-tab-pane label="文生图(可选)" name="image">
<el-tab-pane label="文生图" name="txt2img">
<div class="prompt-box">
<div class="param-line pt">
<div class="flex-row justify-between items-center">
<div class="flex-row justify-start items-center">
<span>提示词</span>
<el-tooltip effect="light" content="输入你想要的内容,用逗号分割" placement="right">
<el-icon>
<InfoFilled/>
</el-icon>
</el-tooltip>
</div>
</div>
</div>
<div class="param-line pt">
<el-input v-model="params.prompt" :autosize="{ minRows: 4, maxRows: 6 }" type="textarea"
ref="promptRef"
placeholder="请在此输入绘画提示词,系统会自动翻译中文提示词,高手请直接输入英文提示词"/>
</div>
<div class="param-line pt">
<div class="flex-row justify-between items-center">
<div class="flex-row justify-start items-center">
<span>不希望出现的内容可选</span>
<el-tooltip effect="light" content="不想出现在图片上的元素(例如:树,建筑)" placement="right">
<el-icon>
<InfoFilled/>
</el-icon>
</el-tooltip>
</div>
</div>
</div>
<div class="param-line pt">
<el-input v-model="params.neg_prompt" :autosize="{ minRows: 4, maxRows: 6 }" type="textarea"
ref="promptRef"
placeholder="请在此输入你不希望出现在图片上的内容,系统会自动翻译中文提示词"/>
</div>
</div>
</el-tab-pane>
<el-tab-pane label="图生图" name="img2img">
<div class="text">图生图以某张图片为底稿参考来创作绘画生成类似风格或类型图像支持 PNG JPG 格式图片
</div>
<div class="param-line">
@ -190,15 +231,15 @@
</div>
<div class="param-line" style="padding-top: 10px">
<el-form-item label="图像权重:">
<el-form-item label="参考权重:">
<template #default>
<div class="form-item-inner">
<el-slider v-model.number="params.weight" :max="1" :step="0.01"
<el-slider v-model.number="params.iw" :max="1" :step="0.01"
style="width: 180px;--el-slider-main-bg-color:#47fff1"/>
<el-tooltip effect="light"
content="使用图像权重参数--iw来调整图像 URL 与文本的重要性 <br/>权重较高时意味着图像提示将对完成的作业产生更大的影响"
raw-content placement="right">
<el-icon style="margin-top: 9px">
<el-icon>
<InfoFilled/>
</el-icon>
</el-tooltip>
@ -248,8 +289,8 @@
</div>
</el-tab-pane>
<el-tab-pane label="融图(可选)" name="blend">
<div class="text">请上传两张以上的图片最多不超过五张超过五张图片请使用生图功能</div>
<el-tab-pane label="融图" name="blend">
<div class="text">请上传两张以上的图片最多不超过五张超过五张图片请使用生图功能</div>
<div class="img-inline">
<div class="img-list-box">
<div class="img-item" v-for="imgURL in imgList">
@ -267,8 +308,8 @@
</div>
</el-tab-pane>
<el-tab-pane label="换脸(可选)" name="swapFace">
<div class="text">请上传两张有脸部的图片右边图片的脸替换左边图片的脸</div>
<el-tab-pane label="换脸" name="swapFace">
<div class="text">请上传两张有脸部的图片左边图片的脸替换右边图片的脸</div>
<div class="img-inline">
<div class="img-list-box">
<div class="img-item" v-for="imgURL in imgList">
@ -285,13 +326,124 @@
</el-upload>
</div>
</el-tab-pane>
<el-tab-pane name="cref">
<template #label>
<el-badge value="New">
<span>一致性</span>
</el-badge>
</template>
<div class="text">注意只有于 niji6 v6 模型支持一致性功能如果选择其他模型此功能将会生成失败</div>
<div class="param-line">
<el-form-item label="角色一致性:" prop="cref">
<el-input v-model="params.cref" placeholder="请输入图片URL或者上传图片"
style="--el-input-focus-border-color:#47fff1;--el-input-text-color:#ffffff; max-width: 500px; width: 100%"
size="small">
<template #append>
<el-upload
:auto-upload="true"
:show-file-list="false"
@click="beforeUpload('cref')"
:http-request="uploadImg"
>
<el-icon class="uploader-icon">
<UploadFilled/>
</el-icon>
</el-upload>
</template>
</el-input>
</el-form-item>
</div>
<div class="param-line">
<el-form-item label="风格一致性:" prop="sref">
<el-input v-model="params.sref" placeholder="请输入图片URL或者上传图片"
style="--el-input-focus-border-color:#47fff1; --el-input-text-color:#ffffff; max-width: 500px; width: 100%"
size="small">
<template #append>
<el-upload
:auto-upload="true"
:show-file-list="false"
@click="beforeUpload('sref')"
:http-request="uploadImg"
>
<el-icon class="uploader-icon">
<UploadFilled/>
</el-icon>
</el-upload>
</template>
</el-input>
</el-form-item>
</div>
<div class="param-line" style="padding-top: 10px">
<el-form-item label="参考权重:">
<template #default>
<div class="form-item-inner">
<el-slider v-model.number="params.cw" :max="100" :step="1"
style="width: 180px;--el-slider-main-bg-color:#47fff1"/>
<el-tooltip effect="light"
content="取值范围 0-100 <br/>默认值100参考原图的脸部、头发和衣服<br/>0则表示只换脸"
raw-content placement="right">
<el-icon>
<InfoFilled/>
</el-icon>
</el-tooltip>
</div>
</template>
</el-form-item>
</div>
<div class="prompt-box">
<div class="param-line pt">
<div class="flex-row justify-between items-center">
<div class="flex-row justify-start items-center">
<span>提示词</span>
<el-tooltip effect="light" content="输入你想要的内容,用逗号分割" placement="right">
<el-icon>
<InfoFilled/>
</el-icon>
</el-tooltip>
</div>
</div>
</div>
<div class="param-line pt">
<el-input v-model="params.prompt" :autosize="{ minRows: 4, maxRows: 6 }" type="textarea"
ref="promptRef"
placeholder="请在此输入绘画提示词,系统会自动翻译中文提示词,高手请直接输入英文提示词"/>
</div>
<div class="param-line pt">
<div class="flex-row justify-between items-center">
<div class="flex-row justify-start items-center">
<span>不希望出现的内容可选</span>
<el-tooltip effect="light" content="不想出现在图片上的元素(例如:树,建筑)" placement="right">
<el-icon>
<InfoFilled/>
</el-icon>
</el-tooltip>
</div>
</div>
</div>
<div class="param-line pt">
<el-input v-model="params.neg_prompt" :autosize="{ minRows: 4, maxRows: 6 }" type="textarea"
ref="promptRef"
placeholder="请在此输入你不希望出现在图片上的内容,系统会自动翻译中文提示词"/>
</div>
</div>
</el-tab-pane>
</el-tabs>
<el-row class="text-info">
<el-tag>每次绘图消耗{{ mjPower }}算力U/V 操作消耗{{ mjActionPower }}算力</el-tag>
<el-tag type="success">当前可用算力{{ power }}</el-tag>
</el-row>
<div class="submit-btn">
<el-button color="#47fff1" :dark="false" @click="generate" round>立即生成</el-button>
<div class="text-info">
<el-tag type="success">当前可用算力{{ power }}</el-tag>
</div>
</div>
</el-form>
</div>
@ -341,7 +493,7 @@
</div>
<h2>创作记录</h2>
<div class="finish-job-list" v-loading="loading" element-loading-background="rgba(255, 255, 255, 0.5)">
<div class="finish-job-list" v-loading="loading" element-loading-background="rgba(0, 0, 0, 0.5)">
<div v-if="finishedJobs.length > 0">
<ItemList :items="finishedJobs" :width="240" :gap="16">
<template #default="scope">
@ -373,10 +525,42 @@
<div class="opt" v-if="scope.item['can_opt']">
<div class="opt-line">
<ul>
<li><a @click="upscale(1, scope.item)">U1</a></li>
<li><a @click="upscale(2, scope.item)">U2</a></li>
<li><a @click="upscale(3, scope.item)">U3</a></li>
<li><a @click="upscale(4, scope.item)">U4</a></li>
<li>
<el-tooltip
class="box-item"
effect="light"
content="放大第一张"
placement="top">
<a @click="upscale(1, scope.item)">U1</a>
</el-tooltip>
</li>
<li>
<el-tooltip
class="box-item"
effect="light"
content="放大第二张"
placement="top">
<a @click="upscale(2, scope.item)">U2</a>
</el-tooltip>
</li>
<li>
<el-tooltip
class="box-item"
effect="light"
content="放大第三张"
placement="top">
<a @click="upscale(3, scope.item)">U3</a>
</el-tooltip>
</li>
<li>
<el-tooltip
class="box-item"
effect="light"
content="放大第四张"
placement="top">
<a @click="upscale(4, scope.item)">U4</a>
</el-tooltip>
</li>
<li class="show-prompt">
<el-popover placement="left" title="提示词" :width="240" trigger="hover">
@ -402,10 +586,42 @@
<div class="opt-line">
<ul>
<li><a @click="variation(1, scope.item)">V1</a></li>
<li><a @click="variation(2, scope.item)">V2</a></li>
<li><a @click="variation(3, scope.item)">V3</a></li>
<li><a @click="variation(4, scope.item)">V4</a></li>
<li>
<el-tooltip
class="box-item"
effect="light"
content="变化第一张"
placement="top">
<a @click="variation(1, scope.item)">V1</a>
</el-tooltip>
</li>
<li>
<el-tooltip
class="box-item"
effect="light"
content="变化第二张"
placement="top">
<a @click="variation(2, scope.item)">V2</a>
</el-tooltip>
</li>
<li>
<el-tooltip
class="box-item"
effect="light"
content="变化第三张"
placement="top">
<a @click="variation(3, scope.item)">V3</a>
</el-tooltip>
</li>
<li>
<el-tooltip
class="box-item"
effect="light"
content="变化第四张"
placement="top">
<a @click="variation(4, scope.item)">V4</a>
</el-tooltip>
</li>
</ul>
</div>
</div>
@ -442,7 +658,7 @@
<script setup>
import {nextTick, onMounted, onUnmounted, ref} from "vue"
import {ChromeFilled, Delete, DocumentCopy, InfoFilled, Picture, Plus} from "@element-plus/icons-vue";
import {ChromeFilled, Delete, DocumentCopy, InfoFilled, Picture, Plus, UploadFilled} from "@element-plus/icons-vue";
import Compressor from "compressorjs";
import {httpGet, httpPost} from "@/utils/http";
import {ElMessage, ElMessageBox, ElNotification} from "element-plus";
@ -451,7 +667,7 @@ import Clipboard from "clipboard";
import {checkSession} from "@/action/session";
import {useRouter} from "vue-router";
import {getSessionId} from "@/store/session";
import {removeArrayItem} from "@/utils/libs";
import {copyObj, removeArrayItem} from "@/utils/libs";
import LoginDialog from "@/components/LoginDialog.vue";
const listBoxHeight = ref(window.innerHeight - 40)
@ -479,11 +695,12 @@ const models = [
{text: "优质模式MJ-5.1", value: " --v 5.1", img: "/images/mj/mj-v5.1.jpg"},
{text: "虚幻模式MJ-5", value: " --v 5", img: "/images/mj/mj-v5.jpg"},
{text: "真实模式MJ-4", value: " --v 4", img: "/images/mj/mj-v4.jpg"},
{text: "动漫风niji5 原始", value: " --niji 5", img: "/images/mj/mj-niji.png"},
{text: "动漫风niji5 可爱", value: " --niji 5 --style cute", img: "/images/mj/nj1.jpg"},
{text: "动漫风niji5 风景", value: " --niji 5 --style scenic", img: "/images/mj/nj2.jpg"},
{text: "动漫风niji5 表现力", value: " --niji 5 --style expressive", img: "/images/mj/nj3.jpg"},
{text: "动漫风niji4", value: " --niji 4", img: "/images/mj/nj4.jpg"},
{text: "动漫风-niji4", value: " --niji 4", img: "/images/mj/nj4.jpg"},
{text: "动漫风-niji5", value: " --niji 5", img: "/images/mj/mj-niji.png"},
{text: "动漫风-niji5 可爱", value: " --niji 5 --style cute", img: "/images/mj/nj1.jpg"},
{text: "动漫风-niji5 风景", value: " --niji 5 --style scenic", img: "/images/mj/nj2.jpg"},
{text: "动漫风-niji6", value: " --niji 6", img: "/images/mj/nj3.jpg"},
]
const options = [
@ -515,17 +732,20 @@ const initParams = {
seed: 0,
img_arr: [],
raw: false,
weight: 0.25,
iw: 0,
prompt: router.currentRoute.value.params["prompt"] ?? "",
neg_prompt: "",
tile: false,
quality: 0
quality: 0,
cref: "",
sref: "",
cw: 0,
}
const params = ref(initParams)
const params = ref(copyObj(initParams))
const imgList = ref([])
const activeName = ref('image')
const activeName = ref('txt2img')
const runningJobs = ref([])
const finishedJobs = ref([])
@ -714,6 +934,11 @@ const changeModel = (item) => {
params.value.model = item.value
}
const imgKey = ref("")
const beforeUpload = (key) => {
imgKey.value = key
}
//
const uploadImg = (file) => {
if (!isLogin.value) {
@ -729,7 +954,12 @@ const uploadImg = (file) => {
formData.append('file', result, result.name);
//
httpPost('/api/upload', formData).then((res) => {
if (imgKey.value === '') {
imgList.value.push(res.data.url)
} else { //
params.value[imgKey.value] = res.data.url
imgKey.value = ''
}
ElMessage.success('上传成功')
}).catch((e) => {
ElMessage.error('上传失败:' + e.message)
@ -764,7 +994,8 @@ const generate = () => {
httpPost("/api/mj/image", params.value).then(() => {
ElMessage.success("绘画任务推送成功,请耐心等待任务执行...")
power.value -= mjPower.value
params.value = initParams
params.value = copyObj(initParams)
imgList.value = []
}).catch(e => {
ElMessage.error("任务推送失败:" + e.message)
})
@ -831,8 +1062,12 @@ const publishImage = (item, action) => {
//
const tabChange = (tab) => {
if (tab === "txt2img" || tab === "img2img" || tab === "cref") {
params.value.task_type = "image"
} else {
params.value.task_type = tab
}
}
//
const removeUploadImage = (url) => {

View File

@ -222,7 +222,7 @@
</div>
</div>
<div class="param-line" v-loading="translating" element-loading-background="rgba(122, 122, 122, 0.8)">
<div class="param-line">
<el-input
v-model="params.prompt"
:autosize="{ minRows: 4, maxRows: 6 }"
@ -246,16 +246,18 @@
</div>
<div class="param-line">
<el-input
v-model="params.negative_prompt"
v-model="params.neg_prompt"
:autosize="{ minRows: 4, maxRows: 6 }"
type="textarea"
placeholder="反向提示词"
/>
</div>
<div class="param-line" style="padding: 10px">
<div class="text-info">
<el-tag>每次绘图消耗{{ sdPower }}算力</el-tag>
<el-tag type="success">当前可用算力{{ power }}</el-tag>
</div>
</el-form>
</div>
<div class="submit-btn">
@ -307,7 +309,7 @@
<el-empty :image-size="100" v-else/>
</div>
<h2>创作记录</h2>
<div class="finish-job-list" v-loading="loading" element-loading-background="rgba(255, 255, 255, 0.5)">
<div class="finish-job-list" v-loading="loading" element-loading-background="rgba(0, 0, 0, 0.5)">
<div v-if="finishedJobs.length > 0">
<ItemList :items="finishedJobs" :width="240" :gap="16">
<template #default="scope">
@ -387,8 +389,8 @@
反向提示词
</el-divider>
<div class="prompt">
<span>{{ item.params.negative_prompt }}</span>
<el-icon class="copy-prompt-sd" :data-clipboard-text="item.params.negative_prompt">
<span>{{ item.params.neg_prompt }}</span>
<el-icon class="copy-prompt-sd" :data-clipboard-text="item.params.neg_prompt">
<DocumentCopy/>
</el-icon>
</div>
@ -479,7 +481,7 @@
<script setup>
import {onMounted, onUnmounted, ref} from "vue"
import {Delete, DocumentCopy, InfoFilled, Orange, Picture, Refresh} from "@element-plus/icons-vue";
import {Delete, DocumentCopy, InfoFilled, Orange, Picture} from "@element-plus/icons-vue";
import {httpGet, httpPost} from "@/utils/http";
import {ElMessage, ElMessageBox, ElNotification} from "element-plus";
import ItemList from "@/components/ItemList.vue";
@ -494,7 +496,6 @@ const mjBoxHeight = ref(window.innerHeight - 150)
const fullImgHeight = ref(window.innerHeight - 60)
const showTaskDialog = ref(false)
const item = ref({})
const translating = ref(false)
const showLoginDialog = ref(false)
const isLogin = ref(false)
@ -517,7 +518,7 @@ const params = ref({
hd_scale_alg: scaleAlg[0],
hd_steps: 0,
prompt: "",
negative_prompt: "nsfw, paintings,low quality,easynegative,ng_deepnegative ,lowres,bad anatomy,bad hands,bad feet",
neg_prompt: "nsfw, paintings,low quality,easynegative,ng_deepnegative ,lowres,bad anatomy,bad hands,bad feet",
})
const runningJobs = ref([])
@ -619,9 +620,9 @@ const initData = () => {
});
}
const fetchRunningJobs = (userId) => {
const fetchRunningJobs = () => {
//
httpGet(`/api/sd/jobs?status=0&user_id=${userId}`).then(res => {
httpGet(`/api/sd/jobs?status=0`).then(res => {
const jobs = res.data
const _jobs = []
for (let i = 0; i < jobs.length; i++) {

View File

@ -4,7 +4,7 @@
<div class="main">
<div class="contain">
<div class="logo">
<el-image src="images/logo.png" fit="cover"/>
<el-image :src="logo" fit="cover"/>
</div>
<div class="header">{{ title }}</div>
<div class="content">
@ -54,7 +54,7 @@
import {ref} from "vue";
import {Lock, UserFilled} from "@element-plus/icons-vue";
import {httpPost} from "@/utils/http";
import {httpGet, httpPost} from "@/utils/http";
import {ElMessage} from "element-plus";
import {useRouter} from "vue-router";
import FooterBar from "@/components/FooterBar.vue";
@ -69,6 +69,15 @@ const title = ref('ChatPlus 用户登录');
const username = ref(process.env.VUE_APP_USER);
const password = ref(process.env.VUE_APP_PASS);
const showResetPass = ref(false)
const logo = ref("/images/logo.png")
//
httpGet("/api/config/get?key=system").then(res => {
logo.value = res.data.logo
}).catch(e => {
ElMessage.error("获取系统配置失败:" + e.message)
})
checkSession().then(() => {
if (isMobile()) {

View File

@ -5,7 +5,7 @@
<div class="page-inner">
<div class="contain" v-if="enableRegister">
<div class="logo">
<el-image src="images/logo.png" fit="cover"/>
<el-image :src="logo" fit="cover"/>
</div>
<div class="header">{{ title }}</div>
@ -141,6 +141,7 @@ const enableRegister = ref(true)
const wxImg = ref("/images/wx.png")
const ways = []
const placeholder = ref("用户名:")
const logo = ref("/images/logo.png")
httpGet("/api/config/get?key=system").then(res => {
if (res.data) {
@ -160,6 +161,7 @@ httpGet("/api/config/get?key=system").then(res => {
if (res.data['wechat_card_url'] !== '') {
wxImg.value = res.data['wechat_card_url']
}
logo.value = res.data.logo
}
}).catch(e => {
ElMessage.error("获取系统配置失败:" + e.message)

View File

@ -5,7 +5,7 @@
<van-tabbar route v-model="active" @change="onChange">
<van-tabbar-item to="/mobile/chat" name="home" icon="chat-o">对话</van-tabbar-item>
<van-tabbar-item to="/mobile/mj" name="imageMj" icon="photo-o">绘图</van-tabbar-item>
<van-tabbar-item to="/mobile/image" name="image" icon="photo-o">绘图</van-tabbar-item>
<van-tabbar-item to="/mobile/img-wall" name="apps" icon="apps-o">广场</van-tabbar-item>
<van-tabbar-item to="/mobile/profile" name="profile" icon="user-o">我的</van-tabbar-item>
</van-tabbar>
@ -64,6 +64,11 @@ const onChange = (index) => {
color #f56c6c
}
.van-toast--success {
background #D6FBCC
color #07C160
}
.van-nav-bar {
position fixed
width 100%

View File

@ -0,0 +1,33 @@
<template>
<div class="mobile-image container">
<van-tabs v-model:active="activeName" class="my-tab" animated sticky>
<van-tab title="MidJourney" name="mj">
<image-mj/>
</van-tab>
<van-tab title="Stable-Diffusion" name="sd">
<image-sd/>
</van-tab>
<van-tab title="DALL-E" name="dall">
<van-empty description="功能正在开发中"/>
</van-tab>
</van-tabs>
</div>
</template>
<script setup>
import {ref} from "vue";
import ImageMj from "@/views/mobile/ImageMj.vue";
import ImageSd from "@/views/mobile/ImageSd.vue";
const activeName = ref("sd")
</script>
<style lang="stylus">
.mobile-image {
.my-tab {
.van-tab__panel {
padding 10px
}
}
}
</style>

View File

@ -1,8 +1,5 @@
<template>
<div class="mobile-mj container">
<van-nav-bar :title="title"/>
<div class="content">
<div class="mobile-mj">
<van-form @submit="generate">
<div class="text-line">图片比例</div>
<div class="text-line">
@ -61,26 +58,100 @@
</van-field>
</div>
<div class="text-line">
<van-tabs v-model:active="activeName" @change="tabChange" animated>
<van-tab title="文生图" name="txt2img">
<div class="text-line">
<van-field v-model="params.prompt"
rows="3"
autosize
type="textarea"
placeholder="请在此输入绘画提示词,系统会自动翻译中文提示词,高手请直接输入英文提示词"/>
</div>
</van-tab>
<van-tab title="图生图" name="img2img">
<div class="text-line">
<van-field v-model="params.prompt"
rows="3"
label="提示词"
autosize
type="textarea"
placeholder="请在此输入绘画提示词,系统会自动翻译中文提示词,高手请直接输入英文提示词"/>
</div>
<van-collapse v-model="activeColspan">
<van-collapse-item title="垫图" name="img">
<van-field>
<template #input>
<div class="text-line">
<van-uploader v-model="imgList" :after-read="uploadImg"/>
</div>
<div class="text-line">
<van-field label="垫图权重">
<template #input>
<van-slider v-model.number="params.iw" :max="1" :step="0.01"
@update:model-value="showToast('当前值:' + params.iw)"/>
</template>
</van-field>
</van-collapse-item>
</div>
<div class="tip-text">提示只有于 niji6 v6 模型支持一致性功能如果选择其他模型此功能将会生成失败</div>
<van-cell-group>
<van-field
v-model="params.cref"
center
clearable
label="角色一致性"
placeholder="请输入图片URL或者上传图片"
>
<template #button>
<van-uploader @click="beforeUpload('cref')" :after-read="uploadImg">
<van-button size="mini" type="primary" icon="plus"/>
</van-uploader>
</template>
</van-field>
</van-cell-group>
<van-cell-group>
<van-field
v-model="params.sref"
center
clearable
label="风格一致性"
placeholder="请输入图片URL或者上传图片"
>
<template #button>
<van-uploader @click="beforeUpload('sref')" :after-read="uploadImg">
<van-button size="mini" type="primary" icon="plus"/>
</van-uploader>
</template>
</van-field>
</van-cell-group>
<div class="text-line">
<van-field label="一致性权重">
<template #input>
<van-slider v-model.number="params.cw" :max="100" :step="1"
@update:model-value="showToast('当前值:' + params.cw)"/>
</template>
</van-field>
</div>
</van-tab>
<van-tab title="融图" name="blend">
<div class="tip-text">请上传两张以上的图片最多不超过五张超过五张图片请使用图生图功能</div>
<div class="text-line">
<van-uploader v-model="imgList" :after-read="uploadImg"/>
</div>
</van-tab>
<van-tab title="换脸" name="swapFace">
<div class="tip-text">请上传两张有脸部的图片用左边图片的脸替换右边图片的脸</div>
<div class="text-line">
<van-uploader v-model="imgList" :after-read="uploadImg"/>
</div>
</van-tab>
</van-tabs>
</div>
<div class="text-line">
<van-collapse v-model="activeColspan">
<van-collapse-item title="反向提示词" name="neg_prompt">
<van-field
v-model="params.prompt"
v-model="params.neg_prompt"
rows="3"
autosize
type="textarea"
@ -88,10 +159,14 @@
/>
</van-collapse-item>
</van-collapse>
</div>
<div class="text-line">
<el-tag>绘图消耗{{ mjPower }}算力U/V 操作消耗{{ mjActionPower }}算力当前算力{{ power }}</el-tag>
</div>
<div class="text-line">
<van-button round block type="primary" native-type="submit">
<van-tag type="success">可用算力额度:{{ power }}</van-tag>
立即生成
</van-button>
</div>
@ -196,7 +271,6 @@
</div>
</div>
</div>
</template>
<script setup>
@ -212,13 +286,11 @@ import {
} from "vant";
import {httpGet, httpPost} from "@/utils/http";
import Compressor from "compressorjs";
import {ElMessage} from "element-plus";
import {getSessionId} from "@/store/session";
import {checkSession} from "@/action/session";
import {useRouter} from "vue-router";
import {Delete} from "@element-plus/icons-vue";
const title = ref('MidJourney 绘画')
const activeColspan = ref([""])
const rates = [
@ -232,10 +304,10 @@ const rates = [
const models = [
{text: "MJ-6.0", value: " --v 6", img: "/images/mj/mj-v6.png"},
{text: "MJ-5.2", value: " --v 5.2", img: "/images/mj/mj-v5.2.png"},
{text: "Niji5 原始", value: " --niji 5", img: "/images/mj/mj-niji.png"},
{text: "Niji5", value: " --niji 5", img: "/images/mj/mj-niji.png"},
{text: "Niji5 可爱", value: " --niji 5 --style cute", img: "/images/mj/nj1.jpg"},
{text: "Niji5 风景", value: " --niji 5 --style scenic", img: "/images/mj/nj2.jpg"},
{text: "Niji5 表现力", value: " --niji 5 --style expressive", img: "/images/mj/nj3.jpg"},
{text: "Niji6", value: " --niji 6", img: "/images/mj/nj3.jpg"},
]
const imgList = ref([])
const params = ref({
@ -247,11 +319,14 @@ const params = ref({
seed: 0,
img_arr: [],
raw: false,
weight: 0.25,
iw: 0,
prompt: "",
neg_prompt: "",
tile: false,
quality: 0
quality: 0,
cref: "",
sref: "",
cw: 0,
})
const userId = ref(0)
const router = useRouter()
@ -259,6 +334,7 @@ const runningJobs = ref([])
const finishedJobs = ref([])
const socket = ref(null)
const power = ref(0)
const activeName = ref("txt2img")
onMounted(() => {
checkSession().then(user => {
@ -284,7 +360,7 @@ httpGet("/api/config/get?key=system").then(res => {
mjPower.value = res.data["mj_power"]
mjActionPower.value = res.data["mj_action_power"]
}).catch(e => {
ElMessage.error("获取系统配置失败:" + e.message)
showNotify({type: "danger", message: "获取系统配置失败:" + e.message})
})
const heartbeatHandle = ref(null)
@ -355,7 +431,7 @@ const fetchRunningJobs = (userId) => {
}
runningJobs.value = _jobs
}).catch(e => {
ElMessage.error("获取任务失败:" + e.message)
showNotify({type: "danger", message: "获取任务失败:" + e.message})
})
}
@ -370,6 +446,19 @@ const fetchFinishJobs = (page) => {
httpGet(`/api/mj/jobs?status=1&page=${page}&page_size=${pageSize.value}`).then(res => {
const jobs = res.data
for (let i = 0; i < jobs.length; i++) {
if (jobs[i].progress === -1) {
showNotify({
message: `任务ID${jobs[i]['task_id']} 原因:${jobs[i]['err_msg']}`,
type: 'danger',
})
if (jobs[i].type === 'image') {
power.value += mjPower.value
} else {
power.value += mjActionPower.value
}
continue
}
if (jobs[i]['use_proxy']) {
jobs[i]['thumb_url'] = jobs[i]['img_url'] + '?x-oss-process=image/quality,q_60&format=webp'
} else {
@ -414,6 +503,10 @@ const changeModel = (item) => {
params.value.model = item.value
}
const imgKey = ref("")
const beforeUpload = (key) => {
imgKey.value = key
}
//
const uploadImg = (file) => {
@ -427,6 +520,10 @@ const uploadImg = (file) => {
//
httpPost('/api/upload', formData).then(res => {
file.url = res.data.url
if (imgKey.value !== "") { //
params.value[imgKey.value] = res.data.url
imgKey.value = ''
}
file.status = "done"
}).catch(e => {
file.status = 'failed'
@ -449,10 +546,10 @@ const send = (url, index, item) => {
session_id: getSessionId(),
prompt: item.prompt,
}).then(() => {
ElMessage.success("任务推送成功,请耐心等待任务执行...")
showSuccessToast("任务推送成功,请耐心等待任务执行...")
power.value -= mjActionPower.value
}).catch(e => {
ElMessage.error("任务推送失败:" + e.message)
showFailToast("任务推送失败:" + e.message)
})
}
@ -490,9 +587,9 @@ const removeImage = (item) => {
'此操作将会删除任务和图片,继续操作码?',
}).then(() => {
httpPost("/api/mj/remove", {id: item.id, img_url: item.img_url, user_id: userId.value}).then(() => {
ElMessage.success("任务删除成功")
showSuccessToast("任务删除成功")
}).catch(e => {
ElMessage.error("任务删除失败:" + e.message)
showFailToast("任务删除失败:" + e.message)
})
}).catch(() => {
showToast("您取消了操作")
@ -524,6 +621,15 @@ const showPrompt = (item) => {
const imageView = (item) => {
showImagePreview([item['img_url']]);
}
//
const tabChange = (tab) => {
if (tab === "txt2img" || tab === "img2img") {
params.value.task_type = "image"
} else {
params.value.task_type = tab
}
}
</script>
<style lang="stylus">

View File

@ -0,0 +1,523 @@
<template>
<div class="mobile-sd">
<van-form @submit="generate">
<van-cell-group inset>
<div>
<van-field
v-model="params.sampler"
is-link
readonly
label="采样方法"
placeholder="选择采样方法"
@click="showSamplerPicker = true"
/>
<van-popup v-model:show="showSamplerPicker" position="bottom" teleport="#app">
<van-picker
:columns="samplers"
@cancel="showSamplerPicker = false"
@confirm="samplerConfirm"
/>
</van-popup>
</div>
<van-field label="图片尺寸">
<template #input>
<van-row gutter="20">
<van-col span="12">
<el-input v-model="params.width" size="small" placeholder="宽"/>
</van-col>
<van-col span="12">
<el-input v-model="params.height" size="small" placeholder="高"/>
</van-col>
</van-row>
</template>
</van-field>
<van-field v-model.number="params.steps" label="迭代步数"
placeholder="">
<template #right-icon>
<van-icon name="info-o"
@click="showInfo('值越大则代表细节越多同时也意味着出图速度越慢一般推荐20-30')"/>
</template>
</van-field>
<van-field v-model.number="params.cfg_scale" label="引导系数" placeholder="">
<template #right-icon>
<van-icon name="info-o"
@click="showInfo('提示词引导系数,图像在多大程度上服从提示词,较低值会产生更有创意的结果')"/>
</template>
</van-field>
<van-field v-model.number="params.seed" label="随机因子" placeholder="">
<template #right-icon>
<van-icon name="info-o"
@click="showInfo('随机数种子,相同的种子会得到相同的结果,设置为 -1 则每次随机生成种子')"/>
</template>
</van-field>
<van-field label="高清修复">
<template #input>
<van-switch v-model="params.hd_fix"/>
</template>
</van-field>
<div v-if="params.hd_fix">
<div>
<van-field
v-model="params.hd_scale_alg"
is-link
readonly
label="放大算法"
placeholder="选择放大算法"
@click="showUpscalePicker = true"
/>
<van-popup v-model:show="showUpscalePicker" position="bottom" teleport="#app">
<van-picker
:columns="upscaleAlgArr"
@cancel="showUpscalePicker = false"
@confirm="upscaleConfirm"
/>
</van-popup>
</div>
<van-field v-model.number="params.hd_scale" label="放大倍数"/>
<van-field v-model.number="params.hd_steps" label="迭代步数"/>
<van-field label="重绘幅度">
<template #input>
<van-slider v-model.number="params.hd_redraw_rate" :max="1" :step="0.1"
@update:model-value="showToast('当前值:' + params.hd_redraw_rate)"/>
</template>
<template #right-icon>
<van-icon name="info-o"
@click="showInfo('决定算法对图像内容的影响程度,较大的值将得到越有创意的图像')"/>
</template>
</van-field>
</div>
<van-field
v-model="params.prompt"
rows="3"
autosize
type="textarea"
placeholder="请在此输入绘画提示词,系统会自动翻译中文提示词,高手请直接输入英文提示词"
/>
<van-collapse v-model="activeColspan">
<van-collapse-item title="反向提示词" name="neg_prompt">
<van-field
v-model="params.neg_prompt"
rows="3"
autosize
type="textarea"
placeholder="不想出现在图片上的元素(例如:树,建筑)"
/>
</van-collapse-item>
</van-collapse>
<div class="text-line pt-6">
<el-tag>绘图消耗{{ sdPower }}算力当前算力{{ power }}</el-tag>
</div>
<div class="text-line">
<van-button round block type="primary" native-type="submit">
立即生成
</van-button>
</div>
</van-cell-group>
</van-form>
<h3>任务列表</h3>
<div class="running-job-list">
<van-empty v-if="runningJobs.length ===0"
image="https://fastly.jsdelivr.net/npm/@vant/assets/custom-empty-image.png"
image-size="80"
description="暂无记录"
/>
<van-grid :gutter="10" :column-num="3" v-else>
<van-grid-item v-for="item in runningJobs">
<div v-if="item.progress > 0">
<van-image :src="item['img_url']">
<template v-slot:error>加载失败</template>
</van-image>
<div class="progress">
<van-circle
v-model:current-rate="item.progress"
:rate="item.progress"
:speed="100"
:text="item.progress+'%'"
:stroke-width="60"
size="90px"
/>
</div>
</div>
<div v-else class="task-in-queue">
<span class="icon"><i class="iconfont icon-quick-start"></i></span>
<span class="text">排队中</span>
</div>
</van-grid-item>
</van-grid>
</div>
<h3>创作记录</h3>
<div class="finish-job-list">
<van-empty v-if="finishedJobs.length ===0"
image="https://fastly.jsdelivr.net/npm/@vant/assets/custom-empty-image.png"
image-size="80"
description="暂无记录"
/>
<van-list v-else
v-model:error="error"
v-model:loading="loading"
:finished="finished"
error-text="请求失败,点击重新加载"
finished-text="没有更多了"
@load="onLoad"
>
<van-grid :gutter="10" :column-num="2">
<van-grid-item v-for="item in finishedJobs">
<div class="job-item">
<van-image
:src="item['img_url']"
:class="item['can_opt'] ? '' : 'upscale'"
lazy-load
@click="imageView(item)"
fit="cover">
<template v-slot:loading>
<van-loading type="spinner" size="20"/>
</template>
</van-image>
<div class="remove">
<el-button type="danger" :icon="Delete" @click="removeImage($event, item)" circle/>
<el-button type="warning" v-if="item.publish" @click="publishImage($event,item, false)"
circle>
<i class="iconfont icon-cancel-share"></i>
</el-button>
<el-button type="success" v-else @click="publishImage($event, item, true)" circle>
<i class="iconfont icon-share-bold"></i>
</el-button>
<el-button type="primary" @click="showTask(item)" circle>
<i class="iconfont icon-prompt"></i>
</el-button>
</div>
</div>
</van-grid-item>
</van-grid>
</van-list>
</div>
</div>
</template>
<script setup>
import {onMounted, onUnmounted, ref} from "vue"
import {Delete} from "@element-plus/icons-vue";
import {httpGet, httpPost} from "@/utils/http";
import Clipboard from "clipboard";
import {checkSession} from "@/action/session";
import {useRouter} from "vue-router";
import {getSessionId} from "@/store/session";
import {
showConfirmDialog, showDialog,
showFailToast,
showImagePreview,
showNotify,
showSuccessToast,
showToast
} from "vant";
const listBoxHeight = ref(window.innerHeight - 40)
const mjBoxHeight = ref(window.innerHeight - 150)
const showTaskDialog = ref(false)
const item = ref({})
const showLoginDialog = ref(false)
const isLogin = ref(false)
const activeColspan = ref([""])
window.onresize = () => {
listBoxHeight.value = window.innerHeight - 40
mjBoxHeight.value = window.innerHeight - 150
}
const samplers = ref([
{text: "Euler a", value: "Euler a"},
{text: "DPM++ 2S a Karras", value: "DPM++ 2S a Karras"},
{text: "DPM++ 2M Karras", value: "DPM++ 2M Karras"},
{text: "DPM++ 2M SDE Karras", value: "DPM++ 2M SDE Karras"},
{text: "DPM++ 2M Karras", value: "DPM++ 2M Karras"},
{text: "DPM++ 3M SDE Karras", value: "DPM++ 3M SDE Karras"},
])
const showSamplerPicker = ref(false)
const upscaleAlgArr = ref([
{text: "Latent", value: "Latent"},
{text: "ESRGAN_4x", value: "ESRGAN_4x"},
{text: "ESRGAN 4x+", value: "ESRGAN 4x+"},
{text: "SwinIR_4x", value: "SwinIR_4x"},
{text: "LDSR", value: "LDSR"},
])
const showUpscalePicker = ref(false)
const params = ref({
width: 1024,
height: 1024,
sampler: samplers.value[0].value,
seed: -1,
steps: 20,
cfg_scale: 7,
hd_fix: false,
hd_redraw_rate: 0.7,
hd_scale: 2,
hd_scale_alg: upscaleAlgArr.value[0].value,
hd_steps: 0,
prompt: "",
neg_prompt: "nsfw, paintings,low quality,easynegative,ng_deepnegative ,lowres,bad anatomy,bad hands,bad feet",
})
const runningJobs = ref([])
const finishedJobs = ref([])
const router = useRouter()
//
const _params = router.currentRoute.value.params["copyParams"]
if (_params) {
params.value = JSON.parse(_params)
}
const power = ref(0)
const sdPower = ref(0) // SD
const socket = ref(null)
const userId = ref(0)
const heartbeatHandle = ref(null)
const connect = () => {
let host = process.env.VUE_APP_WS_HOST
if (host === '') {
if (location.protocol === 'https:') {
host = 'wss://' + location.host;
} else {
host = 'ws://' + location.host;
}
}
//
const sendHeartbeat = () => {
clearTimeout(heartbeatHandle.value)
new Promise((resolve, reject) => {
if (socket.value !== null) {
socket.value.send(JSON.stringify({type: "heartbeat", content: "ping"}))
}
resolve("success")
}).then(() => {
heartbeatHandle.value = setTimeout(() => sendHeartbeat(), 5000)
});
}
const _socket = new WebSocket(host + `/api/sd/client?user_id=${userId.value}`);
_socket.addEventListener('open', () => {
socket.value = _socket;
//
sendHeartbeat()
});
_socket.addEventListener('message', event => {
if (event.data instanceof Blob) {
fetchRunningJobs()
finished.value = false
page.value = 1
fetchFinishJobs(page.value)
}
});
_socket.addEventListener('close', () => {
if (socket.value !== null) {
connect()
}
});
}
const clipboard = ref(null)
onMounted(() => {
initData()
clipboard.value = new Clipboard('.copy-prompt-sd');
clipboard.value.on('success', () => {
showNotify({type: "success", message: "复制成功!"});
})
clipboard.value.on('error', () => {
showNotify({type: "danger", message: '复制失败!'});
})
httpGet("/api/config/get?key=system").then(res => {
sdPower.value = res.data["sd_power"]
}).catch(e => {
showNotify({type: "danger", message: "获取系统配置失败:" + e.message})
})
})
onUnmounted(() => {
clipboard.value.destroy()
socket.value = null
})
const initData = () => {
checkSession().then(user => {
power.value = user['power']
userId.value = user.id
isLogin.value = true
fetchRunningJobs()
fetchFinishJobs(1)
connect()
}).catch(() => {
loading.value = false
});
}
const fetchRunningJobs = () => {
//
httpGet(`/api/sd/jobs?status=0`).then(res => {
const jobs = res.data
const _jobs = []
for (let i = 0; i < jobs.length; i++) {
if (jobs[i].progress === -1) {
showNotify({
message: `任务ID${jobs[i]['task_id']} 原因:${jobs[i]['err_msg']}`,
type: 'danger',
})
power.value += sdPower.value
continue
}
_jobs.push(jobs[i])
}
runningJobs.value = _jobs
}).catch(e => {
showNotify({type: "danger", message: "获取任务失败:" + e.message})
})
}
const loading = ref(false)
const finished = ref(false)
const error = ref(false)
const page = ref(0)
const pageSize = ref(10)
//
const fetchFinishJobs = (page) => {
loading.value = true
httpGet(`/api/sd/jobs?status=1&page=${page}&page_size=${pageSize.value}`).then(res => {
if (res.data.length < pageSize.value) {
finished.value = true
}
if (page === 1) {
finishedJobs.value = res.data
} else {
finishedJobs.value = finishedJobs.value.concat(res.data)
}
loading.value = false
}).catch(e => {
loading.value = false
showNotify({type: "danger", message: "获取任务失败:" + e.message})
})
}
const onLoad = () => {
page.value += 1
fetchFinishJobs(page.value)
}
//
const promptRef = ref(null)
const generate = () => {
if (params.value.prompt === '') {
promptRef.value.focus()
return showToast("请输入绘画提示词!")
}
if (!isLogin.value) {
showLoginDialog.value = true
return
}
if (params.value.seed === '') {
params.value.seed = -1
}
params.value.session_id = getSessionId()
httpPost("/api/sd/image", params.value).then(() => {
showSuccessToast("绘画任务推送成功,请耐心等待任务执行...")
power.value -= sdPower.value
}).catch(e => {
showFailToast("任务推送失败:" + e.message)
})
}
const showTask = (row) => {
item.value = row
showTaskDialog.value = true
}
const copyParams = (row) => {
params.value = row.params
showTaskDialog.value = false
}
const removeImage = (event, item) => {
event.stopPropagation()
showConfirmDialog({
title: '标题',
message:
'此操作将会删除任务和图片,继续操作码?',
}).then(() => {
httpPost("/api/sd/remove", {id: item.id, img_url: item.img_url, user_id: userId.value}).then(() => {
showSuccessToast("任务删除成功")
}).catch(e => {
showFailToast("任务删除失败:" + e.message)
})
}).catch(() => {
showToast("您取消了操作")
});
}
//
const publishImage = (event, item, action) => {
event.stopPropagation()
let text = "图片发布"
if (action === false) {
text = "取消发布"
}
httpPost("/api/sd/publish", {id: item.id, action: action}).then(() => {
showSuccessToast(text + "成功")
item.publish = action
}).catch(e => {
showFailToast(text + "失败:" + e.message)
})
}
const imageView = (item) => {
showImagePreview([item['img_url']]);
}
const samplerConfirm = (item) => {
params.value.sampler = item.selectedOptions[0].text;
showSamplerPicker.value = false
}
const upscaleConfirm = (item) => {
params.value.hd_scale_alg = item.selectedOptions[0].text;
showUpscalePicker.value = false
}
const showInfo = (message) => {
showDialog({
title: "参数说明",
message: message,
}).then(() => {
// on close
});
}
</script>
<style lang="stylus">
@import "@/assets/css/mobile/image-sd.styl"
</style>