feat: the power log page is ready

This commit is contained in:
RockYang 2024-03-20 14:14:30 +08:00
parent ca83f139f7
commit e11c0a3633
19 changed files with 443 additions and 212 deletions

View File

@ -56,6 +56,7 @@ type ChatSession struct {
type ChatModel struct { type ChatModel struct {
Id uint `json:"id"` Id uint `json:"id"`
Platform Platform `json:"platform"` Platform Platform `json:"platform"`
Name string `json:"name"`
Value string `json:"value"` Value string `json:"value"`
Power int `json:"power"` Power int `json:"power"`
MaxTokens int `json:"max_tokens"` // 最大响应长度 MaxTokens int `json:"max_tokens"` // 最大响应长度
@ -86,6 +87,21 @@ const (
PowerReward = PowerType(5) // 众筹 PowerReward = PowerType(5) // 众筹
) )
func (t PowerType) String() string {
switch t {
case PowerRecharge:
return "充值"
case PowerConsume:
return "消费"
case PowerRefund:
return "退款"
case PowerReward:
return "众筹"
}
return "其他"
}
type PowerMark int type PowerMark int
const ( const (

View File

@ -22,6 +22,7 @@ func NewChatRoleHandler(app *core.AppServer, db *gorm.DB) *ChatRoleHandler {
// List 获取用户聊天应用列表 // List 获取用户聊天应用列表
func (h *ChatRoleHandler) List(c *gin.Context) { func (h *ChatRoleHandler) List(c *gin.Context) {
all := h.GetBool(c, "all")
userId := h.GetLoginUserId(c) userId := h.GetLoginUserId(c)
var roles []model.ChatRole var roles []model.ChatRole
res := h.DB.Where("enable", true).Order("sort_num ASC").Find(&roles) res := h.DB.Where("enable", true).Order("sort_num ASC").Find(&roles)
@ -31,7 +32,7 @@ func (h *ChatRoleHandler) List(c *gin.Context) {
} }
// 获取所有角色 // 获取所有角色
if userId == 0 { if userId == 0 || all {
// 转成 vo // 转成 vo
var roleVos = make([]vo.ChatRole, 0) var roleVos = make([]vo.ChatRole, 0)
for _, r := range roles { for _, r := range roles {

View File

@ -105,6 +105,7 @@ func (h *ChatHandler) ChatHandle(c *gin.Context) {
session.ChatId = chatId session.ChatId = chatId
session.Model = types.ChatModel{ session.Model = types.ChatModel{
Id: chatModel.Id, Id: chatModel.Id,
Name: chatModel.Name,
Value: chatModel.Value, Value: chatModel.Value,
Power: chatModel.Power, Power: chatModel.Power,
MaxTokens: chatModel.MaxTokens, MaxTokens: chatModel.MaxTokens,
@ -535,7 +536,7 @@ func (h *ChatHandler) subUserPower(userVo vo.User, session *types.ChatSession, p
Mark: types.PowerSub, Mark: types.PowerSub,
Balance: userVo.Power - power, Balance: userVo.Power - power,
Model: session.Model.Value, Model: session.Model.Value,
Remark: fmt.Sprintf("提问长度:%d回复长度%d", promptTokens, replyTokens), Remark: fmt.Sprintf("模型名称:%s, 提问长度:%d回复长度%d", session.Model.Name, promptTokens, replyTokens),
CreatedAt: time.Now(), CreatedAt: time.Now(),
}) })
} }

View File

@ -0,0 +1,65 @@
package handler
import (
"chatplus/core"
"chatplus/core/types"
"chatplus/store/model"
"chatplus/store/vo"
"chatplus/utils"
"chatplus/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type PowerLogHandler struct {
BaseHandler
}
func NewPowerLogHandler(app *core.AppServer, db *gorm.DB) *PowerLogHandler {
return &PowerLogHandler{BaseHandler: BaseHandler{App: app, DB: db}}
}
func (h *PowerLogHandler) List(c *gin.Context) {
var data struct {
Model string `json:"model"`
Date []string `json:"date"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
session := h.DB.Session(&gorm.Session{})
if data.Model != "" {
session = session.Where("model", data.Model)
}
if len(data.Date) == 2 {
start := data.Date[0] + " 00:00:00"
end := data.Date[1] + " 00:00:00"
session = session.Where("created_at >= ? AND created_at <= ?", start, end)
}
var total int64
session.Model(&model.PowerLog{}).Count(&total)
var items []model.PowerLog
var list = make([]vo.PowerLog, 0)
offset := (data.Page - 1) * data.PageSize
res := session.Debug().Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&items)
if res.Error == nil {
for _, item := range items {
var log vo.PowerLog
err := utils.CopyObject(item, &log)
if err != nil {
continue
}
log.Id = item.Id
log.CreatedAt = item.CreatedAt.Unix()
log.TypeStr = item.Type.String()
list = append(list, log)
}
}
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, list))
}

View File

@ -108,6 +108,20 @@ func (h *UserHandler) Register(c *gin.Context) {
h.DB.Model(&model.InviteCode{}).Where("code = ?", data.InviteCode).UpdateColumn("reg_num", gorm.Expr("reg_num + ?", 1)) h.DB.Model(&model.InviteCode{}).Where("code = ?", data.InviteCode).UpdateColumn("reg_num", gorm.Expr("reg_num + ?", 1))
if h.App.SysConfig.InvitePower > 0 { if h.App.SysConfig.InvitePower > 0 {
h.DB.Model(&model.User{}).Where("id = ?", inviteCode.UserId).UpdateColumn("power", gorm.Expr("power + ?", h.App.SysConfig.InvitePower)) h.DB.Model(&model.User{}).Where("id = ?", inviteCode.UserId).UpdateColumn("power", gorm.Expr("power + ?", h.App.SysConfig.InvitePower))
// 记录邀请算力充值日志
var inviter model.User
h.DB.Where("id", inviteCode.UserId).First(&inviter)
h.DB.Create(&model.PowerLog{
UserId: inviter.Id,
Username: inviter.Username,
Type: types.PowerInvite,
Amount: h.App.SysConfig.InvitePower,
Balance: inviter.Power,
Mark: types.PowerAdd,
Model: "",
Remark: fmt.Sprintf("邀请用户注册奖励,金额:%d邀请码%s新用户%s", h.App.SysConfig.InvitePower, inviteCode.Code, user.Username),
CreatedAt: time.Now(),
})
} }
// 添加邀请记录 // 添加邀请记录

View File

@ -126,6 +126,7 @@ func main() {
fx.Provide(handler.NewOrderHandler), fx.Provide(handler.NewOrderHandler),
fx.Provide(handler.NewProductHandler), fx.Provide(handler.NewProductHandler),
fx.Provide(handler.NewConfigHandler), fx.Provide(handler.NewConfigHandler),
fx.Provide(handler.NewPowerLogHandler),
fx.Provide(admin.NewConfigHandler), fx.Provide(admin.NewConfigHandler),
fx.Provide(admin.NewAdminHandler), fx.Provide(admin.NewAdminHandler),
@ -401,8 +402,9 @@ func main() {
group.GET("remove", h.RemoveChat) group.GET("remove", h.RemoveChat)
group.GET("message/remove", h.RemoveMessage) group.GET("message/remove", h.RemoveMessage)
}), }),
fx.Provide(handler.NewTestHandler), fx.Invoke(func(s *core.AppServer, h *handler.PowerLogHandler) {
fx.Invoke(func(s *core.AppServer, h *handler.TestHandler) { group := s.Engine.Group("/api/powerLog/")
group.POST("list", h.List)
}), }),
fx.Invoke(func(s *core.AppServer, db *gorm.DB) { fx.Invoke(func(s *core.AppServer, db *gorm.DB) {
err := s.Run(db) err := s.Run(db)

View File

@ -6,9 +6,10 @@ type PowerLog struct {
Id uint `json:"id"` Id uint `json:"id"`
UserId uint `json:"user_id"` UserId uint `json:"user_id"`
Username string `json:"username"` Username string `json:"username"`
Type types.PowerType `json:"name"` Type types.PowerType `json:"type"`
TypeStr string `json:"type_str"`
Amount int `json:"amount"` Amount int `json:"amount"`
Mark types.PowerMark `json:"fund_type"` Mark types.PowerMark `json:"mark"`
Balance int `json:"balance"` Balance int `json:"balance"`
Model string `json:"model"` Model string `json:"model"`
Remark string `json:"remark"` Remark string `json:"remark"`

View File

@ -1,5 +1,6 @@
.page-mj { .page-mj {
background-color: #282c34; background-color: #282c34;
height: 100vh;
} }
.page-mj .inner { .page-mj .inner {
display: flex; display: flex;
@ -396,6 +397,10 @@
width: auto; width: auto;
height: auto; height: auto;
} }
.page-mj .inner .task-list-box .no-more-data {
text-align: center;
padding: 20px;
}
.mj-list-item-prompt .el-icon { .mj-list-item-prompt .el-icon {
margin-left: 10px; margin-left: 10px;
cursor: pointer; cursor: pointer;

View File

@ -1,5 +1,6 @@
.page-mj { .page-mj {
background-color: #282c34; background-color: #282c34;
height 100vh
.inner { .inner {
display: flex; display: flex;

View File

@ -281,6 +281,10 @@
width: auto; width: auto;
height: auto; height: auto;
} }
.page-sd .inner .task-list-box .no-more-data {
text-align: center;
padding: 20px;
}
.page-sd .el-overlay-dialog .el-dialog { .page-sd .el-overlay-dialog .el-dialog {
background-color: #1a1b1e; background-color: #1a1b1e;
} }

View File

@ -6,107 +6,87 @@ body,
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
} }
body { body {
font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif; font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
} }
.admin-home a { .admin-home a {
text-decoration: none; text-decoration: none;
} }
.admin-home .content-box { .admin-home .content-box {
position: absolute; position: absolute;
left: 250px; left: 250px;
right: 0; right: 0;
top: 0; top: 0;
bottom: 0; bottom: 0;
/*padding-bottom: 30px;*/ padding-bottom: 30px;
-webkit-transition: left 0.3s ease-in-out; -webkit-transition: left 0.3s ease-in-out;
transition: left 0.3s ease-in-out; transition: left 0.3s ease-in-out;
background: #f0f0f0; background: #f0f0f0;
overflow-y: scroll;
} }
.admin-home .content-box .content { .admin-home .content-box .content {
width: auto; width: auto;
padding: 10px; height: 100vh;
overflow-y: scroll;
box-sizing: border-box; box-sizing: border-box;
/*BaseForm*/ /*BaseForm*/
} }
.admin-home .content-box .content .container { .admin-home .content-box .content .container {
padding: 30px; padding: 30px;
background: #fff; background: #fff;
border: 1px solid #ddd; border: 1px solid #ddd;
border-radius: 5px; border-radius: 5px;
} }
.admin-home .content-box .content .container .handle-box { .admin-home .content-box .content .container .handle-box {
margin-bottom: 20px; margin-bottom: 20px;
} }
.admin-home .content-box .content .crumbs { .admin-home .content-box .content .crumbs {
margin: 10px 0; margin: 10px 0;
} }
.admin-home .content-box .content .el-table th { .admin-home .content-box .content .el-table th {
background-color: #f5f7fa !important; background-color: #f5f7fa !important;
} }
.admin-home .content-box .content .pagination { .admin-home .content-box .content .pagination {
margin: 20px 0; margin: 20px 0;
display: flex; display: flex;
justify-content: center; justify-content: center;
width: 100%; width: 100%;
} }
.admin-home .content-box .content .plugins-tips { .admin-home .content-box .content .plugins-tips {
padding: 20px 10px; padding: 20px 10px;
margin-bottom: 20px; margin-bottom: 20px;
} }
.admin-home .content-box .content .el-button + .el-tooltip { .admin-home .content-box .content .el-button + .el-tooltip {
margin-left: 10px; margin-left: 10px;
} }
.admin-home .content-box .content .el-table tr:hover { .admin-home .content-box .content .el-table tr:hover {
background: #f6faff; background: #f6faff;
} }
.admin-home .content-box .content .mgb20 { .admin-home .content-box .content .mgb20 {
margin-bottom: 20px; margin-bottom: 20px;
} }
.admin-home .content-box .content .move-enter-active, .admin-home .content-box .content .move-enter-active,
.admin-home .content-box .content .move-leave-active { .admin-home .content-box .content .move-leave-active {
transition: opacity 0.1s ease; transition: opacity 0.1s ease;
} }
.admin-home .content-box .content .move-enter-from, .admin-home .content-box .content .move-enter-from,
.admin-home .content-box .content .move-leave-to { .admin-home .content-box .content .move-leave-to {
opacity: 0; opacity: 0;
} }
.admin-home .content-box .content .form-box { .admin-home .content-box .content .form-box {
width: 600px; width: 600px;
} }
.admin-home .content-box .content .form-box .line { .admin-home .content-box .content .form-box .line {
text-align: center; text-align: center;
} }
.admin-home .content-box .content .el-time-panel__content::after, .admin-home .content-box .content .el-time-panel__content::after,
.admin-home .content-box .content .el-time-panel__content::before { .admin-home .content-box .content .el-time-panel__content::before {
margin-top: -7px; margin-top: -7px;
} }
.admin-home .content-box .content .el-time-spinner__wrapper .el-scrollbar__wrap:not(.el-scrollbar__wrap--hidden-default) { .admin-home .content-box .content .el-time-spinner__wrapper .el-scrollbar__wrap:not(.el-scrollbar__wrap--hidden-default) {
padding-bottom: 0; padding-bottom: 0;
} }
.admin-home .content-box .content [class*=" el-icon-"], .admin-home .content-box .content [class*=" el-icon-"],
.admin-home .content-box .content [class^=el-icon-] { .admin-home .content-box .content [class^=el-icon-] {
speak: none; speak: none;
@ -120,7 +100,6 @@ body {
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
.admin-home .content-box .content .el-sub-menu [class^=el-icon-] { .admin-home .content-box .content .el-sub-menu [class^=el-icon-] {
vertical-align: middle; vertical-align: middle;
margin-right: 5px; margin-right: 5px;
@ -128,11 +107,9 @@ body {
text-align: center; text-align: center;
font-size: 18px; font-size: 18px;
} }
.admin-home .content-box .content [hidden] { .admin-home .content-box .content [hidden] {
display: none !important; display: none !important;
} }
.admin-home .content-collapse { .admin-home .content-collapse {
left: 65px; left: 65px;
} }

View File

@ -36,8 +36,7 @@ body {
.content { .content {
width: auto; width: auto;
height: 100%; height: 100vh;
padding: 10px;
overflow-y: scroll; overflow-y: scroll;
box-sizing: border-box; box-sizing: border-box;

View File

@ -292,4 +292,9 @@
} }
} }
} }
.no-more-data {
text-align center
padding 20px
}
} }

View File

@ -69,6 +69,7 @@
<el-input placeholder="手机号码" <el-input placeholder="手机号码"
size="large" size="large"
v-model="data.username" v-model="data.username"
maxlength="11"
autocomplete="off"> autocomplete="off">
<template #prefix> <template #prefix>
<el-icon> <el-icon>

View File

@ -57,7 +57,6 @@ const changeNav = (item) => {
@import '@/assets/iconfont/iconfont.css'; @import '@/assets/iconfont/iconfont.css';
.home { .home {
display: flex; display: flex;
background-color: #25272D;
height 100vh height 100vh
width 100% width 100%
@ -67,6 +66,7 @@ const changeNav = (item) => {
width 70px width 70px
padding 10px 6px padding 10px 6px
border-right: 1px solid #3c3c3c border-right: 1px solid #3c3c3c
background-color: #25272D
.logo { .logo {
display flex display flex

View File

@ -391,8 +391,9 @@
</div> </div>
<h2>创作记录</h2> <h2>创作记录</h2>
<div class="finish-job-list" v-loading="loading" element-loading-background="rgba(0, 0, 0, 0.7)"> <div class="finish-job-list" v-loading="loading" element-loading-background="rgba(255, 255, 255, 0.5)">
<ItemList :items="finishedJobs" v-if="finishedJobs.length > 0" :width="240" :gap="16"> <div v-if="finishedJobs.length > 0">
<ItemList :items="finishedJobs" :width="240" :gap="16">
<template #default="scope"> <template #default="scope">
<div class="job-item"> <div class="job-item">
<el-image <el-image
@ -472,7 +473,11 @@
</div> </div>
</template> </template>
</ItemList> </ItemList>
<div class="no-more-data" v-if="isOver">
<span>没有更多数据了</span>
<i class="iconfont icon-face"></i>
</div>
</div>
<el-empty :image-size="100" v-else/> <el-empty :image-size="100" v-else/>
</div> <!-- end finish job list--> </div> <!-- end finish job list-->
</div> </div>

View File

@ -350,8 +350,9 @@
<el-empty :image-size="100" v-else/> <el-empty :image-size="100" v-else/>
</div> </div>
<h2>创作记录</h2> <h2>创作记录</h2>
<div class="finish-job-list" v-loading="loading" element-loading-background="rgba(0, 0, 0, 0.7)"> <div class="finish-job-list" v-loading="loading" element-loading-background="rgba(255, 255, 255, 0.5)">
<ItemList :items="finishedJobs" v-if="finishedJobs.length > 0" :width="240" :gap="16"> <div v-if="finishedJobs.length > 0">
<ItemList :items="finishedJobs" :width="240" :gap="16">
<template #default="scope"> <template #default="scope">
<div class="job-item animate" @click="showTask(scope.item)"> <div class="job-item animate" @click="showTask(scope.item)">
<el-image <el-image
@ -387,6 +388,12 @@
</div> </div>
</template> </template>
</ItemList> </ItemList>
<div class="no-more-data" v-if="isOver">
<span>没有更多数据了</span>
<i class="iconfont icon-face"></i>
</div>
</div>
<el-empty :image-size="100" v-else/> <el-empty :image-size="100" v-else/>
</div> <!-- end finish job list--> </div> <!-- end finish job list-->
</div> </div>

View File

@ -1,31 +1,149 @@
<template> <template>
<div class="power-log" :style="{ height: winHeight + 'px' }"> <div class="power-log" v-loading="loading">
<div class="inner"> <div class="inner">
<h2>消费日志</h2> <h2>消费日志</h2>
<div class="list-box" :style="{height: listBoxHeight + 'px'}">
<div class="handle-box">
<el-input v-model="query.model" placeholder="模型" class="handle-input mr10" clearable></el-input>
<el-date-picker
v-model="query.date"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="margin: 0 10px;width: 200px; position: relative;top:3px;"
/>
<el-button type="primary" :icon="Search" @click="fetchData">搜索</el-button>
</div>
<el-row v-if="items.length > 0">
<el-table :data="items" :row-key="row => row.id" table-layout="auto" border>
<el-table-column prop="username" label="用户"/>
<el-table-column prop="model" label="模型"/>
<el-table-column prop="type" label="类型">
<template #default="scope">
<el-tag size="small" :type="tagColors[scope.row.type]">{{ scope.row.type_str }}</el-tag>
</template>
</el-table-column>
<el-table-column label="数额">
<template #default="scope">
<div>
<el-text type="success" v-if="scope.row.mark === 1">+{{ scope.row.amount }}</el-text>
<el-text type="danger" v-if="scope.row.mark === 0">-{{ scope.row.amount }}</el-text>
</div>
</template>
</el-table-column>
<el-table-column prop="balance" label="余额"/>
<el-table-column label="发生时间">
<template #default="scope">
<span>{{ dateFormat(scope.row['created_at']) }}</span>
</template>
</el-table-column>
<el-table-column prop="remark" label="备注"/>
</el-table>
<div class="pagination">
<el-pagination v-if="total > 0" background
layout="total,prev, pager, next"
:hide-on-single-page="true"
v-model:current-page="page"
v-model:page-size="pageSize"
@current-change="fetchData()"
:total="total"/>
</div>
</el-row>
<el-empty :image-size="100" v-else/>
</div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import {ref} from "vue" import {onMounted, ref} from "vue"
import {dateFormat} from "@/utils/libs";
import {Back, DocumentCopy, Search} from "@element-plus/icons-vue";
import Clipboard from "clipboard";
import {ElMessage} from "element-plus";
import {httpPost} from "@/utils/http";
const items = ref([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(20)
const loading = ref(false)
const listBoxHeight = window.innerHeight - 117
const query = ref({
model: "",
date: []
})
const tagColors = ref(["", "success", "primary", "danger", "info", "warning"])
onMounted(() => {
fetchData()
const clipboard = new Clipboard('.copy-order-no');
clipboard.on('success', () => {
ElMessage.success("复制成功!");
})
clipboard.on('error', () => {
ElMessage.error('复制失败!');
})
})
//
const fetchData = () => {
loading.value = true
httpPost('/api/powerLog/list', {
model: query.value.model,
date: query.value.date,
page: page.value,
page_size: pageSize.value
}).then((res) => {
if (res.data) {
items.value = res.data.items
total.value = res.data.total
page.value = res.data.page
pageSize.value = res.data.page_size
}
loading.value = false
}).catch(e => {
loading.value = false
ElMessage.error("获取数据失败:" + e.message);
})
}
const winHeight = ref(window.innerHeight)
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>
.power-log { .power-log {
display: flex; background-color #ffffff
justify-content: center;
background-color: #282c34;
color #ffffff
.inner { .inner {
display flex
justify-content left
width 100%
padding 0 20px 20px 20px padding 0 20px 20px 20px
.list-box {
.handle-box {
padding 20px 0
.el-input {
max-width 150px
} }
.el-date-editor {
max-width 200px;
}
}
.pagination {
display flex
justify-content center
width 100%
padding 20px
}
}
}
} }
</style> </style>

View File

@ -44,6 +44,15 @@
</div> </div>
</div> </div>
</el-form-item> </el-form-item>
<el-form-item label="MidJourney算力" prop="mj_power">
<el-input v-model.number="system['mj_power']" placeholder="使用MidJourney画一张图消耗算力"/>
</el-form-item>
<el-form-item label="Stable-Diffusion算力" prop="sd_power">
<el-input v-model.number="system['sd_power']" placeholder="使用Stable-Diffusion画一张图消耗算力"/>
</el-form-item>
<el-form-item label="DALL-E-3算力" prop="dall_power">
<el-input v-model.number="system['dall_power']" placeholder="使用DALL-E-3画一张图消耗算力"/>
</el-form-item>
<el-form-item label="开放注册" prop="enabled_register"> <el-form-item label="开放注册" prop="enabled_register">
<el-switch v-model="system['enabled_register']"/> <el-switch v-model="system['enabled_register']"/>
<el-tooltip <el-tooltip