image task list page for admin console is ready

This commit is contained in:
RockYang 2024-10-09 18:17:44 +08:00
parent 52e40daf23
commit ba20717a09
8 changed files with 728 additions and 25 deletions

View File

@ -1,4 +1,8 @@
# 更新日志
## v4.1.6
* 功能优化优化MysQL容器配置文档解决MysQL容器资源占用过高问题
* 功能新增管理后台增加AI绘图任务管理可在管理后台浏览和删除用户的绘图任务
## v4.1.5
* 功能优化:重构 websocket 组件,减少 websocket 连接数,全站共享一个 websocket 连接
* Bug修复兼容手机端原生微信支付和支付宝支付渠道

View File

@ -0,0 +1,174 @@
package admin
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
// * Copyright 2023 The Geek-AI Authors. All rights reserved.
// * Use of this source code is governed by a Apache-2.0 license
// * that can be found in the LICENSE file.
// * @Author yangjian102621@163.com
// * +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import (
"geekai/core"
"geekai/core/types"
"geekai/handler"
"geekai/store/model"
"geekai/store/vo"
"geekai/utils"
"geekai/utils/resp"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type ImageHandler struct {
handler.BaseHandler
}
func NewImageHandler(app *core.AppServer, db *gorm.DB) *ImageHandler {
return &ImageHandler{BaseHandler: handler.BaseHandler{App: app, DB: db}}
}
type query struct {
Prompt string `json:"prompt"`
Username string `json:"username"`
CreatedAt []string `json:"created_time"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
// MjList Midjourney 任务列表
func (h *ImageHandler) MjList(c *gin.Context) {
var data query
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
session := h.DB.Session(&gorm.Session{})
if data.Username != "" {
var user model.User
err := h.DB.Where("username", data.Username).First(&user).Error
if err == nil {
session = session.Where("user_id", user.Id)
}
}
if data.Prompt != "" {
session = session.Where("prompt LIKE ?", "%"+data.Prompt+"%")
}
if len(data.CreatedAt) == 2 {
start := utils.Str2stamp(data.CreatedAt[0] + " 00:00:00")
end := utils.Str2stamp(data.CreatedAt[1] + " 00:00:00")
session = session.Where("created_at >= ? AND created_at <= ?", start, end)
}
var total int64
session.Model(&model.MidJourneyJob{}).Count(&total)
var list []model.MidJourneyJob
var items = make([]vo.MidJourneyJob, 0)
offset := (data.Page - 1) * data.PageSize
err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error
if err == nil {
// 填充数据
for _, item := range list {
var job vo.MidJourneyJob
err = utils.CopyObject(item, &job)
if err != nil {
continue
}
job.CreatedAt = item.CreatedAt.Unix()
items = append(items, job)
}
}
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items))
}
// SdList Stable Diffusion 任务列表
func (h *ImageHandler) SdList(c *gin.Context) {
var data query
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
session := h.DB.Session(&gorm.Session{})
if data.Username != "" {
var user model.User
err := h.DB.Where("username", data.Username).First(&user).Error
if err == nil {
session = session.Where("user_id", user.Id)
}
}
if data.Prompt != "" {
session = session.Where("prompt LIKE ?", "%"+data.Prompt+"%")
}
if len(data.CreatedAt) == 2 {
start := utils.Str2stamp(data.CreatedAt[0] + " 00:00:00")
end := utils.Str2stamp(data.CreatedAt[1] + " 00:00:00")
session = session.Where("created_at >= ? AND created_at <= ?", start, end)
}
var total int64
session.Model(&model.SdJob{}).Count(&total)
var list []model.SdJob
var items = make([]vo.SdJob, 0)
offset := (data.Page - 1) * data.PageSize
err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error
if err == nil {
// 填充数据
for _, item := range list {
var job vo.SdJob
err = utils.CopyObject(item, &job)
if err != nil {
continue
}
job.CreatedAt = item.CreatedAt.Unix()
items = append(items, job)
}
}
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items))
}
// DallList DALL-E 任务列表
func (h *ImageHandler) DallList(c *gin.Context) {
var data query
if err := c.ShouldBindJSON(&data); err != nil {
resp.ERROR(c, types.InvalidArgs)
return
}
session := h.DB.Session(&gorm.Session{})
if data.Username != "" {
var user model.User
err := h.DB.Where("username", data.Username).First(&user).Error
if err == nil {
session = session.Where("user_id", user.Id)
}
}
if data.Prompt != "" {
session = session.Where("prompt LIKE ?", "%"+data.Prompt+"%")
}
if len(data.CreatedAt) == 2 {
start := utils.Str2stamp(data.CreatedAt[0] + " 00:00:00")
end := utils.Str2stamp(data.CreatedAt[1] + " 00:00:00")
session = session.Where("created_at >= ? AND created_at <= ?", start, end)
}
var total int64
session.Model(&model.DallJob{}).Count(&total)
var list []model.DallJob
var items = make([]vo.DallJob, 0)
offset := (data.Page - 1) * data.PageSize
err := session.Order("id DESC").Offset(offset).Limit(data.PageSize).Find(&list).Error
if err == nil {
// 填充数据
for _, item := range list {
var job vo.DallJob
err = utils.CopyObject(item, &job)
if err != nil {
continue
}
job.CreatedAt = item.CreatedAt.Unix()
items = append(items, job)
}
}
resp.SUCCESS(c, vo.NewPage(total, data.Page, data.PageSize, items))
}

View File

@ -539,6 +539,13 @@ func main() {
},
})
}),
fx.Provide(admin.NewImageHandler),
fx.Invoke(func(s *core.AppServer, h *admin.ImageHandler) {
group := s.Engine.Group("/api/admin/image")
group.POST("/list/mj", h.MjList)
group.POST("/list/sd", h.SdList)
group.POST("/list/dall", h.DallList)
}),
)
// 启动应用程序
go func() {

View File

@ -1,7 +1,5 @@
package vo
import "time"
type MidJourneyJob struct {
Id uint `json:"id"`
Type string `json:"type"`
@ -19,5 +17,5 @@ type MidJourneyJob struct {
Publish bool `json:"publish"`
ErrMsg string `json:"err_msg"`
Power int `json:"power"`
CreatedAt time.Time `json:"created_at"`
CreatedAt int64 `json:"created_at"`
}

View File

@ -2,7 +2,6 @@ package vo
import (
"geekai/core/types"
"time"
)
type SdJob struct {
@ -17,5 +16,5 @@ type SdJob struct {
Publish bool `json:"publish"`
ErrMsg string `json:"err_msg"`
Power int `json:"power"`
CreatedAt time.Time `json:"created_at"`
CreatedAt int64 `json:"created_at"`
}

View File

@ -33,7 +33,7 @@
{{ threeItem.title }}
</el-menu-item>
</el-sub-menu>
<el-menu-item v-else :index="subItem.index">
<el-menu-item v-else :index="subItem.index" :key="subItem.index">
<i v-if="subItem.icon" :class="'iconfont icon-'+subItem.icon"></i>
{{ subItem.title }}
</el-menu-item>
@ -64,8 +64,8 @@ const logo = ref('')
//
httpGet('/api/admin/config/get?key=system').then(res => {
title.value = res.data['admin_title']
logo.value = res.data['logo']
title.value = res.data.admin_title
logo.value = res.data.logo
}).catch(e => {
ElMessage.error("加载系统配置失败: " + e.message)
})
@ -137,6 +137,11 @@ const items = [
index: '/admin/chats',
title: '对话管理',
},
{
icon: 'image',
index: '/admin/images',
title: '绘图管理',
},
{
icon: 'role',
index: '/admin/manger',

View File

@ -233,6 +233,12 @@ const routes = [
meta: {title: '对话管理'},
component: () => import('@/views/admin/ChatList.vue'),
},
{
path: '/admin/images',
name: 'admin-images',
meta: {title: '绘图管理'},
component: () => import('@/views/admin/ImageList.vue'),
},
{
path: '/admin/powerLog',
name: 'admin-power-log',

View File

@ -0,0 +1,510 @@
<template>
<div class="container chat-list">
<el-tabs v-model="activeName" @tab-change="handleChange">
<el-tab-pane label="Midjourney" name="mj" v-loading="data.mj.loading">
<div class="handle-box">
<el-input v-model.number="data.mj.query.username" placeholder="用户名" class="handle-input mr10"
@keyup="search($event,'mj')"></el-input>
<el-input v-model="data.mj.query.prompt" placeholder="提示词" class="handle-input mr10"
@keyup="search($event,'mj')"></el-input>
<el-date-picker
v-model="data.mj.query.created_at"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="margin-right: 10px;width: 200px; position: relative;top:3px;"
/>
<el-button type="primary" :icon="Search" @click="fetchMjData">搜索</el-button>
</div>
<div v-if="data.mj.items.length > 0">
<el-row>
<el-table :data="data.mj.items" :row-key="row => row.id" table-layout="auto">
<el-table-column prop="user_id" label="用户ID"/>
<el-table-column label="任务类型">
<template #default="scope">
<el-button :color="taskTypeTheme[scope.row.type].color" size="small" plain>{{taskTypeTheme[scope.row.type].text}}</el-button>
</template>
</el-table-column>
<el-table-column prop="progress" label="任务进度">
<template #default="scope">
<span v-if="scope.row.progress <= 100">{{scope.row.progress}}%</span>
<el-tag v-else type="danger">已失败</el-tag>
</template>
</el-table-column>
<el-table-column prop="power" label="消耗算力"/>
<el-table-column label="结果图片">
<template #default="scope">
<el-button size="small" type="success" @click="showImage(scope.row.img_url)" v-if="scope.row.img_url !== ''" plain>预览图片</el-button>
</template>
</el-table-column>
<el-table-column label="提示词">
<template #default="scope">
<el-popover
placement="top-start"
title="绘画提示词"
:width="300"
trigger="hover"
:content="scope.row.prompt"
>
<template #reference>
<span>{{ substr(scope.row.prompt, 20) }}</span>
</template>
</el-popover>
</template>
</el-table-column>
<el-table-column label="创建时间">
<template #default="scope">
<span>{{ dateFormat(scope.row['created_at']) }}</span>
</template>
</el-table-column>
<el-table-column label="失败原因">
<template #default="scope">
<el-popover
placement="top-start"
title="失败原因"
:width="300"
trigger="hover"
:content="scope.row.err_msg"
v-if="scope.row.progress === 101"
>
<template #reference>
<el-text type="danger">{{ substr(scope.row.err_msg, 20) }}</el-text>
</template>
</el-popover>
<span v-else></span>
</template>
</el-table-column>
<el-table-column label="操作" width="180">
<template #default="scope">
<el-popconfirm title="确定要删除当前记录吗?" @confirm="remove(scope.row, 'mj')">
<template #reference>
<el-button size="small" type="danger">删除</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
</el-row>
<div class="pagination">
<el-pagination v-if="data.mj.total > 0" background
layout="total,prev, pager, next"
:hide-on-single-page="true"
v-model:current-page="data.mj.page"
v-model:page-size="data.mj.pageSize"
@current-change="fetchMjData()"
:total="data.mj.total"/>
</div>
</div>
<el-empty v-else />
</el-tab-pane>
<el-tab-pane label="Stable-Diffusion" name="sd" v-loading="data.sd.loading">
<div class="handle-box">
<el-input v-model.number="data.sd.query.username" placeholder="用户名" class="handle-input mr10"
@keyup="search($event, 'sd')"></el-input>
<el-input v-model="data.sd.query.prompt" placeholder="提示词" class="handle-input mr10"
@keyup="search($event, 'sd')"></el-input>
<el-date-picker
v-model="data.sd.query.created_at"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="margin-right: 10px;width: 200px; position: relative;top:3px;"
/>
<el-button type="primary" :icon="Search" @click="fetchSdData">搜索</el-button>
</div>
<div v-if="data.sd.items.length > 0">
<el-row>
<el-table :data="data.sd.items" :row-key="row => row.id" table-layout="auto">
<el-table-column prop="user_id" label="用户ID"/>
<el-table-column prop="progress" label="任务进度">
<template #default="scope">
<span v-if="scope.row.progress <= 100">{{scope.row.progress}}%</span>
<el-tag v-else type="danger">已失败</el-tag>
</template>
</el-table-column>
<el-table-column prop="power" label="消耗算力"/>
<el-table-column label="结果图片">
<template #default="scope">
<el-button size="small" type="success" @click="showImage(scope.row.img_url)" v-if="scope.row.img_url !== ''" plain>预览图片</el-button>
</template>
</el-table-column>
<el-table-column label="提示词">
<template #default="scope">
<el-popover
placement="top-start"
title="绘画提示词"
:width="300"
trigger="hover"
:content="scope.row.prompt"
>
<template #reference>
<span>{{ substr(scope.row.prompt, 20) }}</span>
</template>
</el-popover>
</template>
</el-table-column>
<el-table-column label="创建时间">
<template #default="scope">
<span>{{ dateFormat(scope.row['created_at']) }}</span>
</template>
</el-table-column>
<el-table-column label="失败原因">
<template #default="scope">
<el-popover
placement="top-start"
title="失败原因"
:width="300"
trigger="hover"
:content="scope.row.err_msg"
v-if="scope.row.progress === 101"
>
<template #reference>
<el-text type="danger">{{ substr(scope.row.err_msg, 20) }}</el-text>
</template>
</el-popover>
<span v-else></span>
</template>
</el-table-column>
<el-table-column label="操作" width="180">
<template #default="scope">
<el-popconfirm title="确定要删除当前记录吗?" @confirm="remove(scope.row, 'sd')">
<template #reference>
<el-button size="small" type="danger">删除</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
</el-row>
<div class="pagination">
<el-pagination v-if="data.sd.total > 0" background
layout="total,prev, pager, next"
:hide-on-single-page="true"
v-model:current-page="data.sd.page"
v-model:page-size="data.sd.pageSize"
@current-change="fetchSdData()"
:total="data.sd.total"/>
</div>
</div>
<el-empty v-else />
</el-tab-pane>
<el-tab-pane label="DALL-E" name="dall">
<div class="handle-box">
<el-input v-model.number="data.dall.query.username" placeholder="用户名" class="handle-input mr10"
@keyup="search($event,'dall')"></el-input>
<el-input v-model="data.dall.query.prompt" placeholder="提示词" class="handle-input mr10"
@keyup="search($event, 'dall')"></el-input>
<el-date-picker
v-model="data.dall.query.created_at"
type="daterange"
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="margin-right: 10px;width: 200px; position: relative;top:3px;"
/>
<el-button type="primary" :icon="Search" @click="fetchDallData">搜索</el-button>
</div>
<div v-if="data.dall.items.length > 0">
<el-row>
<el-table :data="data.dall.items" :row-key="row => row.id" table-layout="auto">
<el-table-column prop="user_id" label="用户ID"/>
<el-table-column prop="progress" label="任务进度">
<template #default="scope">
<span v-if="scope.row.progress <= 100">{{scope.row.progress}}%</span>
<el-tag v-else type="danger">已失败</el-tag>
</template>
</el-table-column>
<el-table-column prop="power" label="消耗算力"/>
<el-table-column label="结果图片">
<template #default="scope">
<el-button size="small" type="success" @click="showImage(scope.row.img_url)" v-if="scope.row.img_url !== ''" plain>预览图片</el-button>
</template>
</el-table-column>
<el-table-column label="提示词">
<template #default="scope">
<el-popover
placement="top-start"
title="绘画提示词"
:width="300"
trigger="hover"
:content="scope.row.prompt"
>
<template #reference>
<span>{{ substr(scope.row.prompt, 20) }}</span>
</template>
</el-popover>
</template>
</el-table-column>
<el-table-column label="创建时间">
<template #default="scope">
<span>{{ dateFormat(scope.row['created_at']) }}</span>
</template>
</el-table-column>
<el-table-column label="失败原因">
<template #default="scope">
<el-popover
placement="top-start"
title="失败原因"
:width="300"
trigger="hover"
:content="scope.row.err_msg"
v-if="scope.row.progress === 101"
>
<template #reference>
<el-text type="danger">{{ substr(scope.row.err_msg, 20) }}</el-text>
</template>
</el-popover>
<span v-else></span>
</template>
</el-table-column>
<el-table-column label="操作" width="180">
<template #default="scope">
<el-popconfirm title="确定要删除当前记录吗?" @confirm="remove(scope.row, 'dall')">
<template #reference>
<el-button size="small" type="danger">删除</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
</el-row>
<div class="pagination">
<el-pagination v-if="data.dall.total > 0" background
layout="total,prev, pager, next"
:hide-on-single-page="true"
v-model:current-page="data.dall.page"
v-model:page-size="data.dall.pageSize"
@current-change="fetchDallData()"
:total="data.dall.total"/>
</div>
</div>
<el-empty v-else />
</el-tab-pane>
</el-tabs>
<el-dialog
v-model="showImageDialog"
title="图片预览"
>
<el-image
:src="imgURL"
:zoom-rate="1.2"
:max-scale="7"
:min-scale="0.2"
:preview-src-list="[imgURL]"
:initial-index="0"
fit="cover"
/>
</el-dialog>
</div>
</template>
<script setup>
import {onMounted, ref} from "vue";
import {httpGet, httpPost} from "@/utils/http";
import {ElMessage} from "element-plus";
import {dateFormat, substr} from "@/utils/libs";
import {Search} from "@element-plus/icons-vue";
//
const data = ref({
"mj": {
items: [],
query: {prompt: "", username: "", created_at: [], page: 1, page_size: 15},
total: 0,
page: 1,
pageSize: 15,
loading: true
},
"sd": {
items: [],
query: {prompt: "", username: "", created_at: [], page: 1, page_size: 15},
total: 0,
page: 1,
pageSize: 15,
loading: true
},
"dall": {
items: [],
query: {prompt: "", username: "", created_at: [], page: 1, page_size: 15},
total: 0,
page: 1,
pageSize: 15,
loading: true
}
})
const activeName = ref("mj")
const taskTypeTheme = {
image: {text: "绘图", color: "#2185d0"},
upscale: {text: "放大", color: "#f2711c" },
variation: {text: "变换", color: "#00b5ad"},
blend: {text: "融图", color: "#21ba45"},
swapFace: {text: "换脸", color: "#a333c8"}
}
onMounted(() => {
fetchMjData()
})
const handleChange = (tab) => {
switch (tab) {
case "mj":
fetchMjData()
break
case "sd":
fetchSdData()
break
case "dall":
fetchDallData()
break
}
}
//
const search = (evt,tab) => {
if (evt.keyCode === 13) {
handleChange(tab)
}
}
//
const fetchMjData = () => {
const d = data.value.mj
d.query.page = d.page
d.query.page_size = d.pageSize
httpPost('/api/admin/image/list/mj', d.query).then((res) => {
if (res.data) {
d.items = res.data.items
d.total = res.data.total
d.page = res.data.page
d.pageSize = res.data.page_size
}
d.loading = false
}).catch(e => {
ElMessage.error("获取数据失败:" + e.message);
})
}
const fetchSdData = () => {
const d = data.value.sd
d.query.page = d.page
d.query.page_size = d.pageSize
httpPost('/api/admin/image/list/sd', d.query).then((res) => {
if (res.data) {
d.items = res.data.items
d.total = res.data.total
d.page = res.data.page
d.pageSize = res.data.page_size
}
d.loading = false
}).catch(e => {
ElMessage.error("获取数据失败:" + e.message);
})
}
const fetchDallData = () => {
const d = data.value.dall
d.query.page = d.page
d.query.page_size = d.pageSize
httpPost('/api/admin/image/list/dall', d.query).then((res) => {
if (res.data) {
d.items = res.data.items
d.total = res.data.total
d.page = res.data.page
d.pageSize = res.data.page_size
}
d.loading = false
}).catch(e => {
ElMessage.error("获取数据失败:" + e.message);
})
}
const remove = function (row,tab) {
httpGet('/api/admin/chat/remove?chat_id=' + row.chat_id).then(() => {
ElMessage.success("删除成功!")
}).catch((e) => {
ElMessage.error("删除失败:" + e.message)
})
}
const showImageDialog = ref(false)
const imgURL = ref('')
const showImage = (url) => {
showImageDialog.value = true
imgURL.value = url
}
</script>
<style lang="stylus" scoped>
.chat-list {
.handle-box {
margin-bottom 20px
.handle-input {
max-width 150px;
margin-right 10px;
}
}
.opt-box {
padding-bottom: 10px;
display flex;
justify-content flex-end
.el-icon {
margin-right: 5px;
}
}
.el-select {
width: 100%
}
.pagination {
padding 20px 0
display flex
justify-content right
}
.chat-box {
overflow hidden
//
--content-font-size: 16px;
--content-color: #c1c1c1;
font-family: 'Microsoft YaHei', '微软雅黑', Arial, sans-serif;
.chat-line {
//
::-webkit-scrollbar {
width: 0;
height: 0;
background-color: transparent;
}
font-size: 14px;
display: flex;
align-items: flex-start;
}
}
}
</style>