优化管理后台对话显示样式

This commit is contained in:
RockYang
2025-05-28 22:55:53 +08:00
parent eea57790de
commit 97e489901a
5 changed files with 210 additions and 136 deletions

View File

@@ -218,11 +218,19 @@ func (h *ChatHandler) History(c *gin.Context) {
for _, item := range items { for _, item := range items {
var v vo.ChatMessage var v vo.ChatMessage
err := utils.CopyObject(item, &v) err := utils.CopyObject(item, &v)
if err != nil {
continue
}
// 解析内容
var content vo.MsgContent
err = utils.JsonDecode(item.Content, &content)
if err != nil {
content.Text = item.Content
}
v.Content = content
v.CreatedAt = item.CreatedAt.Unix() v.CreatedAt = item.CreatedAt.Unix()
v.UpdatedAt = item.UpdatedAt.Unix() v.UpdatedAt = item.UpdatedAt.Unix()
if err == nil { messages = append(messages, v)
messages = append(messages, v)
}
} }
} }

View File

@@ -64,7 +64,6 @@ type ChatHandler struct {
uploadManager *oss.UploaderManager uploadManager *oss.UploaderManager
licenseService *service.LicenseService licenseService *service.LicenseService
ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function ReqCancelFunc *types.LMap[string, context.CancelFunc] // HttpClient 请求取消 handle function
ChatContexts *types.LMap[string, []any] // 聊天上下文 Map [chatId] => []Message
userService *service.UserService userService *service.UserService
} }
@@ -75,7 +74,6 @@ func NewChatHandler(app *core.AppServer, db *gorm.DB, redis *redis.Client, manag
uploadManager: manager, uploadManager: manager,
licenseService: licenseService, licenseService: licenseService,
ReqCancelFunc: types.NewLMap[string, context.CancelFunc](), ReqCancelFunc: types.NewLMap[string, context.CancelFunc](),
ChatContexts: types.NewLMap[string, []any](),
userService: userService, userService: userService,
} }
} }
@@ -223,28 +221,24 @@ func (h *ChatHandler) sendMessage(ctx context.Context, input ChatInput, c *gin.C
chatCtx := make([]any, 0) chatCtx := make([]any, 0)
messages := make([]any, 0) messages := make([]any, 0)
if h.App.SysConfig.EnableContext { if h.App.SysConfig.EnableContext {
if h.ChatContexts.Has(input.ChatId) { _ = utils.JsonDecode(input.ChatRole.Context, &messages)
messages = h.ChatContexts.Get(input.ChatId) if h.App.SysConfig.ContextDeep > 0 {
} else { var historyMessages []model.ChatMessage
_ = utils.JsonDecode(input.ChatRole.Context, &messages) dbSession := h.DB.Session(&gorm.Session{}).Where("chat_id", input.ChatId)
if h.App.SysConfig.ContextDeep > 0 { if input.LastMsgId > 0 { // 重新生成逻辑
var historyMessages []model.ChatMessage dbSession = dbSession.Where("id < ?", input.LastMsgId)
dbSession := h.DB.Session(&gorm.Session{}).Where("chat_id", input.ChatId) // 删除对应的聊天记录
if input.LastMsgId > 0 { // 重新生成逻辑 h.DB.Debug().Where("chat_id", input.ChatId).Where("id >= ?", input.LastMsgId).Delete(&model.ChatMessage{})
dbSession = dbSession.Where("id < ?", input.LastMsgId) }
// 删除对应的聊天记录 err = dbSession.Limit(h.App.SysConfig.ContextDeep).Order("id DESC").Find(&historyMessages).Error
h.DB.Where("chat_id", input.ChatId).Where("id >= ?", input.LastMsgId).Delete(&model.ChatMessage{}) if err == nil {
} for i := len(historyMessages) - 1; i >= 0; i-- {
err = dbSession.Limit(h.App.SysConfig.ContextDeep).Order("id DESC").Find(&historyMessages).Error msg := historyMessages[i]
if err == nil { ms := types.Message{Role: "user", Content: msg.Content}
for i := len(historyMessages) - 1; i >= 0; i-- { if msg.Type == types.ReplyMsg {
msg := historyMessages[i] ms.Role = "assistant"
ms := types.Message{Role: "user", Content: msg.Content}
if msg.Type == types.ReplyMsg {
ms.Role = "assistant"
}
chatCtx = append(chatCtx, ms)
} }
chatCtx = append(chatCtx, ms)
} }
} }
} }
@@ -521,13 +515,6 @@ func (h *ChatHandler) saveChatHistory(
promptCreatedAt time.Time, promptCreatedAt time.Time,
replyCreatedAt time.Time) { replyCreatedAt time.Time) {
// 更新上下文消息
if h.App.SysConfig.EnableContext {
chatCtx := req.Messages // 提问消息
chatCtx = append(chatCtx, message) // 回复消息
h.ChatContexts.Put(input.ChatId, chatCtx)
}
// 追加聊天记录 // 追加聊天记录
// for prompt // for prompt
var promptTokens, replyTokens, totalTokens int var promptTokens, replyTokens, totalTokens int

View File

@@ -104,8 +104,6 @@ func (h *ChatHandler) Clear(c *gin.Context) {
var chatIds = make([]string, 0) var chatIds = make([]string, 0)
for _, chat := range chats { for _, chat := range chats {
chatIds = append(chatIds, chat.ChatId) chatIds = append(chatIds, chat.ChatId)
// 清空会话上下文
h.ChatContexts.Delete(chat.ChatId)
} }
err = h.DB.Transaction(func(tx *gorm.DB) error { err = h.DB.Transaction(func(tx *gorm.DB) error {
res := h.DB.Where("user_id =?", user.Id).Delete(&model.ChatItem{}) res := h.DB.Where("user_id =?", user.Id).Delete(&model.ChatItem{})
@@ -187,10 +185,6 @@ func (h *ChatHandler) Remove(c *gin.Context) {
return return
} }
// TODO: 是否要删除 MidJourney 绘画记录和图片文件?
// 清空会话上下文
h.ChatContexts.Delete(chatId)
resp.SUCCESS(c, types.OkMsg) resp.SUCCESS(c, types.OkMsg)
} }

View File

@@ -873,7 +873,6 @@ const sendMessage = (messageId) => {
}, },
model: getModelValue(modelID.value), model: getModelValue(modelID.value),
created_at: new Date().getTime() / 1000, created_at: new Date().getTime() / 1000,
message_id: messageId,
}) })
// 添加空回复消息 // 添加空回复消息
@@ -909,6 +908,7 @@ const sendMessage = (messageId) => {
tools: toolSelected.value, tools: toolSelected.value,
stream: stream.value, stream: stream.value,
files: files.value, files: files.value,
last_msg_id: messageId,
}) })
prompt.value = '' prompt.value = ''
@@ -1196,10 +1196,10 @@ const reGenerate = function (messageId) {
// 保存用户消息内容,填入输入框 // 保存用户消息内容,填入输入框
const userPrompt = chatData.value[chatData.value.length - 1].content.text const userPrompt = chatData.value[chatData.value.length - 1].content.text
// 删除用户消息 // 删除用户消息
chatData.value.pop() const lastMessage = chatData.value.pop()
// 填入输入框 // 填入输入框
prompt.value = userPrompt prompt.value = userPrompt
sendMessage(messageId) sendMessage(lastMessage.id)
// 将光标定位到输入框并聚焦 // 将光标定位到输入框并聚焦
nextTick(() => { nextTick(() => {
if (inputRef.value) { if (inputRef.value) {

View File

@@ -3,8 +3,20 @@
<el-tabs v-model="activeName" @tab-change="handleChange"> <el-tabs v-model="activeName" @tab-change="handleChange">
<el-tab-pane label="Midjourney" name="mj" v-loading="data.mj.loading"> <el-tab-pane label="Midjourney" name="mj" v-loading="data.mj.loading">
<div class="handle-box"> <div class="handle-box">
<el-input v-model="data.mj.query.username" placeholder="用户名" class="handle-input mr10" @keyup="search($event, 'mj')" clearable /> <el-input
<el-input v-model="data.mj.query.prompt" placeholder="提示词" class="handle-input mr10" @keyup="search($event, 'mj')" clearable /> v-model="data.mj.query.username"
placeholder="用户名"
class="handle-input mr10"
@keyup="search($event, 'mj')"
clearable
/>
<el-input
v-model="data.mj.query.prompt"
placeholder="提示词"
class="handle-input mr10"
@keyup="search($event, 'mj')"
clearable
/>
<el-date-picker <el-date-picker
v-model="data.mj.query.created_at" v-model="data.mj.query.created_at"
type="daterange" type="daterange"
@@ -23,7 +35,9 @@
<el-table-column prop="user_id" label="用户ID" /> <el-table-column prop="user_id" label="用户ID" />
<el-table-column label="任务类型"> <el-table-column label="任务类型">
<template #default="scope"> <template #default="scope">
<el-button :color="taskTypeTheme[scope.row.type].color" size="small" plain>{{ taskTypeTheme[scope.row.type].text }}</el-button> <el-button :color="taskTypeTheme[scope.row.type].color" size="small" plain>{{
taskTypeTheme[scope.row.type].text
}}</el-button>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="progress" label="任务进度"> <el-table-column prop="progress" label="任务进度">
@@ -35,12 +49,25 @@
<el-table-column prop="power" label="消耗算力" /> <el-table-column prop="power" label="消耗算力" />
<el-table-column label="结果图片"> <el-table-column label="结果图片">
<template #default="scope"> <template #default="scope">
<el-button size="small" type="success" @click="showImage(scope.row.img_url)" v-if="scope.row.img_url !== ''" plain>预览图片</el-button> <el-button
size="small"
type="success"
@click="showImage(scope.row.img_url)"
v-if="scope.row.img_url !== ''"
plain
>预览图片</el-button
>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="提示词"> <el-table-column label="提示词">
<template #default="scope"> <template #default="scope">
<el-popover placement="top-start" title="绘画提示词" :width="300" trigger="hover" :content="scope.row.prompt"> <el-popover
placement="top-start"
title="绘画提示词"
:width="300"
trigger="hover"
:content="scope.row.prompt"
>
<template #reference> <template #reference>
<span>{{ substr(scope.row.prompt, 20) }}</span> <span>{{ substr(scope.row.prompt, 20) }}</span>
</template> </template>
@@ -49,7 +76,7 @@
</el-table-column> </el-table-column>
<el-table-column label="创建时间"> <el-table-column label="创建时间">
<template #default="scope"> <template #default="scope">
<span>{{ dateFormat(scope.row["created_at"]) }}</span> <span>{{ dateFormat(scope.row['created_at']) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="失败原因"> <el-table-column label="失败原因">
@@ -98,8 +125,20 @@
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="Stable-Diffusion" name="sd" v-loading="data.sd.loading"> <el-tab-pane label="Stable-Diffusion" name="sd" v-loading="data.sd.loading">
<div class="handle-box"> <div class="handle-box">
<el-input v-model="data.sd.query.username" placeholder="用户名" class="handle-input mr10" @keyup="search($event, 'sd')" clearable /> <el-input
<el-input v-model="data.sd.query.prompt" placeholder="提示词" class="handle-input mr10" @keyup="search($event, 'sd')" clearable /> v-model="data.sd.query.username"
placeholder="用户名"
class="handle-input mr10"
@keyup="search($event, 'sd')"
clearable
/>
<el-input
v-model="data.sd.query.prompt"
placeholder="提示词"
class="handle-input mr10"
@keyup="search($event, 'sd')"
clearable
/>
<el-date-picker <el-date-picker
v-model="data.sd.query.created_at" v-model="data.sd.query.created_at"
type="daterange" type="daterange"
@@ -125,12 +164,25 @@
<el-table-column prop="power" label="消耗算力" /> <el-table-column prop="power" label="消耗算力" />
<el-table-column label="结果图片"> <el-table-column label="结果图片">
<template #default="scope"> <template #default="scope">
<el-button size="small" type="success" @click="showImage(scope.row.img_url)" v-if="scope.row.img_url !== ''" plain>预览图片</el-button> <el-button
size="small"
type="success"
@click="showImage(scope.row.img_url)"
v-if="scope.row.img_url !== ''"
plain
>预览图片</el-button
>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="提示词"> <el-table-column label="提示词">
<template #default="scope"> <template #default="scope">
<el-popover placement="top-start" title="绘画提示词" :width="300" trigger="hover" :content="scope.row.prompt"> <el-popover
placement="top-start"
title="绘画提示词"
:width="300"
trigger="hover"
:content="scope.row.prompt"
>
<template #reference> <template #reference>
<span>{{ substr(scope.row.prompt, 20) }}</span> <span>{{ substr(scope.row.prompt, 20) }}</span>
</template> </template>
@@ -139,7 +191,7 @@
</el-table-column> </el-table-column>
<el-table-column label="创建时间"> <el-table-column label="创建时间">
<template #default="scope"> <template #default="scope">
<span>{{ dateFormat(scope.row["created_at"]) }}</span> <span>{{ dateFormat(scope.row['created_at']) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="失败原因"> <el-table-column label="失败原因">
@@ -188,8 +240,20 @@
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="DALL-E" name="dall"> <el-tab-pane label="DALL-E" name="dall">
<div class="handle-box"> <div class="handle-box">
<el-input v-model="data.dall.query.username" placeholder="用户名" class="handle-input mr10" @keyup="search($event, 'dall')" clearable /> <el-input
<el-input v-model="data.dall.query.prompt" placeholder="提示词" class="handle-input mr10" @keyup="search($event, 'dall')" clearable /> v-model="data.dall.query.username"
placeholder="用户名"
class="handle-input mr10"
@keyup="search($event, 'dall')"
clearable
/>
<el-input
v-model="data.dall.query.prompt"
placeholder="提示词"
class="handle-input mr10"
@keyup="search($event, 'dall')"
clearable
/>
<el-date-picker <el-date-picker
v-model="data.dall.query.created_at" v-model="data.dall.query.created_at"
type="daterange" type="daterange"
@@ -215,12 +279,25 @@
<el-table-column prop="power" label="消耗算力" /> <el-table-column prop="power" label="消耗算力" />
<el-table-column label="结果图片"> <el-table-column label="结果图片">
<template #default="scope"> <template #default="scope">
<el-button size="small" type="success" @click="showImage(scope.row.img_url)" v-if="scope.row.img_url !== ''" plain>预览图片</el-button> <el-button
size="small"
type="success"
@click="showImage(scope.row.img_url)"
v-if="scope.row.img_url !== ''"
plain
>预览图片</el-button
>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="提示词"> <el-table-column label="提示词">
<template #default="scope"> <template #default="scope">
<el-popover placement="top-start" title="绘画提示词" :width="300" trigger="hover" :content="scope.row.prompt"> <el-popover
placement="top-start"
title="绘画提示词"
:width="300"
trigger="hover"
:content="scope.row.prompt"
>
<template #reference> <template #reference>
<span>{{ substr(scope.row.prompt, 20) }}</span> <span>{{ substr(scope.row.prompt, 20) }}</span>
</template> </template>
@@ -229,7 +306,7 @@
</el-table-column> </el-table-column>
<el-table-column label="创建时间"> <el-table-column label="创建时间">
<template #default="scope"> <template #default="scope">
<span>{{ dateFormat(scope.row["created_at"]) }}</span> <span>{{ dateFormat(scope.row['created_at']) }}</span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="失败原因"> <el-table-column label="失败原因">
@@ -278,24 +355,32 @@
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
<el-dialog v-model="showImageDialog" title="图片预览"> <el-dialog v-model="showImageDialog" title="图片预览" style="height: 95vh; overflow: auto">
<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-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> </el-dialog>
</div> </div>
</template> </template>
<script setup> <script setup>
import { onMounted, ref } from "vue"; import { onMounted, ref } from 'vue'
import { httpGet, httpPost } from "@/utils/http"; import { httpGet, httpPost } from '@/utils/http'
import { ElMessage } from "element-plus"; import { ElMessage } from 'element-plus'
import { dateFormat, substr } from "@/utils/libs"; import { dateFormat, substr } from '@/utils/libs'
import { Search } from "@element-plus/icons-vue"; import { Search } from '@element-plus/icons-vue'
// 变量定义 // 变量定义
const data = ref({ const data = ref({
mj: { mj: {
items: [], items: [],
query: { prompt: "", username: "", created_at: [], page: 1, page_size: 15 }, query: { prompt: '', username: '', created_at: [], page: 1, page_size: 15 },
total: 0, total: 0,
page: 1, page: 1,
pageSize: 15, pageSize: 15,
@@ -303,7 +388,7 @@ const data = ref({
}, },
sd: { sd: {
items: [], items: [],
query: { prompt: "", username: "", created_at: [], page: 1, page_size: 15 }, query: { prompt: '', username: '', created_at: [], page: 1, page_size: 15 },
total: 0, total: 0,
page: 1, page: 1,
pageSize: 15, pageSize: 15,
@@ -311,122 +396,122 @@ const data = ref({
}, },
dall: { dall: {
items: [], items: [],
query: { prompt: "", username: "", created_at: [], page: 1, page_size: 15 }, query: { prompt: '', username: '', created_at: [], page: 1, page_size: 15 },
total: 0, total: 0,
page: 1, page: 1,
pageSize: 15, pageSize: 15,
loading: true, loading: true,
}, },
}); })
const activeName = ref("mj"); const activeName = ref('mj')
const taskTypeTheme = { const taskTypeTheme = {
image: { text: "绘图", color: "#2185d0" }, image: { text: '绘图', color: '#2185d0' },
upscale: { text: "放大", color: "#f2711c" }, upscale: { text: '放大', color: '#f2711c' },
variation: { text: "变换", color: "#00b5ad" }, variation: { text: '变换', color: '#00b5ad' },
blend: { text: "融图", color: "#21ba45" }, blend: { text: '融图', color: '#21ba45' },
swapFace: { text: "换脸", color: "#a333c8" }, swapFace: { text: '换脸', color: '#a333c8' },
}; }
onMounted(() => { onMounted(() => {
fetchMjData(); fetchMjData()
}); })
const handleChange = (tab) => { const handleChange = (tab) => {
switch (tab) { switch (tab) {
case "mj": case 'mj':
fetchMjData(); fetchMjData()
break; break
case "sd": case 'sd':
fetchSdData(); fetchSdData()
break; break
case "dall": case 'dall':
fetchDallData(); fetchDallData()
break; break
} }
}; }
// 搜索对话 // 搜索对话
const search = (evt, tab) => { const search = (evt, tab) => {
if (evt.keyCode === 13) { if (evt.keyCode === 13) {
handleChange(tab); handleChange(tab)
} }
}; }
// 获取数据 // 获取数据
const fetchMjData = () => { const fetchMjData = () => {
const d = data.value.mj; const d = data.value.mj
d.query.page = d.page; d.query.page = d.page
d.query.page_size = d.pageSize; d.query.page_size = d.pageSize
httpPost("/api/admin/image/list/mj", d.query) httpPost('/api/admin/image/list/mj', d.query)
.then((res) => { .then((res) => {
if (res.data) { if (res.data) {
d.items = res.data.items; d.items = res.data.items
d.total = res.data.total; d.total = res.data.total
d.page = res.data.page; d.page = res.data.page
d.pageSize = res.data.page_size; d.pageSize = res.data.page_size
} }
d.loading = false; d.loading = false
}) })
.catch((e) => { .catch((e) => {
ElMessage.error("获取数据失败" + e.message); ElMessage.error('获取数据失败' + e.message)
}); })
}; }
const fetchSdData = () => { const fetchSdData = () => {
const d = data.value.sd; const d = data.value.sd
d.query.page = d.page; d.query.page = d.page
d.query.page_size = d.pageSize; d.query.page_size = d.pageSize
httpPost("/api/admin/image/list/sd", d.query) httpPost('/api/admin/image/list/sd', d.query)
.then((res) => { .then((res) => {
if (res.data) { if (res.data) {
d.items = res.data.items; d.items = res.data.items
d.total = res.data.total; d.total = res.data.total
d.page = res.data.page; d.page = res.data.page
d.pageSize = res.data.page_size; d.pageSize = res.data.page_size
} }
d.loading = false; d.loading = false
}) })
.catch((e) => { .catch((e) => {
ElMessage.error("获取数据失败" + e.message); ElMessage.error('获取数据失败' + e.message)
}); })
}; }
const fetchDallData = () => { const fetchDallData = () => {
const d = data.value.dall; const d = data.value.dall
d.query.page = d.page; d.query.page = d.page
d.query.page_size = d.pageSize; d.query.page_size = d.pageSize
httpPost("/api/admin/image/list/dall", d.query) httpPost('/api/admin/image/list/dall', d.query)
.then((res) => { .then((res) => {
if (res.data) { if (res.data) {
d.items = res.data.items; d.items = res.data.items
d.total = res.data.total; d.total = res.data.total
d.page = res.data.page; d.page = res.data.page
d.pageSize = res.data.page_size; d.pageSize = res.data.page_size
} }
d.loading = false; d.loading = false
}) })
.catch((e) => { .catch((e) => {
ElMessage.error("获取数据失败" + e.message); ElMessage.error('获取数据失败' + e.message)
}); })
}; }
const remove = function (row, tab) { const remove = function (row, tab) {
httpGet(`/api/admin/image/remove?id=${row.id}&tab=${tab}`) httpGet(`/api/admin/image/remove?id=${row.id}&tab=${tab}`)
.then(() => { .then(() => {
ElMessage.success("删除成功"); ElMessage.success('删除成功')
handleChange(tab); handleChange(tab)
}) })
.catch((e) => { .catch((e) => {
ElMessage.error("删除失败" + e.message); ElMessage.error('删除失败' + e.message)
}); })
}; }
const showImageDialog = ref(false); const showImageDialog = ref(false)
const imgURL = ref(""); const imgURL = ref('')
const showImage = (url) => { const showImage = (url) => {
showImageDialog.value = true; showImageDialog.value = true
imgURL.value = url; imgURL.value = url
}; }
</script> </script>
<style lang="stylus" scoped> <style lang="stylus" scoped>