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

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

View File

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

View File

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

View File

@@ -3,8 +3,20 @@
<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="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-input
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
v-model="data.mj.query.created_at"
type="daterange"
@@ -23,7 +35,9 @@
<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>
<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="任务进度">
@@ -35,12 +49,25 @@
<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>
<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">
<el-popover
placement="top-start"
title="绘画提示词"
:width="300"
trigger="hover"
:content="scope.row.prompt"
>
<template #reference>
<span>{{ substr(scope.row.prompt, 20) }}</span>
</template>
@@ -49,7 +76,7 @@
</el-table-column>
<el-table-column label="创建时间">
<template #default="scope">
<span>{{ dateFormat(scope.row["created_at"]) }}</span>
<span>{{ dateFormat(scope.row['created_at']) }}</span>
</template>
</el-table-column>
<el-table-column label="失败原因">
@@ -98,8 +125,20 @@
</el-tab-pane>
<el-tab-pane label="Stable-Diffusion" name="sd" v-loading="data.sd.loading">
<div class="handle-box">
<el-input 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-input
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
v-model="data.sd.query.created_at"
type="daterange"
@@ -125,12 +164,25 @@
<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>
<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">
<el-popover
placement="top-start"
title="绘画提示词"
:width="300"
trigger="hover"
:content="scope.row.prompt"
>
<template #reference>
<span>{{ substr(scope.row.prompt, 20) }}</span>
</template>
@@ -139,7 +191,7 @@
</el-table-column>
<el-table-column label="创建时间">
<template #default="scope">
<span>{{ dateFormat(scope.row["created_at"]) }}</span>
<span>{{ dateFormat(scope.row['created_at']) }}</span>
</template>
</el-table-column>
<el-table-column label="失败原因">
@@ -188,8 +240,20 @@
</el-tab-pane>
<el-tab-pane label="DALL-E" name="dall">
<div class="handle-box">
<el-input 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-input
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
v-model="data.dall.query.created_at"
type="daterange"
@@ -215,12 +279,25 @@
<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>
<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">
<el-popover
placement="top-start"
title="绘画提示词"
:width="300"
trigger="hover"
:content="scope.row.prompt"
>
<template #reference>
<span>{{ substr(scope.row.prompt, 20) }}</span>
</template>
@@ -229,7 +306,7 @@
</el-table-column>
<el-table-column label="创建时间">
<template #default="scope">
<span>{{ dateFormat(scope.row["created_at"]) }}</span>
<span>{{ dateFormat(scope.row['created_at']) }}</span>
</template>
</el-table-column>
<el-table-column label="失败原因">
@@ -278,24 +355,32 @@
</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 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-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";
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 },
query: { prompt: '', username: '', created_at: [], page: 1, page_size: 15 },
total: 0,
page: 1,
pageSize: 15,
@@ -303,7 +388,7 @@ const data = ref({
},
sd: {
items: [],
query: { prompt: "", username: "", created_at: [], page: 1, page_size: 15 },
query: { prompt: '', username: '', created_at: [], page: 1, page_size: 15 },
total: 0,
page: 1,
pageSize: 15,
@@ -311,122 +396,122 @@ const data = ref({
},
dall: {
items: [],
query: { prompt: "", username: "", created_at: [], page: 1, page_size: 15 },
query: { prompt: '', username: '', created_at: [], page: 1, page_size: 15 },
total: 0,
page: 1,
pageSize: 15,
loading: true,
},
});
const activeName = ref("mj");
})
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" },
};
image: { text: '绘图', color: '#2185d0' },
upscale: { text: '放大', color: '#f2711c' },
variation: { text: '变换', color: '#00b5ad' },
blend: { text: '融图', color: '#21ba45' },
swapFace: { text: '换脸', color: '#a333c8' },
}
onMounted(() => {
fetchMjData();
});
fetchMjData()
})
const handleChange = (tab) => {
switch (tab) {
case "mj":
fetchMjData();
break;
case "sd":
fetchSdData();
break;
case "dall":
fetchDallData();
break;
case 'mj':
fetchMjData()
break
case 'sd':
fetchSdData()
break
case 'dall':
fetchDallData()
break
}
};
}
// 搜索对话
const search = (evt, tab) => {
if (evt.keyCode === 13) {
handleChange(tab);
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)
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.items = res.data.items
d.total = res.data.total
d.page = res.data.page
d.pageSize = res.data.page_size
}
d.loading = false;
d.loading = false
})
.catch((e) => {
ElMessage.error("获取数据失败" + e.message);
});
};
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)
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.items = res.data.items
d.total = res.data.total
d.page = res.data.page
d.pageSize = res.data.page_size
}
d.loading = false;
d.loading = false
})
.catch((e) => {
ElMessage.error("获取数据失败" + e.message);
});
};
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)
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.items = res.data.items
d.total = res.data.total
d.page = res.data.page
d.pageSize = res.data.page_size
}
d.loading = false;
d.loading = false
})
.catch((e) => {
ElMessage.error("获取数据失败" + e.message);
});
};
ElMessage.error('获取数据失败' + e.message)
})
}
const remove = function (row, tab) {
httpGet(`/api/admin/image/remove?id=${row.id}&tab=${tab}`)
.then(() => {
ElMessage.success("删除成功");
handleChange(tab);
ElMessage.success('删除成功')
handleChange(tab)
})
.catch((e) => {
ElMessage.error("删除失败" + e.message);
});
};
ElMessage.error('删除失败' + e.message)
})
}
const showImageDialog = ref(false);
const imgURL = ref("");
const showImageDialog = ref(false)
const imgURL = ref('')
const showImage = (url) => {
showImageDialog.value = true;
imgURL.value = url;
};
showImageDialog.value = true
imgURL.value = url
}
</script>
<style lang="stylus" scoped>