Merge remote-tracking branch 'refs/remotes/yudao/dev' into dev-crm
commit
bc8948eecf
|
@ -6,17 +6,17 @@
|
|||
"private": false,
|
||||
"scripts": {
|
||||
"i": "pnpm install",
|
||||
"dev": "vite",
|
||||
"dev": "vite --mode env.local",
|
||||
"dev-server": "vite --mode dev",
|
||||
"ts:check": "vue-tsc --noEmit",
|
||||
"build:local": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build",
|
||||
"build:dev": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode local-dev",
|
||||
"build:dev": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode dev",
|
||||
"build:test": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode test",
|
||||
"build:stage": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode stage",
|
||||
"build:prod": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build --mode prod",
|
||||
"serve:dev": "vite preview --mode dev",
|
||||
"serve:prod": "vite preview --mode prod",
|
||||
"preview": "pnpm build:local-dev && vite preview",
|
||||
"preview": "pnpm build:local && vite preview",
|
||||
"clean": "npx rimraf node_modules",
|
||||
"clean:cache": "npx rimraf node_modules/.cache",
|
||||
"lint:eslint": "eslint --fix --ext .js,.ts,.vue ./src",
|
||||
|
@ -29,6 +29,7 @@
|
|||
"@form-create/designer": "^3.1.3",
|
||||
"@form-create/element-ui": "^3.1.24",
|
||||
"@iconify/iconify": "^3.1.1",
|
||||
"@microsoft/fetch-event-source": "^2.0.1",
|
||||
"@videojs-player/vue": "^1.0.0",
|
||||
"@vueuse/core": "^10.9.0",
|
||||
"@wangeditor/editor": "^5.1.23",
|
||||
|
@ -51,6 +52,7 @@
|
|||
"highlight.js": "^11.9.0",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"marked": "^12.0.2",
|
||||
"min-dash": "^4.1.1",
|
||||
"mitt": "^3.0.1",
|
||||
"nprogress": "^0.2.0",
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
import request from '@/config/axios'
|
||||
|
||||
// AI 聊天会话 VO
|
||||
export interface ChatConversationVO {
|
||||
id: number // ID 编号
|
||||
userId: number // 用户编号
|
||||
title: string // 会话标题
|
||||
pinned: boolean // 是否置顶
|
||||
roleId: number // 角色编号
|
||||
modelId: number // 模型编号
|
||||
model: string // 模型标志
|
||||
temperature: number // 温度参数
|
||||
maxTokens: number // 单条回复的最大 Token 数量
|
||||
maxContexts: number // 上下文的最大 Message 数量
|
||||
updateTime: number // 更新时间
|
||||
// 额外字段
|
||||
modelName?: string // 模型名字
|
||||
roleAvatar?: string // 角色头像
|
||||
modelMaxTokens?: string // 模型的单条回复的最大 Token 数量
|
||||
modelMaxContexts?: string // 模型的上下文的最大 Message 数量
|
||||
}
|
||||
|
||||
// AI 聊天会话 API
|
||||
export const ChatConversationApi = {
|
||||
// 获得【我的】聊天会话
|
||||
getChatConversationMy: async (id: number) => {
|
||||
return await request.get({ url: `/ai/chat/conversation/get-my?id=${id}` })
|
||||
},
|
||||
|
||||
// 新增【我的】聊天会话
|
||||
createChatConversationMy: async (data?: ChatConversationVO) => {
|
||||
return await request.post({ url: `/ai/chat/conversation/create-my`, data })
|
||||
},
|
||||
|
||||
// 更新【我的】聊天会话
|
||||
updateChatConversationMy: async (data: ChatConversationVO) => {
|
||||
return await request.put({ url: `/ai/chat/conversation/update-my`, data })
|
||||
},
|
||||
|
||||
// 删除【我的】聊天会话
|
||||
deleteChatConversationMy: async (id: number) => {
|
||||
return await request.delete({ url: `/ai/chat/conversation/delete-my?id=${id}` })
|
||||
},
|
||||
|
||||
// 删除【我的】所有对话,置顶除外
|
||||
deleteMyAllExceptPinned: async () => {
|
||||
return await request.delete({ url: `/ai/chat/conversation/delete-my-all-except-pinned` })
|
||||
},
|
||||
|
||||
// 获得【我的】聊天会话列表
|
||||
getChatConversationMyList: async () => {
|
||||
return await request.get({ url: `/ai/chat/conversation/my-list` })
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
import request from '@/config/axios'
|
||||
import { fetchEventSource } from '@microsoft/fetch-event-source'
|
||||
import { getAccessToken } from '@/utils/auth'
|
||||
import { config } from '@/config/axios/config'
|
||||
|
||||
// 聊天VO
|
||||
export interface ChatMessageVO {
|
||||
id: number // 编号
|
||||
conversationId: number // 会话编号
|
||||
type: string // 消息类型
|
||||
userId: string // 用户编号
|
||||
roleId: string // 角色编号
|
||||
model: number // 模型标志
|
||||
modelId: number // 模型编号
|
||||
content: string // 聊天内容
|
||||
tokens: number // 消耗 Token 数量
|
||||
createTime: Date // 创建时间
|
||||
}
|
||||
|
||||
export interface ChatMessageSendVO {
|
||||
conversationId: string // 会话编号
|
||||
content: number // 聊天内容
|
||||
}
|
||||
|
||||
// AI chat 聊天
|
||||
export const ChatMessageApi = {
|
||||
// 消息列表
|
||||
messageList: async (conversationId: number | null) => {
|
||||
return await request.get({
|
||||
url: `/ai/chat/message/list-by-conversation-id?conversationId=${conversationId}`
|
||||
})
|
||||
},
|
||||
|
||||
// 发送 send stream 消息
|
||||
// TODO axios 可以么? https://apifox.com/apiskills/how-to-create-axios-stream/
|
||||
sendStream: async (
|
||||
conversationId: number,
|
||||
content: string,
|
||||
ctrl,
|
||||
onMessage,
|
||||
onError,
|
||||
onClose
|
||||
) => {
|
||||
const token = getAccessToken()
|
||||
return fetchEventSource(`${config.base_url}/ai/chat/message/send-stream`, {
|
||||
method: 'post',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${token}`
|
||||
},
|
||||
openWhenHidden: true,
|
||||
body: JSON.stringify({
|
||||
conversationId,
|
||||
content
|
||||
}),
|
||||
onmessage: onMessage,
|
||||
onerror: onError,
|
||||
onclose: onClose,
|
||||
signal: ctrl.signal
|
||||
})
|
||||
},
|
||||
|
||||
// 发送 send 消息
|
||||
delete: async (id: string) => {
|
||||
return await request.delete({ url: `/ai/chat/message/delete?id=${id}` })
|
||||
}
|
||||
}
|
|
@ -12,27 +12,32 @@ export interface ApiKeyVO {
|
|||
|
||||
// AI API 密钥 API
|
||||
export const ApiKeyApi = {
|
||||
// 查询AI API 密钥分页
|
||||
// 查询 API 密钥分页
|
||||
getApiKeyPage: async (params: any) => {
|
||||
return await request.get({ url: `/ai/api-key/page`, params })
|
||||
},
|
||||
|
||||
// 查询AI API 密钥详情
|
||||
// 获得 API 密钥列表
|
||||
getApiKeySimpleList: async () => {
|
||||
return await request.get({ url: `/ai/api-key/simple-list` })
|
||||
},
|
||||
|
||||
// 查询 API 密钥详情
|
||||
getApiKey: async (id: number) => {
|
||||
return await request.get({ url: `/ai/api-key/get?id=` + id })
|
||||
},
|
||||
|
||||
// 新增AI API 密钥
|
||||
// 新增 API 密钥
|
||||
createApiKey: async (data: ApiKeyVO) => {
|
||||
return await request.post({ url: `/ai/api-key/create`, data })
|
||||
},
|
||||
|
||||
// 修改AI API 密钥
|
||||
// 修改 API 密钥
|
||||
updateApiKey: async (data: ApiKeyVO) => {
|
||||
return await request.put({ url: `/ai/api-key/update`, data })
|
||||
},
|
||||
|
||||
// 删除AI API 密钥
|
||||
// 删除 API 密钥
|
||||
deleteApiKey: async (id: number) => {
|
||||
return await request.delete({ url: `/ai/api-key/delete?id=` + id })
|
||||
}
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
import request from '@/config/axios'
|
||||
|
||||
// AI 聊天模型 VO
|
||||
export interface ChatModelVO {
|
||||
id: number // 编号
|
||||
keyId: number // API 秘钥编号
|
||||
name: string // 模型名字
|
||||
model: string // 模型标识
|
||||
platform: string // 模型平台
|
||||
sort: number // 排序
|
||||
status: number // 状态
|
||||
temperature: number // 温度参数
|
||||
maxTokens: number // 单条回复的最大 Token 数量
|
||||
maxContexts: number // 上下文的最大 Message 数量
|
||||
}
|
||||
|
||||
// AI 聊天模型 API
|
||||
export const ChatModelApi = {
|
||||
// 查询聊天模型分页
|
||||
getChatModelPage: async (params: any) => {
|
||||
return await request.get({ url: `/ai/chat-model/page`, params })
|
||||
},
|
||||
|
||||
// 获得聊天模型列表
|
||||
getChatModelSimpleList: async (status?: number) => {
|
||||
return await request.get({
|
||||
url: `/ai/chat-model/simple-list`,
|
||||
params: {
|
||||
status
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
// 查询聊天模型详情
|
||||
getChatModel: async (id: number) => {
|
||||
return await request.get({ url: `/ai/chat-model/get?id=` + id })
|
||||
},
|
||||
|
||||
// 新增聊天模型
|
||||
createChatModel: async (data: ChatModelVO) => {
|
||||
return await request.post({ url: `/ai/chat-model/create`, data })
|
||||
},
|
||||
|
||||
// 修改聊天模型
|
||||
updateChatModel: async (data: ChatModelVO) => {
|
||||
return await request.put({ url: `/ai/chat-model/update`, data })
|
||||
},
|
||||
|
||||
// 删除聊天模型
|
||||
deleteChatModel: async (id: number) => {
|
||||
return await request.delete({ url: `/ai/chat-model/delete?id=` + id })
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
import request from '@/config/axios'
|
||||
|
||||
// AI 聊天角色 VO
|
||||
export interface ChatRoleVO {
|
||||
id: number // 角色编号
|
||||
modelId: number // 模型编号
|
||||
name: string // 角色名称
|
||||
avatar: string // 角色头像
|
||||
category: string // 角色类别
|
||||
sort: number // 角色排序
|
||||
description: string // 角色描述
|
||||
systemMessage: string // 角色设定
|
||||
welcomeMessage: string // 角色设定
|
||||
publicStatus: boolean // 是否公开
|
||||
status: number // 状态
|
||||
}
|
||||
|
||||
// AI 聊天角色 分页请求 vo
|
||||
export interface ChatRolePageReqVO {
|
||||
name?: string // 角色名称
|
||||
category?: string // 角色类别
|
||||
publicStatus: boolean // 是否公开
|
||||
pageNo: number // 是否公开
|
||||
pageSize: number // 是否公开
|
||||
}
|
||||
|
||||
// AI 聊天角色 API
|
||||
export const ChatRoleApi = {
|
||||
// 查询聊天角色分页
|
||||
getChatRolePage: async (params: any) => {
|
||||
return await request.get({ url: `/ai/chat-role/page`, params })
|
||||
},
|
||||
|
||||
// 查询聊天角色详情
|
||||
getChatRole: async (id: number) => {
|
||||
return await request.get({ url: `/ai/chat-role/get?id=` + id })
|
||||
},
|
||||
|
||||
// 新增聊天角色
|
||||
createChatRole: async (data: ChatRoleVO) => {
|
||||
return await request.post({ url: `/ai/chat-role/create`, data })
|
||||
},
|
||||
|
||||
// 修改聊天角色
|
||||
updateChatRole: async (data: ChatRoleVO) => {
|
||||
return await request.put({ url: `/ai/chat-role/update`, data })
|
||||
},
|
||||
|
||||
// 删除聊天角色
|
||||
deleteChatRole: async (id: number) => {
|
||||
return await request.delete({ url: `/ai/chat-role/delete?id=` + id })
|
||||
},
|
||||
|
||||
// ======= chat 聊天
|
||||
|
||||
// 获取 my role
|
||||
getMyPage: async (params: ChatRolePageReqVO) => {
|
||||
return await request.get({ url: `/ai/chat-role/my-page`, params})
|
||||
},
|
||||
|
||||
// 获取角色分类
|
||||
getCategoryList: async () => {
|
||||
return await request.get({ url: `/ai/chat-role/category-list`})
|
||||
},
|
||||
|
||||
// 创建角色
|
||||
createMy: async (data: ChatRoleVO) => {
|
||||
return await request.post({ url: `/ai/chat-role/create-my`, data})
|
||||
},
|
||||
|
||||
// 更新角色
|
||||
updateMy: async (data: ChatRoleVO) => {
|
||||
return await request.put({ url: `/ai/chat-role/update-my`, data})
|
||||
},
|
||||
|
||||
// 删除角色 my
|
||||
deleteMy: async (id: number) => {
|
||||
return await request.delete({ url: `/ai/chat-role/delete-my?id=` + id })
|
||||
},
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1715606039621" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4256" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M878.250667 981.333333H375.338667a104.661333 104.661333 0 0 1-104.661334-104.661333V375.338667a104.661333 104.661333 0 0 1 104.661334-104.661334h502.912a104.661333 104.661333 0 0 1 104.661333 104.661334v502.912C981.333333 934.485333 934.485333 981.333333 878.250667 981.333333zM375.338667 364.373333a10.666667 10.666667 0 0 0-10.922667 10.965334v502.912c0 6.229333 4.693333 10.922667 10.922667 10.922666h502.912a10.666667 10.666667 0 0 0 10.922666-10.922666V375.338667a10.666667 10.666667 0 0 0-10.922666-10.922667H375.338667z" fill="#ffffff" p-id="4257"></path><path d="M192.597333 753.322667H147.328A104.661333 104.661333 0 0 1 42.666667 648.661333V147.328A104.661333 104.661333 0 0 1 147.328 42.666667H650.24a104.661333 104.661333 0 0 1 104.618667 104.661333v49.962667c0 26.538667-20.309333 46.848-46.848 46.848a46.037333 46.037333 0 0 1-46.848-46.848V147.328a10.666667 10.666667 0 0 0-10.922667-10.965333H147.328a10.666667 10.666667 0 0 0-10.965333 10.965333V650.24c0 6.229333 4.693333 10.922667 10.965333 10.922667h45.269333c26.538667 0 46.848 20.309333 46.848 46.848 0 26.538667-21.845333 45.312-46.848 45.312z" fill="#ffffff" p-id="4258"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1715352878351" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1499" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M624.5 786.3c92.9 0 168.2-75.3 168.2-168.2V309c0-92.4-75.3-168.2-168.2-168.2H303.6c-92.4 0-168.2 75.3-168.2 168.2v309.1c0 92.4 75.3 168.2 168.2 168.2h320.9zM178.2 618.1V309c0-69.4 56.1-125.5 125.5-125.5h320.9c69.4 0 125.5 56.1 125.5 125.5v309.1c0 69.4-56.1 125.5-125.5 125.5h-321c-69.4 0-125.4-56.1-125.4-125.5z" p-id="1500" fill="#8a8a8a"></path><path d="M849.8 295.1v361.5c0 102.7-83.6 186.3-186.3 186.3H279.1v42.7h384.4c126.3 0 229.1-102.8 229.1-229.1V295.1h-42.8zM307.9 361.8h312.3c11.8 0 21.4-9.6 21.4-21.4 0-11.8-9.6-21.4-21.4-21.4H307.9c-11.8 0-21.4 9.6-21.4 21.4 0 11.9 9.6 21.4 21.4 21.4zM307.9 484.6h312.3c11.8 0 21.4-9.6 21.4-21.4 0-11.8-9.6-21.4-21.4-21.4H307.9c-11.8 0-21.4 9.6-21.4 21.4 0 11.9 9.6 21.4 21.4 21.4z" p-id="1501" fill="#8a8a8a"></path><path d="M620.2 607.4c11.8 0 21.4-9.6 21.4-21.4 0-11.8-9.6-21.4-21.4-21.4H307.9c-11.8 0-21.4 9.6-21.4 21.4 0 11.8 9.6 21.4 21.4 21.4h312.3z" p-id="1502" fill="#8a8a8a"></path></svg>
|
After Width: | Height: | Size: 1.2 KiB |
|
@ -0,0 +1 @@
|
|||
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1715354120346" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3256" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M907.1 263.7H118.9c-9.1 0-16.4-7.3-16.4-16.4s7.3-16.4 16.4-16.4H907c9.1 0 16.4 7.3 16.4 16.4s-7.3 16.4-16.3 16.4z" fill="#8a8a8a" p-id="3257"></path><path d="M772.5 928.3H257.4c-27.7 0-50.2-22.5-50.2-50.2V247.2c0-9.1 7.3-16.4 16.4-16.4H801c12.1 0 21.9 9.8 21.9 21.9v625.2c0 27.8-22.6 50.4-50.4 50.4zM240 263.7v614.4c0 9.6 7.8 17.4 17.4 17.4h515.2c9.7 0 17.5-7.9 17.5-17.5V263.7H240zM657.4 131.1H368.6c-9.1 0-16.4-7.3-16.4-16.4s7.3-16.4 16.4-16.4h288.7c9.1 0 16.4 7.3 16.4 16.4s-7.3 16.4-16.3 16.4z" fill="#8a8a8a" p-id="3258"></path><path d="M416 754.5c-9.1 0-16.4-7.3-16.4-16.4V517.8c0-9.1 7.3-16.4 16.4-16.4s16.4 7.3 16.4 16.4V738c0.1 9.1-7.3 16.5-16.4 16.5z" fill="#8a8a8a" p-id="3259"></path><path d="M416 465.2c-9.1 0-16.4-7.3-16.4-16.4v-59.4c0-9.1 7.3-16.4 16.4-16.4s16.4 7.3 16.4 16.4v59.4c0.1 9.1-7.3 16.4-16.4 16.4zM604.9 754.5c-9.1 0-16.4-7.3-16.4-16.4v-67.2c0-9.1 7.3-16.4 16.4-16.4s16.4 7.3 16.4 16.4V738c0 9.1-7.3 16.5-16.4 16.5z" fill="#8a8a8a" opacity=".4" p-id="3260"></path><path d="M604.9 619.1c-9.1 0-16.4-7.3-16.4-16.4V389.4c0-9.1 7.3-16.4 16.4-16.4s16.4 7.3 16.4 16.4v213.3c0 9.1-7.3 16.4-16.4 16.4z" fill="#8a8a8a" p-id="3261"></path></svg>
|
After Width: | Height: | Size: 1.5 KiB |
|
@ -0,0 +1,213 @@
|
|||
|
||||
<template>
|
||||
<div v-html="contentHtml"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {marked} from 'marked'
|
||||
import 'highlight.js/styles/vs2015.min.css'
|
||||
import hljs from 'highlight.js'
|
||||
import {ref} from "vue";
|
||||
|
||||
// 代码高亮:https://highlightjs.org/
|
||||
// 转换 markdown:marked
|
||||
|
||||
// marked 渲染器
|
||||
const renderer = {
|
||||
code(code, language, c) {
|
||||
const highlightHtml = hljs.highlight(code, {language: language, ignoreIllegals: true}).value
|
||||
const copyHtml = `<div id="copy" data-copy='${code}' style="position: absolute; right: 10px; top: 5px; color: #fff;cursor: pointer;">复制</div>`
|
||||
return `<pre>${copyHtml}<code class="hljs">${highlightHtml}</code></pre>`
|
||||
}
|
||||
}
|
||||
|
||||
// 配置 marked
|
||||
marked.use({
|
||||
renderer: renderer
|
||||
})
|
||||
|
||||
// 渲染的html内容
|
||||
const contentHtml = ref<any>()
|
||||
|
||||
// 定义组件属性
|
||||
const props = defineProps({
|
||||
content: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
// 将 props 变为引用类型
|
||||
const { content } = toRefs(props)
|
||||
|
||||
// 监听 content 变化
|
||||
watch(content, async (newValue, oldValue) => {
|
||||
await renderMarkdown(newValue);
|
||||
})
|
||||
|
||||
// 渲染 markdown
|
||||
const renderMarkdown = async (content: string) => {
|
||||
contentHtml.value = await marked(content)
|
||||
}
|
||||
|
||||
// 组件挂在时
|
||||
onMounted(async () => {
|
||||
// 解析转换 markdown
|
||||
await renderMarkdown(props.content as string);
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped lang="scss">
|
||||
.markdown-view {
|
||||
font-family: PingFang SC;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.6rem;
|
||||
letter-spacing: 0em;
|
||||
text-align: left;
|
||||
color: #3b3e55;
|
||||
max-width: 100%;
|
||||
|
||||
pre {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
pre code.hljs {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
code.hljs {
|
||||
border-radius: 6px;
|
||||
padding-top: 20px;
|
||||
width: auto;
|
||||
@media screen and (min-width: 1536px) {
|
||||
width: 960px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1536px) and (min-width: 1024px) {
|
||||
width: calc(100vw - 400px - 64px - 32px * 2);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1024px) and (min-width: 768px) {
|
||||
width: calc(100vw - 32px * 2);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
width: calc(100vw - 16px * 2);
|
||||
}
|
||||
}
|
||||
|
||||
p,
|
||||
code.hljs {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
p {
|
||||
//margin-bottom: 1rem !important;
|
||||
margin: 0;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
/* 标题通用格式 */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
color: var(--color-G900);
|
||||
margin: 24px 0 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 16px;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
/* 列表(有序,无序) */
|
||||
ul,
|
||||
ol {
|
||||
margin: 0 0 8px 0;
|
||||
padding: 0;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
color: #3b3e55; // var(--color-CG600);
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 4px 0 0 20px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol > li {
|
||||
list-style-type: decimal;
|
||||
margin-bottom: 1rem;
|
||||
// 表达式,修复有序列表序号展示不全的问题
|
||||
// &:nth-child(n + 10) {
|
||||
// margin-left: 30px;
|
||||
// }
|
||||
|
||||
// &:nth-child(n + 100) {
|
||||
// margin-left: 30px;
|
||||
// }
|
||||
}
|
||||
|
||||
ul > li {
|
||||
list-style-type: disc;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
margin-right: 11px;
|
||||
margin-bottom: 1rem;
|
||||
color: #3b3e55; // var(--color-G900);
|
||||
}
|
||||
|
||||
ol ul,
|
||||
ol ul > li,
|
||||
ul ul,
|
||||
ul ul li {
|
||||
// list-style: circle;
|
||||
font-size: 16px;
|
||||
list-style: none;
|
||||
margin-left: 6px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ul ul ul,
|
||||
ul ul ul li,
|
||||
ol ol,
|
||||
ol ol > li,
|
||||
ol ul ul,
|
||||
ol ul ul > li,
|
||||
ul ol,
|
||||
ul ol > li {
|
||||
list-style: square;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -79,13 +79,13 @@ const resetFlowCondition = () => {
|
|||
bpmnElement.value = bpmnInstances().bpmnElement
|
||||
bpmnElementSource.value = bpmnElement.value.source
|
||||
bpmnElementSourceRef.value = bpmnElement.value.businessObject.sourceRef
|
||||
// 初始化默认type为default
|
||||
flowConditionForm.value = { type: 'default' }
|
||||
if (
|
||||
bpmnElementSourceRef.value &&
|
||||
bpmnElementSourceRef.value.default &&
|
||||
bpmnElementSourceRef.value.default.id === bpmnElement.value.id &&
|
||||
flowConditionForm.value.type == 'default'
|
||||
bpmnElementSourceRef.value.default.id === bpmnElement.value.id
|
||||
) {
|
||||
// 默认
|
||||
flowConditionForm.value = { type: 'default' }
|
||||
} else if (!bpmnElement.value.businessObject.conditionExpression) {
|
||||
// 普通
|
||||
|
|
|
@ -29,7 +29,7 @@ const download = {
|
|||
html: (data: Blob, fileName: string) => {
|
||||
download0(data, fileName, 'text/html')
|
||||
},
|
||||
// 下载 Markdown 方法
|
||||
// 下载 MarkdownView 方法
|
||||
markdown: (data: Blob, fileName: string) => {
|
||||
download0(data, fileName, 'text/markdown')
|
||||
}
|
||||
|
|
|
@ -64,7 +64,7 @@
|
|||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="24" style="padding-right: 10px; padding-left: 10px">
|
||||
<el-form-item v-if="loginData.tenantEnable === 'true'" prop="tenantName">
|
||||
<el-form-item v-if="loginData.tenantEnable" prop="tenantName">
|
||||
<el-input
|
||||
v-model="loginData.loginForm.tenantName"
|
||||
:placeholder="t('login.tenantNamePlaceholder')"
|
||||
|
@ -207,7 +207,7 @@ const loginData = reactive({
|
|||
// 获取验证码
|
||||
const getCode = async () => {
|
||||
// 情况一,未开启:则直接登录
|
||||
if (loginData.captchaEnable) {
|
||||
if (!loginData.captchaEnable) {
|
||||
await handleLogin({})
|
||||
} else {
|
||||
// 情况二,已开启:则展示验证码;只有完成验证码的情况,才进行登录
|
||||
|
|
|
@ -0,0 +1,141 @@
|
|||
<template>
|
||||
<Dialog title="设定" v-model="dialogVisible">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="130px"
|
||||
v-loading="formLoading"
|
||||
>
|
||||
<el-form-item label="角色设定" prop="systemMessage">
|
||||
<el-input type="textarea" v-model="formData.systemMessage" placeholder="请输入角色设定" />
|
||||
</el-form-item>
|
||||
<el-form-item label="模型" prop="modelId">
|
||||
<el-select v-model="formData.modelId" placeholder="请选择模型">
|
||||
<el-option
|
||||
v-for="chatModel in chatModelList"
|
||||
:key="chatModel.id"
|
||||
:label="chatModel.name"
|
||||
:value="chatModel.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="温度参数" prop="temperature">
|
||||
<el-input-number
|
||||
v-model="formData.temperature"
|
||||
placeholder="请输入温度参数"
|
||||
:min="0"
|
||||
:max="2"
|
||||
:precision="2"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="回复数 Token 数" prop="maxTokens">
|
||||
<el-input-number
|
||||
v-model="formData.maxTokens"
|
||||
placeholder="请输入回复数 Token 数"
|
||||
:min="0"
|
||||
:max="4096"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="上下文数量" prop="maxContexts">
|
||||
<el-input-number
|
||||
v-model="formData.maxContexts"
|
||||
placeholder="请输入上下文数量"
|
||||
:min="0"
|
||||
:max="20"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { CommonStatusEnum } from '@/utils/constants'
|
||||
import { ChatModelApi, ChatModelVO } from '@/api/ai/model/chatModel'
|
||||
import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation'
|
||||
|
||||
/** AI 聊天角色 表单 */
|
||||
defineOptions({ name: 'ChatConversationUpdateForm' })
|
||||
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const dialogVisible = ref(false) // 弹窗的是否展示
|
||||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
const formData = ref({
|
||||
id: undefined,
|
||||
systemMessage: undefined,
|
||||
modelId: undefined,
|
||||
temperature: undefined,
|
||||
maxTokens: undefined,
|
||||
maxContexts: undefined
|
||||
})
|
||||
const formRules = reactive({
|
||||
modelId: [{ required: true, message: '模型不能为空', trigger: 'blur' }],
|
||||
status: [{ required: true, message: '状态不能为空', trigger: 'blur' }],
|
||||
temperature: [{ required: true, message: '温度参数不能为空', trigger: 'blur' }],
|
||||
maxTokens: [{ required: true, message: '回复数 Token 数不能为空', trigger: 'blur' }],
|
||||
maxContexts: [{ required: true, message: '上下文数量不能为空', trigger: 'blur' }]
|
||||
})
|
||||
const formRef = ref() // 表单 Ref
|
||||
const chatModelList = ref([] as ChatModelVO[]) // 聊天模型列表
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = async (id: number) => {
|
||||
dialogVisible.value = true
|
||||
resetForm()
|
||||
// 修改时,设置数据
|
||||
if (id) {
|
||||
formLoading.value = true
|
||||
try {
|
||||
const data = await ChatConversationApi.getChatConversationMy(id)
|
||||
formData.value = Object.keys(formData.value).reduce((obj, key) => {
|
||||
if (data.hasOwnProperty(key)) {
|
||||
obj[key] = data[key]
|
||||
}
|
||||
return obj
|
||||
}, {})
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
// 获得下拉数据
|
||||
chatModelList.value = await ChatModelApi.getChatModelSimpleList(CommonStatusEnum.ENABLE)
|
||||
}
|
||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||
|
||||
/** 提交表单 */
|
||||
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
|
||||
const submitForm = async () => {
|
||||
// 校验表单
|
||||
await formRef.value.validate()
|
||||
// 提交请求
|
||||
formLoading.value = true
|
||||
try {
|
||||
const data = formData.value as unknown as ChatConversationVO
|
||||
await ChatConversationApi.updateChatConversationMy(data)
|
||||
message.success('对话配置已更新')
|
||||
dialogVisible.value = false
|
||||
// 发送操作成功的事件
|
||||
emit('success')
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置表单 */
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
systemMessage: undefined,
|
||||
modelId: undefined,
|
||||
temperature: undefined,
|
||||
maxTokens: undefined,
|
||||
maxContexts: undefined
|
||||
}
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,50 @@
|
|||
<!-- header -->
|
||||
<template>
|
||||
<el-header class="chat-header">
|
||||
<div class="title">
|
||||
{{ title }}
|
||||
</div>
|
||||
<div class="title-right">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</el-header>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// 设置组件属性
|
||||
defineProps({
|
||||
title: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.chat-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
background-color: #ececec;
|
||||
width: 100%;
|
||||
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
overflow: hidden;
|
||||
color: #3e3e3e;
|
||||
max-width: 220px;
|
||||
}
|
||||
|
||||
.title-right {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
|
@ -0,0 +1,156 @@
|
|||
|
||||
<template>
|
||||
<div class="message-container" ref="messageContainer">
|
||||
<div class="chat-list" v-for="(item, index) in list" :key="index">
|
||||
<!-- 靠左 message -->
|
||||
<div class="left-message message-item" v-if="item.type === 'system'">
|
||||
<div class="avatar" >
|
||||
<el-avatar
|
||||
src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"
|
||||
/>
|
||||
</div>
|
||||
<div class="message">
|
||||
<div>
|
||||
<el-text class="time">{{formatDate(item.createTime)}}</el-text>
|
||||
</div>
|
||||
<div class="left-text-container">
|
||||
<el-text class="left-text">
|
||||
{{item.content}}
|
||||
</el-text>
|
||||
</div>
|
||||
<div class="left-btns">
|
||||
<div class="btn-cus" @click="noCopy(item.content)">
|
||||
<img class="btn-image" src="@/assets/ai/copy.svg"/>
|
||||
<el-text class="btn-cus-text">复制</el-text>
|
||||
</div>
|
||||
<div class="btn-cus" style="margin-left: 20px;" @click="onDelete(item.id)">
|
||||
<img class="btn-image" src="@/assets/ai/delete.svg" style="height: 17px;"/>
|
||||
<el-text class="btn-cus-text">删除</el-text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 靠右 message -->
|
||||
<div class="right-message message-item" v-if="item.type === 'user'">
|
||||
<div class="avatar">
|
||||
<el-avatar
|
||||
src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"
|
||||
/>
|
||||
</div>
|
||||
<div class="message">
|
||||
<div>
|
||||
<el-text class="time">{{formatDate(item.createTime)}}</el-text>
|
||||
</div>
|
||||
<div class="right-text-container">
|
||||
<el-text class="right-text">
|
||||
{{item.content}}
|
||||
</el-text>
|
||||
</div>
|
||||
<div class="right-btns">
|
||||
<div class="btn-cus" @click="noCopy(item.content)">
|
||||
<img class="btn-image" src="@/assets/ai/copy.svg"/>
|
||||
<el-text class="btn-cus-text">复制</el-text>
|
||||
</div>
|
||||
<div class="btn-cus" style="margin-left: 20px;" @click="onDelete(item.id)">
|
||||
<img class="btn-image" src="@/assets/ai/delete.svg" style="height: 17px;"/>
|
||||
<el-text class="btn-cus-text">删除</el-text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { ChatMessageApi, ChatMessageVO, ChatMessageSendVO} from '@/api/ai/chat/message'
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
|
||||
// 初始化 copy 到粘贴板
|
||||
const { copy, isSupported } = useClipboard();
|
||||
/** chat message 列表 */
|
||||
defineOptions({ name: 'chatMessageList' })
|
||||
const list = ref<ChatMessageVO[]>([]) // 列表的数据
|
||||
|
||||
// 对话id TODO @范 先写死
|
||||
const conversationId = '1781604279872581648'
|
||||
const content = '苹果是什么颜色?'
|
||||
|
||||
/** 查询列表 */
|
||||
const messageList = async () => {
|
||||
try {
|
||||
// 获取列表数据
|
||||
const res = await ChatMessageApi.messageList(conversationId)
|
||||
list.value = res;
|
||||
// 滚动到最下面
|
||||
scrollToBottom();
|
||||
} finally {
|
||||
}
|
||||
}
|
||||
// ref
|
||||
const messageContainer: any = ref(null);
|
||||
const isScrolling = ref(false)//用于判断用户是否在滚动
|
||||
|
||||
function scrollToBottom() {
|
||||
nextTick(() => {
|
||||
//注意要使用nexttick以免获取不到dom
|
||||
console.log('isScrolling.value', isScrolling.value)
|
||||
if (!isScrolling.value) {
|
||||
messageContainer.value.scrollTop = messageContainer.value.scrollHeight - messageContainer.value.offsetHeight
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
const scrollContainer = messageContainer.value
|
||||
const scrollTop = scrollContainer.scrollTop
|
||||
const scrollHeight = scrollContainer.scrollHeight
|
||||
const offsetHeight = scrollContainer.offsetHeight
|
||||
|
||||
if (scrollTop + offsetHeight < scrollHeight) {
|
||||
// 用户开始滚动并在最底部之上,取消保持在最底部的效果
|
||||
isScrolling.value = true
|
||||
} else {
|
||||
// 用户停止滚动并滚动到最底部,开启保持到最底部的效果
|
||||
isScrolling.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function noCopy(content) {
|
||||
copy(content)
|
||||
ElMessage({
|
||||
message: '复制成功!',
|
||||
type: 'success',
|
||||
})
|
||||
}
|
||||
|
||||
const onDelete = async (id) => {
|
||||
// 删除 message
|
||||
await ChatMessageApi.delete(id)
|
||||
ElMessage({
|
||||
message: '删除成功!',
|
||||
type: 'success',
|
||||
})
|
||||
// 重新获取 message 列表
|
||||
await messageList();
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(async () => {
|
||||
// 获取列表数据
|
||||
messageList();
|
||||
|
||||
// scrollToBottom();
|
||||
// await nextTick
|
||||
// 监听滚动事件,判断用户滚动状态
|
||||
messageContainer.value.addEventListener('scroll', handleScroll)
|
||||
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
|
||||
</style>
|
|
@ -4,8 +4,8 @@
|
|||
<el-aside width="260px" class="conversation-container">
|
||||
<div>
|
||||
<!-- 左顶部:新建对话 -->
|
||||
<el-button class="w-1/1 btn-new-conversation" type="primary">
|
||||
<Icon icon="ep:plus" class="mr-5px" />
|
||||
<el-button class="w-1/1 btn-new-conversation" type="primary" @click="createConversation">
|
||||
<Icon icon="ep:plus" class="mr-5px"/>
|
||||
新建对话
|
||||
</el-button>
|
||||
<!-- 左顶部:搜索对话 -->
|
||||
|
@ -17,45 +17,51 @@
|
|||
@keyup="searchConversation"
|
||||
>
|
||||
<template #prefix>
|
||||
<Icon icon="ep:search" />
|
||||
<Icon icon="ep:search"/>
|
||||
</template>
|
||||
</el-input>
|
||||
<!-- 左中间:对话列表 -->
|
||||
<div class="conversation-list" >
|
||||
<!-- TODO @芋艿,置顶、聊天记录、一星期钱、30天前,前端对数据重新做一下分组,或者后端接口改一下 -->
|
||||
<div>
|
||||
<el-text class="mx-1" size="small" tag="b">置顶</el-text>
|
||||
</div>
|
||||
<el-row v-for="conversation in conversationList" :key="conversation.id">
|
||||
<div
|
||||
:class="conversation.id === conversationId ? 'conversation active' : 'conversation'"
|
||||
@click="changeConversation(conversation)"
|
||||
>
|
||||
<div class="title-wrapper">
|
||||
<img class="avatar" :src="conversation.avatar" />
|
||||
<span class="title">{{ conversation.title }}</span>
|
||||
</div>
|
||||
<div class="button-wrapper">
|
||||
<el-icon title="编辑" @click="updateConversationTitle(conversation)">
|
||||
<Icon icon="ep:edit" />
|
||||
</el-icon>
|
||||
<el-icon title="删除会话" @click="deleteConversationTitle(conversation)">
|
||||
<Icon icon="ep:delete" />
|
||||
</el-icon>
|
||||
</div>
|
||||
<div class="conversation-list">
|
||||
<!-- TODO @fain:置顶、聊天记录、一星期钱、30天前,前端对数据重新做一下分组,或者后端接口改一下 -->
|
||||
<div v-for="conversationKey in Object.keys(conversationMap)" :key="conversationKey" >
|
||||
<div v-if="conversationMap[conversationKey].length">
|
||||
<el-text class="mx-1" size="small" tag="b">{{conversationKey}}</el-text>
|
||||
</div>
|
||||
</el-row>
|
||||
<el-row
|
||||
v-for="conversation in conversationMap[conversationKey]"
|
||||
:key="conversation.id"
|
||||
@click="handleConversationClick(conversation.id)">
|
||||
<div
|
||||
:class="conversation.id === conversationId ? 'conversation active' : 'conversation'"
|
||||
@click="changeConversation(conversation.id)"
|
||||
>
|
||||
<div class="title-wrapper">
|
||||
<img class="avatar" :src="conversation.roleAvatar"/>
|
||||
<span class="title">{{ conversation.title }}</span>
|
||||
</div>
|
||||
<!-- TODO @fan:缺一个【置顶】按钮,效果改成 hover 上去展示 -->
|
||||
<div class="button-wrapper">
|
||||
<el-icon title="编辑" @click="updateConversationTitle(conversation)">
|
||||
<Icon icon="ep:edit"/>
|
||||
</el-icon>
|
||||
<el-icon title="删除会话" @click="deleteChatConversation(conversation)">
|
||||
<Icon icon="ep:delete"/>
|
||||
</el-icon>
|
||||
</div>
|
||||
</div>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 左底部:工具栏 -->
|
||||
<div class="tool-box">
|
||||
<div>
|
||||
<Icon icon="ep:user" />
|
||||
<div @click="handleRoleRepository">
|
||||
<Icon icon="ep:user"/>
|
||||
<el-text size="small">角色仓库</el-text>
|
||||
</div>
|
||||
<div>
|
||||
<Icon icon="ep:delete" />
|
||||
<el-text size="small" >清空未置顶对话</el-text>
|
||||
<div @click="handleClearConversation">
|
||||
<Icon icon="ep:delete"/>
|
||||
<el-text size="small">清空未置顶对话</el-text>
|
||||
</div>
|
||||
</div>
|
||||
</el-aside>
|
||||
|
@ -64,73 +70,579 @@
|
|||
<!-- 右顶部 TODO 芋艿:右对齐 -->
|
||||
<el-header class="header">
|
||||
<div class="title">
|
||||
标题......
|
||||
{{ useConversation?.title }}
|
||||
</div>
|
||||
<div>
|
||||
<el-button>3.5-turbo-0125 <Icon icon="ep:setting" /></el-button>
|
||||
<el-button>
|
||||
<Icon icon="ep:user" />
|
||||
<!-- TODO @fan:样式改下;这里我已经改成点击后,弹出了 -->
|
||||
<el-button type="primary" @click="openChatConversationUpdateForm">
|
||||
<span v-html="useConversation?.modelName"></span>
|
||||
<Icon icon="ep:setting" style="margin-left: 10px"/>
|
||||
</el-button>
|
||||
<el-button>
|
||||
<Icon icon="ep:download" />
|
||||
<Icon icon="ep:user"/>
|
||||
</el-button>
|
||||
<el-button>
|
||||
<Icon icon="ep:arrow-up" />
|
||||
<Icon icon="ep:download"/>
|
||||
</el-button>
|
||||
<el-button>
|
||||
<Icon icon="ep:arrow-up"/>
|
||||
</el-button>
|
||||
</div>
|
||||
</el-header>
|
||||
<el-main>对话列表</el-main>
|
||||
<el-footer>
|
||||
<el-input
|
||||
class="prompt-input"
|
||||
type="textarea"
|
||||
placeholder="请输入提示词..."
|
||||
/>
|
||||
|
||||
<!-- main -->
|
||||
<el-main class="main-container">
|
||||
<div class="message-container" ref="messageContainer">
|
||||
<div class="chat-list" v-for="(item, index) in list" :key="index">
|
||||
<!-- 靠左 message -->
|
||||
<!-- TODO 芋艿:类型判断 -->
|
||||
<div class="left-message message-item" v-if="item.type === 'system'">
|
||||
<div class="avatar">
|
||||
<el-avatar
|
||||
src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"
|
||||
/>
|
||||
</div>
|
||||
<div class="message">
|
||||
<div>
|
||||
<el-text class="time">{{ formatDate(item.createTime) }}</el-text>
|
||||
</div>
|
||||
<div class="left-text-container" ref="markdownViewRef">
|
||||
<!-- <div class="left-text markdown-view" v-html="item.content"></div>-->
|
||||
<!-- <mdPreview :content="item.content" :delay="false" />-->
|
||||
<MarkdownView class="left-text" :content="item.content" />
|
||||
</div>
|
||||
<div class="left-btns">
|
||||
<div class="btn-cus" @click="noCopy(item.content)">
|
||||
<img class="btn-image" src="../../../assets/ai/copy.svg"/>
|
||||
<el-text class="btn-cus-text">复制</el-text>
|
||||
</div>
|
||||
<div class="btn-cus" style="margin-left: 20px" @click="onDelete(item.id)">
|
||||
<img class="btn-image" src="@/assets/ai/delete.svg" style="height: 17px"/>
|
||||
<el-text class="btn-cus-text">删除</el-text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 靠右 message -->
|
||||
<div class="right-message message-item" v-if="item.type === 'user'">
|
||||
<div class="avatar">
|
||||
<el-avatar
|
||||
src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"
|
||||
/>
|
||||
</div>
|
||||
<div class="message">
|
||||
<div>
|
||||
<el-text class="time">{{ formatDate(item.createTime) }}</el-text>
|
||||
</div>
|
||||
<div class="right-text-container">
|
||||
<div class="right-text">{{ item.content }}</div>
|
||||
<!-- <MarkdownView class="right-text" :content="item.content" />-->
|
||||
</div>
|
||||
<div class="right-btns">
|
||||
<div class="btn-cus" @click="noCopy(item.content)">
|
||||
<img class="btn-image" src="@/assets/ai/copy.svg"/>
|
||||
<el-text class="btn-cus-text">复制</el-text>
|
||||
</div>
|
||||
<div class="btn-cus" style="margin-left: 20px" @click="onDelete(item.id)">
|
||||
<img class="btn-image" src="@/assets/ai/delete.svg" style="height: 17px"/>
|
||||
<el-text class="btn-cus-text">删除</el-text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 角色仓库抽屉 -->
|
||||
<el-drawer v-model="drawer" title="角色仓库" size="50%">
|
||||
<Role/>
|
||||
</el-drawer>
|
||||
</el-main>
|
||||
<el-footer class="footer-container">
|
||||
<form @submit.prevent="onSend" class="prompt-from">
|
||||
<textarea
|
||||
class="prompt-input"
|
||||
v-model="prompt"
|
||||
@keyup.enter="onSend"
|
||||
@input="onPromptInput"
|
||||
@compositionstart="onCompositionstart"
|
||||
@compositionend="onCompositionend"
|
||||
placeholder="问我任何问题...(Shift+Enter 换行,按下 Enter 发送)"
|
||||
></textarea>
|
||||
<div class="prompt-btns">
|
||||
<el-switch/>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="default"
|
||||
@click="onSend()"
|
||||
:loading="conversationInProgress"
|
||||
v-if="conversationInProgress == false"
|
||||
>
|
||||
{{ conversationInProgress ? '进行中' : '发送' }}
|
||||
</el-button>
|
||||
<el-button
|
||||
type="danger"
|
||||
size="default"
|
||||
@click="stopStream()"
|
||||
v-if="conversationInProgress == true"
|
||||
>
|
||||
停止
|
||||
</el-button>
|
||||
</div>
|
||||
</form>
|
||||
</el-footer>
|
||||
</el-container>
|
||||
</el-container>
|
||||
|
||||
<ChatConversationUpdateForm
|
||||
ref="chatConversationUpdateFormRef"
|
||||
@success="getChatConversationList"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const conversationList = [
|
||||
{
|
||||
id: 1,
|
||||
title: '测试标题',
|
||||
avatar:
|
||||
'http://test.yudao.iocoder.cn/96c787a2ce88bf6d0ce3cd8b6cf5314e80e7703cd41bf4af8cd2e2909dbd6b6d.png'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '测试对话',
|
||||
avatar:
|
||||
'http://test.yudao.iocoder.cn/96c787a2ce88bf6d0ce3cd8b6cf5314e80e7703cd41bf4af8cd2e2909dbd6b6d.png'
|
||||
import MarkdownView from '@/components/MarkdownView/index.vue'
|
||||
import {ChatMessageApi, ChatMessageVO} from '@/api/ai/chat/message'
|
||||
import {ChatConversationApi, ChatConversationVO} from '@/api/ai/chat/conversation'
|
||||
import ChatConversationUpdateForm from './components/ChatConversationUpdateForm.vue'
|
||||
import Role from '@/views/ai/chat/role/index.vue'
|
||||
import {formatDate} from '@/utils/formatTime'
|
||||
import {useClipboard} from '@vueuse/core'
|
||||
|
||||
const route = useRoute() // 路由
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const conversationList = ref([] as ChatConversationVO[])
|
||||
const conversationMap = ref<any>({})
|
||||
// 初始化 copy 到粘贴板
|
||||
const {copy} = useClipboard()
|
||||
|
||||
const drawer = ref<boolean>(false) // 角色仓库抽屉
|
||||
const searchName = ref('') // 查询的内容
|
||||
const inputTimeout = ref<any>() // 处理输入中回车的定时器
|
||||
const conversationId = ref<number | null>(null) // 选中的对话编号
|
||||
const conversationInProgress = ref(false) // 对话进行中
|
||||
const conversationInAbortController = ref<any>() // 对话进行中 abort 控制器(控制 stream 对话)
|
||||
|
||||
const prompt = ref<string>() // prompt
|
||||
|
||||
// 判断 消息列表 滚动的位置(用于判断是否需要滚动到消息最下方)
|
||||
const messageContainer: any = ref(null)
|
||||
const isScrolling = ref(false) //用于判断用户是否在滚动
|
||||
const isComposing = ref(false) // 判断用户是否在输入
|
||||
|
||||
/** chat message 列表 */
|
||||
// defineOptions({ name: 'chatMessageList' })
|
||||
const list = ref<ChatMessageVO[]>([]) // 列表的数据
|
||||
const useConversation = ref<ChatConversationVO | null>(null) // 使用的 Conversation
|
||||
|
||||
/** 新建对话 */
|
||||
const createConversation = async () => {
|
||||
// 新建对话
|
||||
const conversationId = await ChatConversationApi.createChatConversationMy(
|
||||
{} as unknown as ChatConversationVO
|
||||
)
|
||||
changeConversation(conversationId)
|
||||
// 刷新对话列表
|
||||
await getChatConversationList()
|
||||
}
|
||||
|
||||
const changeConversation = (id: number) => {
|
||||
// 切换对话
|
||||
conversationId.value = id
|
||||
// TODO 芋艿:待实现
|
||||
// 刷新 message 列表
|
||||
messageList()
|
||||
}
|
||||
|
||||
/** 更新聊天会话的标题 */
|
||||
const updateConversationTitle = async (conversation: ChatConversationVO) => {
|
||||
// 二次确认
|
||||
const {value} = await ElMessageBox.prompt('修改标题', {
|
||||
inputPattern: /^[\s\S]*.*\S[\s\S]*$/, // 判断非空,且非空格
|
||||
inputErrorMessage: '标题不能为空',
|
||||
inputValue: conversation.title
|
||||
})
|
||||
// 发起修改
|
||||
await ChatConversationApi.updateChatConversationMy({
|
||||
id: conversation.id,
|
||||
title: value
|
||||
} as ChatConversationVO)
|
||||
message.success('重命名成功')
|
||||
// 刷新列表
|
||||
await getChatConversationList()
|
||||
}
|
||||
|
||||
/** 删除聊天会话 */
|
||||
const deleteChatConversation = async (conversation: ChatConversationVO) => {
|
||||
try {
|
||||
// 删除的二次确认
|
||||
await message.delConfirm(`是否确认删除会话 - ${conversation.title}?`)
|
||||
// 发起删除
|
||||
await ChatConversationApi.deleteChatConversationMy(conversation.id)
|
||||
message.success('会话已删除')
|
||||
// 刷新列表
|
||||
await getChatConversationList()
|
||||
} catch {
|
||||
}
|
||||
]
|
||||
const conversationId = ref(1)
|
||||
const searchName = ref('')
|
||||
const leftHeight = window.innerHeight - 240 // TODO 芋艿:这里还不太对
|
||||
|
||||
const changeConversation = (conversation) => {
|
||||
console.log(conversation)
|
||||
conversationId.value = conversation.id
|
||||
// TODO 芋艿:待实现
|
||||
}
|
||||
|
||||
const updateConversationTitle = (conversation) => {
|
||||
console.log(conversation)
|
||||
// TODO 芋艿:待实现
|
||||
}
|
||||
|
||||
const deleteConversationTitle = (conversation) => {
|
||||
console.log(conversation)
|
||||
// TODO 芋艿:待实现
|
||||
}
|
||||
|
||||
const searchConversation = () => {
|
||||
// TODO 芋艿:待实现
|
||||
// TODO fan:待实现
|
||||
}
|
||||
|
||||
/** send */
|
||||
const onSend = async () => {
|
||||
// 判断用户是否在输入
|
||||
if (isComposing.value) {
|
||||
return
|
||||
}
|
||||
// 进行中不允许发送
|
||||
if (conversationInProgress.value) {
|
||||
return
|
||||
}
|
||||
const content = prompt.value?.trim() + ''
|
||||
if (content.length < 2) {
|
||||
ElMessage({
|
||||
message: '请输入内容!',
|
||||
type: 'error'
|
||||
})
|
||||
return
|
||||
}
|
||||
// TODO 芋艿:这块交互要在优化;应该是先插入到 UI 界面,里面会有当前的消息,和正在思考中;之后发起请求;
|
||||
// 清空输入框
|
||||
prompt.value = ''
|
||||
// const requestParams = {
|
||||
// conversationId: conversationId.value,
|
||||
// content: content
|
||||
// } as unknown as ChatMessageSendVO
|
||||
// // 添加 message
|
||||
const userMessage = {
|
||||
conversationId: conversationId.value,
|
||||
content: content
|
||||
} as ChatMessageVO
|
||||
// list.value.push(userMessage)
|
||||
// // 滚动到住下面
|
||||
// scrollToBottom()
|
||||
// stream
|
||||
await doSendStream(userMessage)
|
||||
//
|
||||
}
|
||||
|
||||
const doSendStream = async (userMessage: ChatMessageVO) => {
|
||||
// 创建AbortController实例,以便中止请求
|
||||
conversationInAbortController.value = new AbortController()
|
||||
// 标记对话进行中
|
||||
conversationInProgress.value = true
|
||||
try {
|
||||
// 发送 event stream
|
||||
let isFirstMessage = true
|
||||
let content = ''
|
||||
ChatMessageApi.sendStream(
|
||||
userMessage.conversationId, // TODO 芋艿:这里可能要在优化;
|
||||
userMessage.content,
|
||||
conversationInAbortController.value,
|
||||
(message) => {
|
||||
console.log('message', message)
|
||||
const data = JSON.parse(message.data) // TODO 芋艿:类型处理;
|
||||
// debugger
|
||||
// 如果没有内容结束链接
|
||||
if (data.receive.content === '') {
|
||||
// 标记对话结束
|
||||
conversationInProgress.value = false
|
||||
// 结束 stream 对话
|
||||
conversationInAbortController.value.abort()
|
||||
}
|
||||
// 首次返回需要添加一个 message 到页面,后面的都是更新
|
||||
if (isFirstMessage) {
|
||||
isFirstMessage = false
|
||||
// debugger
|
||||
list.value.push(data.send)
|
||||
list.value.push(data.receive)
|
||||
} else {
|
||||
// debugger
|
||||
content = content + data.receive.content
|
||||
const lastMessage = list.value[list.value.length - 1]
|
||||
lastMessage.content = content
|
||||
list.value[list.value - 1] = lastMessage
|
||||
}
|
||||
// 滚动到最下面
|
||||
scrollToBottom()
|
||||
},
|
||||
(error) => {
|
||||
console.log('error', error)
|
||||
// 标记对话结束
|
||||
conversationInProgress.value = false
|
||||
// 结束 stream 对话
|
||||
conversationInAbortController.value.abort()
|
||||
},
|
||||
() => {
|
||||
console.log('close')
|
||||
// 标记对话结束
|
||||
conversationInProgress.value = false
|
||||
// 结束 stream 对话
|
||||
conversationInAbortController.value.abort()
|
||||
}
|
||||
)
|
||||
} finally {
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询列表 */
|
||||
const messageList = async () => {
|
||||
try {
|
||||
if (conversationId.value === null) {
|
||||
return
|
||||
}
|
||||
// 获取列表数据
|
||||
const res = await ChatMessageApi.messageList(conversationId.value)
|
||||
list.value = res
|
||||
|
||||
// 滚动到最下面
|
||||
scrollToBottom()
|
||||
} finally {
|
||||
}
|
||||
}
|
||||
|
||||
function scrollToBottom() {
|
||||
nextTick(() => {
|
||||
//注意要使用nexttick以免获取不到dom
|
||||
console.log('isScrolling.value', isScrolling.value)
|
||||
if (!isScrolling.value) {
|
||||
messageContainer.value.scrollTop =
|
||||
messageContainer.value.scrollHeight - messageContainer.value.offsetHeight
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function handleScroll() {
|
||||
const scrollContainer = messageContainer.value
|
||||
const scrollTop = scrollContainer.scrollTop
|
||||
const scrollHeight = scrollContainer.scrollHeight
|
||||
const offsetHeight = scrollContainer.offsetHeight
|
||||
|
||||
if (scrollTop + offsetHeight < scrollHeight) {
|
||||
// 用户开始滚动并在最底部之上,取消保持在最底部的效果
|
||||
isScrolling.value = true
|
||||
} else {
|
||||
// 用户停止滚动并滚动到最底部,开启保持到最底部的效果
|
||||
isScrolling.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function noCopy(content) {
|
||||
copy(content)
|
||||
ElMessage({
|
||||
message: '复制成功!',
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
|
||||
const onDelete = async (id) => {
|
||||
// 删除 message
|
||||
await ChatMessageApi.delete(id)
|
||||
ElMessage({
|
||||
message: '删除成功!',
|
||||
type: 'success'
|
||||
})
|
||||
// tip:如果 stream 进行中的 message,就需要调用 controller 结束
|
||||
stopStream()
|
||||
// 重新获取 message 列表
|
||||
await messageList()
|
||||
}
|
||||
|
||||
const stopStream = async () => {
|
||||
// tip:如果 stream 进行中的 message,就需要调用 controller 结束
|
||||
if (conversationInAbortController.value) {
|
||||
conversationInAbortController.value.abort()
|
||||
}
|
||||
// 设置为 false
|
||||
conversationInProgress.value = false
|
||||
}
|
||||
|
||||
/** 修改聊天会话 */
|
||||
const chatConversationUpdateFormRef = ref()
|
||||
const openChatConversationUpdateForm = async () => {
|
||||
chatConversationUpdateFormRef.value.open(conversationId.value)
|
||||
}
|
||||
|
||||
// 输入
|
||||
const onCompositionstart = () => {
|
||||
console.log('onCompositionstart。。。.')
|
||||
isComposing.value = true
|
||||
}
|
||||
|
||||
const onCompositionend = () => {
|
||||
// console.log('输入结束...')
|
||||
setTimeout(() => {
|
||||
console.log('输入结束...')
|
||||
isComposing.value = false
|
||||
}, 200)
|
||||
}
|
||||
|
||||
const onPromptInput = (event) => {
|
||||
// 非输入法 输入设置为 true
|
||||
if (!isComposing.value) {
|
||||
// 回车 event data 是 null
|
||||
if (event.data == null) {
|
||||
return
|
||||
}
|
||||
console.log('setTimeout 输入开始...')
|
||||
isComposing.value = true
|
||||
}
|
||||
// 清理定时器
|
||||
if (inputTimeout.value) {
|
||||
clearTimeout(inputTimeout.value)
|
||||
}
|
||||
// 重置定时器
|
||||
inputTimeout.value = setTimeout(() => {
|
||||
console.log('setTimeout 输入结束...')
|
||||
isComposing.value = false
|
||||
}, 400)
|
||||
}
|
||||
|
||||
const getConversation = async (conversationId: number | null) => {
|
||||
if (!conversationId) {
|
||||
return
|
||||
}
|
||||
// 获取对话信息
|
||||
useConversation.value = await ChatConversationApi.getChatConversationMy(conversationId)
|
||||
console.log('useConversation.value', useConversation.value)
|
||||
}
|
||||
|
||||
/** 获得聊天会话列表 */
|
||||
const getChatConversationList = async () => {
|
||||
conversationList.value = await ChatConversationApi.getChatConversationMyList()
|
||||
// 默认选中第一条
|
||||
if (conversationList.value.length === 0) {
|
||||
conversationId.value = null
|
||||
list.value = []
|
||||
} else {
|
||||
if (conversationId.value === null) {
|
||||
conversationId.value = conversationList.value[0].id
|
||||
changeConversation(conversationList.value[0].id)
|
||||
}
|
||||
}
|
||||
// map
|
||||
const groupRes = await conversationTimeGroup(conversationList.value)
|
||||
conversationMap.value = groupRes
|
||||
}
|
||||
|
||||
const conversationTimeGroup = async (list: ChatConversationVO[]) => {
|
||||
// 排序、指定、时间分组(今天、一天前、三天前、七天前、30天前)
|
||||
const groupMap = {
|
||||
'置顶': [],
|
||||
'今天': [],
|
||||
'一天前': [],
|
||||
'三天前': [],
|
||||
'七天前': [],
|
||||
'三十天前': []
|
||||
}
|
||||
// 当前时间的时间戳
|
||||
const now = Date.now();
|
||||
// 定义时间间隔常量(单位:毫秒)
|
||||
const oneDay = 24 * 60 * 60 * 1000;
|
||||
const threeDays = 3 * oneDay;
|
||||
const sevenDays = 7 * oneDay;
|
||||
const thirtyDays = 30 * oneDay;
|
||||
console.log('listlistlist', list)
|
||||
for (const conversation: ChatConversationVO of list) {
|
||||
// 置顶
|
||||
if (conversation.pinned) {
|
||||
groupMap['置顶'].push(conversation)
|
||||
continue
|
||||
}
|
||||
// 计算时间差(单位:毫秒)
|
||||
const diff = now - conversation.updateTime;
|
||||
// 根据时间间隔判断
|
||||
if (diff < oneDay) {
|
||||
groupMap['今天'].push(conversation)
|
||||
} else if (diff < threeDays) {
|
||||
groupMap['一天前'].push(conversation)
|
||||
} else if (diff < sevenDays) {
|
||||
groupMap['三天前'].push(conversation)
|
||||
} else if (diff < thirtyDays) {
|
||||
groupMap['七天前'].push(conversation)
|
||||
} else {
|
||||
groupMap['三十天前'].push(conversation)
|
||||
}
|
||||
}
|
||||
return groupMap
|
||||
}
|
||||
|
||||
|
||||
// 对话点击
|
||||
const handleConversationClick = async (id: number) => {
|
||||
// 切换对话
|
||||
conversationId.value = id
|
||||
console.log('conversationId.value', conversationId.value)
|
||||
// 获取列表数据
|
||||
await messageList()
|
||||
}
|
||||
|
||||
// 角色仓库
|
||||
const handleRoleRepository = async () => {
|
||||
drawer.value = !drawer.value
|
||||
}
|
||||
|
||||
// 清空对话
|
||||
const handleClearConversation = async () => {
|
||||
ElMessageBox.confirm(
|
||||
'确认后对话会全部清空,置顶的对话除外。',
|
||||
'确认提示',
|
||||
{
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
}
|
||||
)
|
||||
.then(async () => {
|
||||
await ChatConversationApi.deleteMyAllExceptPinned()
|
||||
ElMessage({
|
||||
message: '操作成功!',
|
||||
type: 'success'
|
||||
})
|
||||
// 清空选中的对话
|
||||
useConversation.value = null
|
||||
conversationId.value = null
|
||||
list.value = []
|
||||
// 获得聊天会话列表
|
||||
await getChatConversationList()
|
||||
})
|
||||
.catch(() => {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(async () => {
|
||||
// 设置当前对话
|
||||
if (route.query.conversationId) {
|
||||
conversationId.value = route.query.conversationId as number
|
||||
}
|
||||
// 获得聊天会话列表
|
||||
await getChatConversationList()
|
||||
// 获取对话信息
|
||||
await getConversation(conversationId.value)
|
||||
// 获取列表数据
|
||||
await messageList()
|
||||
// scrollToBottom();
|
||||
// await nextTick
|
||||
// 监听滚动事件,判断用户滚动状态
|
||||
messageContainer.value.addEventListener('scroll', handleScroll)
|
||||
// 添加 copy 监听
|
||||
messageContainer.value.addEventListener('click', (e: any) => {
|
||||
console.log(e)
|
||||
if (e.target.id === 'copy') {
|
||||
copy(e.target?.dataset?.copy)
|
||||
ElMessage({
|
||||
message: '复制成功!',
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
|
||||
.ai-layout {
|
||||
// TODO @范 这里height不能 100% 先这样临时处理
|
||||
position: absolute;
|
||||
|
@ -172,8 +684,10 @@ const searchConversation = () => {
|
|||
border-radius: 5px;
|
||||
align-items: center;
|
||||
line-height: 30px;
|
||||
|
||||
&.active {
|
||||
background-color: #e6e6e6;
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
}
|
||||
|
@ -184,6 +698,7 @@ const searchConversation = () => {
|
|||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
padding: 5px 10px;
|
||||
max-width: 220px;
|
||||
|
@ -192,6 +707,7 @@ const searchConversation = () => {
|
|||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
|
@ -199,6 +715,7 @@ const searchConversation = () => {
|
|||
flex-direction: row;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
// 对话编辑、删除
|
||||
.button-wrapper {
|
||||
right: 2px;
|
||||
|
@ -206,6 +723,7 @@ const searchConversation = () => {
|
|||
flex-direction: row;
|
||||
justify-items: center;
|
||||
color: #606266;
|
||||
|
||||
.el-icon {
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
@ -227,6 +745,8 @@ const searchConversation = () => {
|
|||
color: #606266;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
|
||||
> span {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
@ -234,6 +754,7 @@ const searchConversation = () => {
|
|||
}
|
||||
}
|
||||
|
||||
// 头部
|
||||
.detail-container {
|
||||
background: #ffffff;
|
||||
|
||||
|
@ -243,16 +764,176 @@ const searchConversation = () => {
|
|||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: #fbfbfb;
|
||||
box-shadow: 0 0 0 0 #dcdfe6;
|
||||
|
||||
.title {
|
||||
font-size: 23px;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// main 容器
|
||||
.main-container {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.message-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
//width: 100%;
|
||||
//height: 100%;
|
||||
overflow-y: scroll;
|
||||
padding: 0 15px;
|
||||
}
|
||||
|
||||
// 中间
|
||||
.chat-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: hidden;
|
||||
|
||||
.message-item {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.left-message {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.right-message {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
//height: 170px;
|
||||
//width: 170px;
|
||||
}
|
||||
|
||||
.message {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
text-align: left;
|
||||
margin: 0 15px;
|
||||
|
||||
.time {
|
||||
text-align: left;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.left-text-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-wrap: break-word;
|
||||
background-color: rgba(228, 228, 228, 0.8);
|
||||
box-shadow: 0 0 0 1px rgba(228, 228, 228, 0.8);
|
||||
border-radius: 10px;
|
||||
padding: 10px 10px 5px 10px;
|
||||
|
||||
.left-text {
|
||||
color: #393939;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
}
|
||||
|
||||
.right-text-container {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
|
||||
.right-text {
|
||||
font-size: 0.95rem;
|
||||
color: #fff;
|
||||
display: inline;
|
||||
background-color: #267fff;
|
||||
color: #fff;
|
||||
box-shadow: 0 0 0 1px #267fff;
|
||||
border-radius: 10px;
|
||||
padding: 10px;
|
||||
width: auto;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.left-btns,
|
||||
.right-btns {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
// 复制、删除按钮
|
||||
.btn-cus {
|
||||
display: flex;
|
||||
background-color: transparent;
|
||||
align-items: center;
|
||||
|
||||
.btn-image {
|
||||
height: 20px;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.btn-cus-text {
|
||||
color: #757575;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-cus:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.btn-cus:focus {
|
||||
background-color: #8c939d;
|
||||
}
|
||||
}
|
||||
|
||||
// 底部
|
||||
.footer-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
|
||||
.prompt-from {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: auto;
|
||||
border: 1px solid #e3e3e3;
|
||||
border-radius: 10px;
|
||||
margin: 20px 20px;
|
||||
padding: 9px 10px;
|
||||
}
|
||||
|
||||
.prompt-input {
|
||||
height: 80px;
|
||||
//box-shadow: none;
|
||||
border: none;
|
||||
box-sizing: border-box;
|
||||
resize: none;
|
||||
padding: 0px 2px;
|
||||
//padding: 5px 5px;
|
||||
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.prompt-input:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.prompt-btns {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding-bottom: 0px;
|
||||
padding-top: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
<template>
|
||||
<div class="category-list">
|
||||
<div class="category" v-for="category in categoryList" :key="category">
|
||||
<el-button plain v-if="category !== active" @click="handleCategoryClick(category)">{{ category }}</el-button>
|
||||
<el-button plain type="primary" v-else @click="handleCategoryClick(category)">{{ category }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {PropType} from "vue";
|
||||
|
||||
// 定义属性
|
||||
defineProps({
|
||||
categoryList: {
|
||||
type: Array as PropType<string[]>,
|
||||
required: true
|
||||
},
|
||||
active: {
|
||||
type: String,
|
||||
required: false
|
||||
}
|
||||
})
|
||||
|
||||
// 定义回调
|
||||
const emits = defineEmits(['onCategoryClick'])
|
||||
|
||||
// 处理分类点击事件
|
||||
const handleCategoryClick = async (category) => {
|
||||
emits('onCategoryClick', category)
|
||||
}
|
||||
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.category-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
|
||||
.category {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-right: 20px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,155 @@
|
|||
<template>
|
||||
<div class="card-list">
|
||||
<el-card class="card" body-class="card-body" v-for="role in roleList" :key="role.id">
|
||||
<!-- 更多 -->
|
||||
<div class="more-container">
|
||||
<el-dropdown @command="handleMoreClick">
|
||||
<span class="el-dropdown-link">
|
||||
<el-button type="text" >
|
||||
<el-icon><More /></el-icon>
|
||||
</el-button>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item :command="['edit', role]" >
|
||||
<el-icon><EditPen /></el-icon>编辑
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item :command="['delete', role]" style="color: red;" >
|
||||
<el-icon><Delete /></el-icon>
|
||||
<span>删除</span>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
<!-- 头像 -->
|
||||
<div>
|
||||
<img class="avatar" :src="role.avatar"/>
|
||||
</div>
|
||||
<div class="right-container">
|
||||
<div class="content-container">
|
||||
<div class="title">{{ role.name }}</div>
|
||||
<div class="description">{{ role.description }}</div>
|
||||
|
||||
</div>
|
||||
<div class="btn-container">
|
||||
<el-button type="primary" size="small" @click="handleUseClick(role)">使用</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ChatRoleVO} from '@/api/ai/model/chatRole'
|
||||
import {PropType} from "vue";
|
||||
import {Delete, EditPen, More} from "@element-plus/icons-vue";
|
||||
|
||||
// 定义属性
|
||||
const props = defineProps({
|
||||
roleList: {
|
||||
type: Array as PropType<ChatRoleVO[]>,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
// 定义钩子
|
||||
const emits = defineEmits(['onDelete', 'onEdit', 'onUse'])
|
||||
|
||||
// more 点击
|
||||
const handleMoreClick = async (data) => {
|
||||
const type = data[0]
|
||||
const role = data[1]
|
||||
if (type === 'delete') {
|
||||
emits('onDelete', role)
|
||||
} else {
|
||||
emits('onEdit', role)
|
||||
}
|
||||
}
|
||||
|
||||
// 使用
|
||||
const handleUseClick = (role) => {
|
||||
emits('onUse', role)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
console.log('props', props.roleList)
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
// 重写 card 组件 body 样式
|
||||
.card-body {
|
||||
max-width: 300px;
|
||||
width: 300px;
|
||||
padding: 15px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: flex-start;
|
||||
position: relative;
|
||||
|
||||
}
|
||||
</style>
|
||||
<style scoped lang="scss">
|
||||
|
||||
// 卡片列表
|
||||
.card-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
position: relative;
|
||||
|
||||
.card {
|
||||
margin-right: 20px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 30px;
|
||||
position: relative;
|
||||
|
||||
.more-container {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.right-container {
|
||||
margin-left: 10px;
|
||||
width: 100%;
|
||||
//height: 100px;
|
||||
|
||||
.content-container {
|
||||
height: 85px;
|
||||
overflow: hidden;
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #3e3e3e;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-top: 10px;
|
||||
font-size: 14px;
|
||||
color: #6a6a6a;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-container {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
margin-top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</style>
|
|
@ -0,0 +1,219 @@
|
|||
<!-- chat 角色仓库 -->
|
||||
<template>
|
||||
<el-container class="role-container">
|
||||
<ChatRoleForm ref="formRef" @success="handlerAddRoleSuccess" />
|
||||
|
||||
<Header title="角色仓库"/>
|
||||
<el-main class="role-main">
|
||||
<div class="search-container">
|
||||
<!-- 搜索按钮 -->
|
||||
<el-input
|
||||
v-model="search"
|
||||
class="search-input"
|
||||
size="default"
|
||||
placeholder="请输入搜索的内容"
|
||||
:suffix-icon="Search"
|
||||
@change="getActiveTabsRole"
|
||||
/>
|
||||
<el-button type="primary" @click="handlerAddRole" style="margin-left: 20px;">
|
||||
<el-icon><User /></el-icon>
|
||||
添加角色
|
||||
</el-button>
|
||||
</div>
|
||||
<!-- tabs -->
|
||||
<el-tabs v-model="activeRole" class="tabs" @tab-click="handleTabsClick">
|
||||
<el-tab-pane class="role-pane" label="我的角色" name="my-role">
|
||||
<RoleList :role-list="myRoleList" @onDelete="handlerCardDelete" @onEdit="handlerCardEdit" @onUse="handlerCardUse" style="margin-top: 20px;" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="公共角色" name="public-role">
|
||||
<RoleCategoryList :category-list="categoryList" :active="activeCategory" @onCategoryClick="handlerCategoryClick" />
|
||||
<RoleList :role-list="publicRoleList" @onDelete="handlerCardDelete" @onEdit="handlerCardEdit" style="margin-top: 20px;" />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-main>
|
||||
</el-container>
|
||||
|
||||
</template>
|
||||
|
||||
<!-- setup -->
|
||||
<script setup lang="ts">
|
||||
import {ref} from "vue";
|
||||
import Header from '@/views/ai/chat/components/Header.vue'
|
||||
import RoleList from './RoleList.vue'
|
||||
import ChatRoleForm from '@/views/ai/model/chatRole/ChatRoleForm.vue'
|
||||
import RoleCategoryList from './RoleCategoryList.vue'
|
||||
import {ChatRoleApi, ChatRolePageReqVO, ChatRoleVO} from '@/api/ai/model/chatRole'
|
||||
import {ChatConversationApi, ChatConversationVO} from '@/api/ai/chat/conversation'
|
||||
import {TabsPaneContext} from "element-plus";
|
||||
import {Search, User} from "@element-plus/icons-vue";
|
||||
|
||||
// 获取路由
|
||||
const router = useRouter()
|
||||
|
||||
// 属性定义
|
||||
const activeRole = ref<string>('my-role') // 选中的角色
|
||||
const loadding = ref<boolean>(true) // 加载中
|
||||
const search = ref<string>('') // 加载中
|
||||
const myPageNo = ref<number>(1) // my 分页下标
|
||||
const myPageSize = ref<number>(50) // my 分页大小
|
||||
const myRoleList = ref<ChatRoleVO[]>([]) // my 分页大小
|
||||
const publicPageNo = ref<number>(1) // public 分页下标
|
||||
const publicPageSize = ref<number>(50) // public 分页大小
|
||||
const publicRoleList = ref<ChatRoleVO[]>([]) // public 分页大小
|
||||
const activeCategory = ref<string>('') // 选择中的分类
|
||||
const categoryList = ref<string[]>([]) // 角色分类类别
|
||||
/** 添加/修改操作 */
|
||||
const formRef = ref()
|
||||
// tabs 点击
|
||||
const handleTabsClick = async (tab: TabsPaneContext) => {
|
||||
// 设置切换状态
|
||||
const activeTabs = tab.paneName + ''
|
||||
activeRole.value = activeTabs;
|
||||
// 切换的时候重新加载数据
|
||||
await getActiveTabsRole()
|
||||
}
|
||||
|
||||
// 获取 my role
|
||||
const getMyRole = async () => {
|
||||
const params:ChatRolePageReqVO = {
|
||||
pageNo: myPageNo.value,
|
||||
pageSize: myPageSize.value,
|
||||
category: activeCategory.value,
|
||||
name: search.value,
|
||||
publicStatus: false
|
||||
}
|
||||
const { total, list } = await ChatRoleApi.getMyPage(params)
|
||||
myRoleList.value = list
|
||||
}
|
||||
|
||||
// 获取 public role
|
||||
const getPublicRole = async () => {
|
||||
const params:ChatRolePageReqVO = {
|
||||
pageNo: publicPageNo.value,
|
||||
pageSize: publicPageSize.value,
|
||||
category: activeCategory.value,
|
||||
name: search.value,
|
||||
publicStatus: true
|
||||
}
|
||||
const { total, list } = await ChatRoleApi.getMyPage(params)
|
||||
publicRoleList.value = list
|
||||
}
|
||||
|
||||
// 获取选中的 tabs 角色
|
||||
const getActiveTabsRole = async () => {
|
||||
if (activeRole.value === 'my-role') {
|
||||
await getMyRole()
|
||||
} else {
|
||||
await getPublicRole()
|
||||
}
|
||||
}
|
||||
|
||||
// 获取角色分类列表
|
||||
const getRoleCategoryList = async () => {
|
||||
categoryList.value = await ChatRoleApi.getCategoryList()
|
||||
}
|
||||
|
||||
// 处理分类点击
|
||||
const handlerCategoryClick = async (category: string) => {
|
||||
if (activeCategory.value === category) {
|
||||
activeCategory.value = ''
|
||||
} else {
|
||||
activeCategory.value = category
|
||||
}
|
||||
await getActiveTabsRole()
|
||||
}
|
||||
|
||||
// 添加角色
|
||||
const handlerAddRole = async () => {
|
||||
formRef.value.open('my-create', null, '添加角色')
|
||||
}
|
||||
|
||||
// card 删除
|
||||
const handlerCardDelete = async (role) => {
|
||||
await ChatRoleApi.deleteMy(role.id)
|
||||
// 刷新数据
|
||||
await getActiveTabsRole()
|
||||
}
|
||||
|
||||
// card 编辑
|
||||
const handlerCardEdit = async (role) => {
|
||||
formRef.value.open('my-update', role.id, '编辑角色')
|
||||
}
|
||||
|
||||
// card 使用
|
||||
const handlerCardUse = async (role) => {
|
||||
const data : ChatConversationVO = {
|
||||
roleId: role.id
|
||||
} as unknown as ChatConversationVO
|
||||
// 创建对话
|
||||
const conversation = await ChatConversationApi.createChatConversationMy(data)
|
||||
// 调整页面
|
||||
router.push({
|
||||
path: `/ai/chat/index`,
|
||||
query: {
|
||||
conversationId: conversation,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 添加角色成功
|
||||
const handlerAddRoleSuccess = async (e) => {
|
||||
console.log(e)
|
||||
// 刷新数据
|
||||
await getActiveTabsRole()
|
||||
}
|
||||
|
||||
//
|
||||
onMounted( async () => {
|
||||
// 获取分类
|
||||
await getRoleCategoryList()
|
||||
// 获取 role 数据
|
||||
await getActiveTabsRole()
|
||||
})
|
||||
</script>
|
||||
<!-- 样式 -->
|
||||
<style scoped lang="scss">
|
||||
|
||||
// 跟容器
|
||||
.role-container {
|
||||
position: absolute;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background-color: #ffffff;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.role-main {
|
||||
position: relative;
|
||||
|
||||
.search-container {
|
||||
//position: absolute;
|
||||
//right: 20px;
|
||||
//top: 10px;
|
||||
//z-index: 100;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.tabs {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.role-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
|
@ -7,13 +7,7 @@
|
|||
label-width="120px"
|
||||
v-loading="formLoading"
|
||||
>
|
||||
<el-form-item label="名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密钥" prop="apiKey">
|
||||
<el-input v-model="formData.apiKey" placeholder="请输入密钥" />
|
||||
</el-form-item>
|
||||
<el-form-item label="平台" prop="platform">
|
||||
<el-form-item label="所属平台" prop="platform">
|
||||
<el-select v-model="formData.platform" placeholder="请输入平台" clearable>
|
||||
<el-option
|
||||
v-for="dict in getStrDictOptions(DICT_TYPE.AI_PLATFORM)"
|
||||
|
@ -23,8 +17,14 @@
|
|||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="自定义 API 地址" prop="url">
|
||||
<el-input v-model="formData.url" placeholder="请输入自定义 API 地址" />
|
||||
<el-form-item label="名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入名称" />
|
||||
</el-form-item>
|
||||
<el-form-item label="密钥" prop="apiKey">
|
||||
<el-input v-model="formData.apiKey" placeholder="请输入密钥" />
|
||||
</el-form-item>
|
||||
<el-form-item label="自定义 API URL" prop="url">
|
||||
<el-input v-model="formData.url" placeholder="请输入自定义 API URL" />
|
||||
</el-form-item>
|
||||
<el-form-item label="状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
|
|
|
@ -60,15 +60,14 @@
|
|||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
|
||||
<el-table-column label="编号" align="center" prop="id" />
|
||||
<el-table-column label="名称" align="center" prop="name" />
|
||||
<el-table-column label="密钥" align="center" prop="apiKey" />
|
||||
<el-table-column label="平台" align="center" prop="platform">
|
||||
<el-table-column label="所属平台" align="center" prop="platform">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.AI_PLATFORM" :value="scope.row.platform" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="自定义 API 地址" align="center" prop="url" />
|
||||
<el-table-column label="名称" align="center" prop="name" />
|
||||
<el-table-column label="密钥" align="center" prop="apiKey" />
|
||||
<el-table-column label="自定义 API URL" align="center" prop="url" />
|
||||
<el-table-column label="状态" align="center" prop="status">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
|
||||
|
|
|
@ -0,0 +1,165 @@
|
|||
<template>
|
||||
<Dialog :title="dialogTitle" v-model="dialogVisible">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="120px"
|
||||
v-loading="formLoading"
|
||||
>
|
||||
<el-form-item label="所属平台" prop="platform">
|
||||
<el-select v-model="formData.platform" placeholder="请输入平台" clearable>
|
||||
<el-option
|
||||
v-for="dict in getStrDictOptions(DICT_TYPE.AI_PLATFORM)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="API 秘钥" prop="keyId">
|
||||
<el-select v-model="formData.keyId" placeholder="请选择 API 秘钥" clearable>
|
||||
<el-option
|
||||
v-for="apiKey in apiKeyList"
|
||||
:key="apiKey.id"
|
||||
:label="apiKey.name"
|
||||
:value="apiKey.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="模型名字" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入模型名字" />
|
||||
</el-form-item>
|
||||
<el-form-item label="模型标识" prop="model">
|
||||
<el-input v-model="formData.model" placeholder="请输入模型标识" />
|
||||
</el-form-item>
|
||||
<el-form-item label="模型排序" prop="sort">
|
||||
<el-input-number v-model="formData.sort" placeholder="请输入模型排序" class="!w-1/1" />
|
||||
</el-form-item>
|
||||
<el-form-item label="开启状态" prop="status">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:label="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="温度参数" prop="temperature">
|
||||
<el-input v-model="formData.temperature" placeholder="请输入温度参数" />
|
||||
</el-form-item>
|
||||
<el-form-item label="回复数 Token 数" prop="maxTokens">
|
||||
<el-input v-model="formData.maxTokens" placeholder="请输入回复数 Token 数" />
|
||||
</el-form-item>
|
||||
<el-form-item label="上下文数量" prop="maxContexts">
|
||||
<el-input v-model="formData.maxContexts" placeholder="请输入上下文数量" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ChatModelApi, ChatModelVO } from '@/api/ai/model/chatModel'
|
||||
import { ApiKeyApi, ApiKeyVO } from '@/api/ai/model/apiKey'
|
||||
import { CommonStatusEnum } from '@/utils/constants'
|
||||
import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict'
|
||||
|
||||
/** API 聊天模型 表单 */
|
||||
defineOptions({ name: 'ChatModelForm' })
|
||||
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const dialogVisible = ref(false) // 弹窗的是否展示
|
||||
const dialogTitle = ref('') // 弹窗的标题
|
||||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
const formType = ref('') // 表单的类型:create - 新增;update - 修改
|
||||
const formData = ref({
|
||||
id: undefined,
|
||||
keyId: undefined,
|
||||
name: undefined,
|
||||
model: undefined,
|
||||
platform: undefined,
|
||||
sort: undefined,
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
temperature: undefined,
|
||||
maxTokens: undefined,
|
||||
maxContexts: undefined
|
||||
})
|
||||
const formRules = reactive({
|
||||
keyId: [{ required: true, message: 'API 秘钥不能为空', trigger: 'blur' }],
|
||||
name: [{ required: true, message: '模型名字不能为空', trigger: 'blur' }],
|
||||
model: [{ required: true, message: '模型标识不能为空', trigger: 'blur' }],
|
||||
platform: [{ required: true, message: '所属平台不能为空', trigger: 'blur' }],
|
||||
sort: [{ required: true, message: '排序不能为空', trigger: 'blur' }],
|
||||
status: [{ required: true, message: '状态不能为空', trigger: 'blur' }]
|
||||
})
|
||||
const formRef = ref() // 表单 Ref
|
||||
const apiKeyList = ref([] as ApiKeyVO[]) // API 密钥列表
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = async (type: string, id?: number) => {
|
||||
dialogVisible.value = true
|
||||
dialogTitle.value = t('action.' + type)
|
||||
formType.value = type
|
||||
resetForm()
|
||||
// 修改时,设置数据
|
||||
if (id) {
|
||||
formLoading.value = true
|
||||
try {
|
||||
formData.value = await ChatModelApi.getChatModel(id)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
// 获得下拉数据
|
||||
apiKeyList.value = await ApiKeyApi.getApiKeySimpleList(CommonStatusEnum.ENABLE)
|
||||
}
|
||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||
|
||||
/** 提交表单 */
|
||||
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
|
||||
const submitForm = async () => {
|
||||
// 校验表单
|
||||
await formRef.value.validate()
|
||||
// 提交请求
|
||||
formLoading.value = true
|
||||
try {
|
||||
const data = formData.value as unknown as ChatModelVO
|
||||
if (formType.value === 'create') {
|
||||
await ChatModelApi.createChatModel(data)
|
||||
message.success(t('common.createSuccess'))
|
||||
} else {
|
||||
await ChatModelApi.updateChatModel(data)
|
||||
message.success(t('common.updateSuccess'))
|
||||
}
|
||||
dialogVisible.value = false
|
||||
// 发送操作成功的事件
|
||||
emit('success')
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 重置表单 */
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
keyId: undefined,
|
||||
name: undefined,
|
||||
model: undefined,
|
||||
platform: undefined,
|
||||
sort: undefined,
|
||||
status: CommonStatusEnum.ENABLE,
|
||||
temperature: undefined,
|
||||
maxTokens: undefined,
|
||||
maxContexts: undefined
|
||||
}
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,185 @@
|
|||
<template>
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form-item label="模型名字" prop="name">
|
||||
<el-input
|
||||
v-model="queryParams.name"
|
||||
placeholder="请输入模型名字"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="模型标识" prop="model">
|
||||
<el-input
|
||||
v-model="queryParams.model"
|
||||
placeholder="请输入模型标识"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="模型平台" prop="platform">
|
||||
<el-input
|
||||
v-model="queryParams.platform"
|
||||
placeholder="请输入模型平台"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
@click="openForm('create')"
|
||||
v-hasPermi="['ai:chat-model:create']"
|
||||
>
|
||||
<Icon icon="ep:plus" class="mr-5px" /> 新增
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
|
||||
<el-table-column label="所属平台" align="center" prop="platform">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.AI_PLATFORM" :value="scope.row.platform" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="模型名字" align="center" prop="name" />
|
||||
<el-table-column label="模型标识" align="center" prop="model" />
|
||||
<el-table-column label="API 秘钥" align="center" prop="keyId" min-width="140">
|
||||
<template #default="scope">
|
||||
<span>{{ apiKeyList.find((item) => item.id === scope.row.keyId)?.name }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="排序" align="center" prop="sort" />
|
||||
<el-table-column label="状态" align="center" prop="status">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="温度参数" align="center" prop="temperature" />
|
||||
<el-table-column label="回复数 Token 数" align="center" prop="maxTokens" min-width="140" />
|
||||
<el-table-column label="上下文数量" align="center" prop="maxContexts" />
|
||||
<el-table-column label="操作" align="center">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="openForm('update', scope.row.id)"
|
||||
v-hasPermi="['ai:chat-model:update']"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row.id)"
|
||||
v-hasPermi="['ai:chat-model:delete']"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<ChatModelForm ref="formRef" @success="getList" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ChatModelApi, ChatModelVO } from '@/api/ai/model/chatModel'
|
||||
import ChatModelForm from './ChatModelForm.vue'
|
||||
import { DICT_TYPE } from '@/utils/dict'
|
||||
import { ApiKeyApi, ApiKeyVO } from '@/api/ai/model/apiKey'
|
||||
|
||||
/** API 聊天模型 列表 */
|
||||
defineOptions({ name: 'AiChatModel' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<ChatModelVO[]>([]) // 列表的数据
|
||||
const total = ref(0) // 列表的总页数
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
name: undefined,
|
||||
model: undefined,
|
||||
platform: undefined
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
const apiKeyList = ref([] as ApiKeyVO[]) // API 密钥列表
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await ChatModelApi.getChatModelPage(queryParams)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 添加/修改操作 */
|
||||
const formRef = ref()
|
||||
const openForm = (type: string, id?: number) => {
|
||||
formRef.value.open(type, id)
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
// 删除的二次确认
|
||||
await message.delConfirm()
|
||||
// 发起删除
|
||||
await ChatModelApi.deleteChatModel(id)
|
||||
message.success(t('common.delSuccess'))
|
||||
// 刷新列表
|
||||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(async () => {
|
||||
getList()
|
||||
// 获得下拉数据
|
||||
apiKeyList.value = await ApiKeyApi.getApiKeySimpleList()
|
||||
})
|
||||
</script>
|
|
@ -0,0 +1,205 @@
|
|||
<template>
|
||||
<Dialog :title="dialogTitle" v-model="dialogVisible">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="formData"
|
||||
:rules="formRules"
|
||||
label-width="100px"
|
||||
v-loading="formLoading"
|
||||
>
|
||||
<el-form-item label="角色名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入角色名称"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色头像" prop="avatar">
|
||||
<UploadImg v-model="formData.avatar" height="60px" width="60px"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="绑定模型" prop="modelId" v-if="!isUser(formType)">
|
||||
<el-select v-model="formData.modelId" placeholder="请选择模型" clearable>
|
||||
<el-option
|
||||
v-for="chatModel in chatModelList"
|
||||
:key="chatModel.id"
|
||||
:label="chatModel.name"
|
||||
:value="chatModel.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色类别" prop="category" v-if="!isUser(formType)">
|
||||
<el-input v-model="formData.category" placeholder="请输入角色类别"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色描述" prop="description">
|
||||
<el-input type="textarea" v-model="formData.description" placeholder="请输入角色描述"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色设定" prop="systemMessage">
|
||||
<el-input type="textarea" v-model="formData.systemMessage" placeholder="请输入角色设定" />
|
||||
</el-form-item>
|
||||
<el-form-item label="欢迎语👏🏻" prop="welcomeMessage" v-if="isUser(formType)">
|
||||
<el-input type="textarea" v-model="formData.welcomeMessage" placeholder="请输入欢迎语"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否公开" prop="publicStatus" v-if="!isUser(formType)">
|
||||
<el-radio-group v-model="formData.publicStatus">
|
||||
<el-radio
|
||||
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
|
||||
:key="dict.value"
|
||||
:label="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色排序" prop="sort" v-if="!isUser(formType)">
|
||||
<el-input-number v-model="formData.sort" placeholder="请输入角色排序" class="!w-1/1"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="开启状态" prop="status" v-if="!isUser(formType)">
|
||||
<el-radio-group v-model="formData.status">
|
||||
<el-radio
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)"
|
||||
:key="dict.value"
|
||||
:label="dict.value"
|
||||
>
|
||||
{{ dict.label }}
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button>
|
||||
<el-button @click="dialogVisible = false">取 消</el-button>
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {getIntDictOptions, getBoolDictOptions, DICT_TYPE} from '@/utils/dict'
|
||||
import {ChatRoleApi, ChatRoleVO} from '@/api/ai/model/chatRole'
|
||||
import {CommonStatusEnum} from '@/utils/constants'
|
||||
import {ChatModelApi, ChatModelVO} from '@/api/ai/model/chatModel'
|
||||
|
||||
/** AI 聊天角色 表单 */
|
||||
defineOptions({name: 'ChatRoleForm'})
|
||||
|
||||
const {t} = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const dialogVisible = ref(false) // 弹窗的是否展示
|
||||
const dialogTitle = ref('') // 弹窗的标题
|
||||
const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用
|
||||
const formType = ref('') // 表单的类型:create - 新增;update - 修改
|
||||
const formData = ref({
|
||||
id: undefined,
|
||||
modelId: undefined,
|
||||
name: undefined,
|
||||
avatar: undefined,
|
||||
category: undefined,
|
||||
sort: undefined,
|
||||
description: undefined,
|
||||
systemMessage: undefined,
|
||||
welcomeMessage: undefined,
|
||||
publicStatus: true,
|
||||
status: CommonStatusEnum.ENABLE
|
||||
})
|
||||
|
||||
// 是否
|
||||
const isUser = (type: string) => {
|
||||
return (type === 'my-create' || type === 'my-update')
|
||||
}
|
||||
|
||||
const formRules = ref() // reactive(formRulesObj)
|
||||
const formRef = ref() // 表单 Ref
|
||||
const chatModelList = ref([] as ChatModelVO[]) // 聊天模型列表
|
||||
|
||||
const getFormRules = async (type: string) => {
|
||||
let formRulesObj = {
|
||||
name: [{required: true, message: '角色名称不能为空', trigger: 'blur'}],
|
||||
avatar: [{required: true, message: '角色头像不能为空', trigger: 'blur'}],
|
||||
category: [{required: true, message: '角色类别不能为空', trigger: 'blur'}],
|
||||
sort: [{required: true, message: '角色排序不能为空', trigger: 'blur'}],
|
||||
description: [{required: true, message: '角色描述不能为空', trigger: 'blur'}],
|
||||
systemMessage: [{required: true, message: '角色设定不能为空', trigger: 'blur'}],
|
||||
// welcomeMessage: [{ required: true, message: '欢迎语不能为空', trigger: 'blur' }],
|
||||
publicStatus: [{required: true, message: '是否公开不能为空', trigger: 'blur'}]
|
||||
}
|
||||
|
||||
if (isUser(type)) {
|
||||
formRulesObj['welcomeMessage'] = [{
|
||||
required: true,
|
||||
message: '欢迎语不能为空',
|
||||
trigger: 'blur'
|
||||
}]
|
||||
}
|
||||
|
||||
formRules.value = reactive(formRulesObj)
|
||||
}
|
||||
|
||||
/** 打开弹窗 */
|
||||
const open = async (type: string, id?: number, title?: string) => {
|
||||
dialogVisible.value = true
|
||||
dialogTitle.value = title || t('action.' + type)
|
||||
formType.value = type
|
||||
getFormRules(type)
|
||||
resetForm()
|
||||
// 修改时,设置数据
|
||||
if (id) {
|
||||
formLoading.value = true
|
||||
try {
|
||||
formData.value = await ChatRoleApi.getChatRole(id)
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
// 获得下拉数据
|
||||
chatModelList.value = await ChatModelApi.getChatModelSimpleList(CommonStatusEnum.ENABLE)
|
||||
}
|
||||
defineExpose({open}) // 提供 open 方法,用于打开弹窗
|
||||
|
||||
/** 提交表单 */
|
||||
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
|
||||
const submitForm = async () => {
|
||||
// 校验表单
|
||||
await formRef.value.validate()
|
||||
// 提交请求
|
||||
formLoading.value = true
|
||||
try {
|
||||
const data = formData.value as unknown as ChatRoleVO
|
||||
|
||||
// tip: my-create、my-update 是 chat 角色仓库调用
|
||||
// tip: create、else 是后台管理调用
|
||||
|
||||
if (formType.value === 'my-create') {
|
||||
await ChatRoleApi.createMy(data)
|
||||
message.success(t('common.createSuccess'))
|
||||
} else if (formType.value === 'my-update') {
|
||||
await ChatRoleApi.updateMy(data)
|
||||
message.success(t('common.updateSuccess'))
|
||||
} else if (formType.value === 'create') {
|
||||
await ChatRoleApi.createChatRole(data)
|
||||
message.success(t('common.createSuccess'))
|
||||
} else {
|
||||
await ChatRoleApi.updateChatRole(data)
|
||||
message.success(t('common.updateSuccess'))
|
||||
}
|
||||
dialogVisible.value = false
|
||||
// 发送操作成功的事件
|
||||
emit('success')
|
||||
} finally {
|
||||
formLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** 重置表单 */
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
id: undefined,
|
||||
modelId: undefined,
|
||||
name: undefined,
|
||||
avatar: undefined,
|
||||
category: undefined,
|
||||
sort: undefined,
|
||||
description: undefined,
|
||||
systemMessage: undefined,
|
||||
welcomeMessage: undefined,
|
||||
publicStatus: true,
|
||||
status: CommonStatusEnum.ENABLE
|
||||
}
|
||||
formRef.value?.resetFields()
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,187 @@
|
|||
<template>
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form-item label="角色名称" prop="name">
|
||||
<el-input
|
||||
v-model="queryParams.name"
|
||||
placeholder="请输入角色名称"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色类别" prop="category">
|
||||
<el-input
|
||||
v-model="queryParams.category"
|
||||
placeholder="请输入角色类别"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="是否公开" prop="publicStatus">
|
||||
<el-select
|
||||
v-model="queryParams.publicStatus"
|
||||
placeholder="请选择是否公开"
|
||||
clearable
|
||||
class="!w-240px"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button>
|
||||
<el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
plain
|
||||
@click="openForm('create')"
|
||||
v-hasPermi="['ai:chat-role:create']"
|
||||
>
|
||||
<Icon icon="ep:plus" class="mr-5px" /> 新增
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 列表 -->
|
||||
<ContentWrap>
|
||||
<el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true">
|
||||
<el-table-column label="角色名称" align="center" prop="name" />
|
||||
<el-table-column label="绑定模型" align="center" prop="modelName" />
|
||||
<el-table-column label="角色头像" align="center" prop="avatar">
|
||||
<template #default="scope">
|
||||
<el-image :src="scope?.row.avatar" class="w-32px h-32px" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="角色类别" align="center" prop="category" />
|
||||
<el-table-column label="角色描述" align="center" prop="description" />
|
||||
<el-table-column label="角色设定" align="center" prop="systemMessage" />
|
||||
<el-table-column label="是否公开" align="center" prop="publicStatus">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.publicStatus" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="状态" align="center" prop="status">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="角色排序" align="center" prop="sort" />
|
||||
<el-table-column label="操作" align="center">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="openForm('update', scope.row.id)"
|
||||
v-hasPermi="['ai:chat-role:update']"
|
||||
>
|
||||
编辑
|
||||
</el-button>
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row.id)"
|
||||
v-hasPermi="['ai:chat-role:delete']"
|
||||
>
|
||||
删除
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<!-- 分页 -->
|
||||
<Pagination
|
||||
:total="total"
|
||||
v-model:page="queryParams.pageNo"
|
||||
v-model:limit="queryParams.pageSize"
|
||||
@pagination="getList"
|
||||
/>
|
||||
</ContentWrap>
|
||||
|
||||
<!-- 表单弹窗:添加/修改 -->
|
||||
<ChatRoleForm ref="formRef" @success="getList" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
|
||||
import { ChatRoleApi, ChatRoleVO } from '@/api/ai/model/chatRole'
|
||||
import ChatRoleForm from './ChatRoleForm.vue'
|
||||
|
||||
/** AI 聊天角色 列表 */
|
||||
defineOptions({ name: 'AiChatRole' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<ChatRoleVO[]>([]) // 列表的数据
|
||||
const total = ref(0) // 列表的总页数
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
name: undefined,
|
||||
category: undefined,
|
||||
publicStatus: true
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await ChatRoleApi.getChatRolePage(queryParams)
|
||||
list.value = data.list
|
||||
total.value = data.total
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
queryParams.pageNo = 1
|
||||
getList()
|
||||
}
|
||||
|
||||
/** 重置按钮操作 */
|
||||
const resetQuery = () => {
|
||||
queryFormRef.value.resetFields()
|
||||
handleQuery()
|
||||
}
|
||||
|
||||
/** 添加/修改操作 */
|
||||
const formRef = ref()
|
||||
const openForm = (type: string, id?: number) => {
|
||||
formRef.value.open(type, id)
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
// 删除的二次确认
|
||||
await message.delConfirm()
|
||||
// 发起删除
|
||||
await ChatRoleApi.deleteChatRole(id)
|
||||
message.success(t('common.delSuccess'))
|
||||
// 刷新列表
|
||||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
|
@ -83,7 +83,7 @@
|
|||
<el-table-column align="center" label="申请编号" prop="id" />
|
||||
<el-table-column align="center" label="状态" prop="result">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.result" />
|
||||
<dict-tag :type="DICT_TYPE.BPM_PROCESS_INSTANCE_STATUS" :value="scope.row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<!-- 员工业绩统计 -->
|
||||
<!-- TODO @scholar:参考 ReceivablePricePerformance 建议改 -->
|
||||
<template>
|
||||
<!-- Echarts图 -->
|
||||
<el-card shadow="never">
|
||||
|
@ -64,13 +63,13 @@ const echartsOption = reactive<EChartsOption>({
|
|||
data: []
|
||||
},
|
||||
{
|
||||
name: '同比增长率(%)',
|
||||
name: '环比增长率(%)',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: []
|
||||
},
|
||||
{
|
||||
name: '环比增长率(%)',
|
||||
name: '同比增长率(%)',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: []
|
||||
|
@ -173,7 +172,9 @@ const loadData = async () => {
|
|||
(s: StatisticsPerformanceRespVO) => s.lastMonthCount
|
||||
)
|
||||
echartsOption.series[3]['data'] = performanceList.map((s: StatisticsPerformanceRespVO) =>
|
||||
s.lastMonthCount !== 0 ? ((s.currentMonthCount / s.lastMonthCount) * 100).toFixed(2) : 'NULL'
|
||||
s.lastMonthCount !== 0
|
||||
? (((s.currentMonthCount - s.lastMonthCount) / s.lastMonthCount) * 100).toFixed(2)
|
||||
: 'NULL'
|
||||
)
|
||||
}
|
||||
if (echartsOption.series && echartsOption.series[2] && echartsOption.series[2]['data']) {
|
||||
|
@ -181,7 +182,9 @@ const loadData = async () => {
|
|||
(s: StatisticsPerformanceRespVO) => s.lastYearCount
|
||||
)
|
||||
echartsOption.series[4]['data'] = performanceList.map((s: StatisticsPerformanceRespVO) =>
|
||||
s.lastYearCount !== 0 ? ((s.currentMonthCount / s.lastYearCount) * 100).toFixed(2) : 'NULL'
|
||||
s.lastYearCount !== 0
|
||||
? (((s.currentMonthCount - s.lastYearCount) / s.lastYearCount) * 100).toFixed(2)
|
||||
: 'NULL'
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -197,8 +200,8 @@ const tableData = reactive([
|
|||
{ title: '当月合同数量统计(个)' },
|
||||
{ title: '上月合同数量统计(个)' },
|
||||
{ title: '去年当月合同数量统计(个)' },
|
||||
{ title: '同比增长率(%)' },
|
||||
{ title: '环比增长率(%)' }
|
||||
{ title: '环比增长率(%)' },
|
||||
{ title: '同比增长率(%)' }
|
||||
])
|
||||
|
||||
// 定义 convertListData 方法,数据行列转置,展示每月数据
|
||||
|
@ -214,9 +217,13 @@ const convertListData = () => {
|
|||
tableData[1]['prop' + index] = item.lastMonthCount
|
||||
tableData[2]['prop' + index] = item.lastYearCount
|
||||
tableData[3]['prop' + index] =
|
||||
item.lastMonthCount !== 0 ? (item.currentMonthCount / item.lastMonthCount).toFixed(2) : 'NULL'
|
||||
item.lastMonthCount !== 0
|
||||
? (((item.currentMonthCount - item.lastMonthCount) / item.lastMonthCount) * 100).toFixed(2)
|
||||
: 'NULL'
|
||||
tableData[4]['prop' + index] =
|
||||
item.lastYearCount !== 0 ? (item.currentMonthCount / item.lastYearCount).toFixed(2) : 'NULL'
|
||||
item.lastYearCount !== 0
|
||||
? (((item.currentMonthCount - item.lastYearCount) / item.lastYearCount) * 100).toFixed(2)
|
||||
: 'NULL'
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<!-- 员工业绩统计 -->
|
||||
<!-- TODO @scholar:参考 ReceivablePricePerformance 建议改 -->
|
||||
<template>
|
||||
<!-- Echarts图 -->
|
||||
<el-card shadow="never">
|
||||
|
@ -64,13 +63,13 @@ const echartsOption = reactive<EChartsOption>({
|
|||
data: []
|
||||
},
|
||||
{
|
||||
name: '同比增长率(%)',
|
||||
name: '环比增长率(%)',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: []
|
||||
},
|
||||
{
|
||||
name: '环比增长率(%)',
|
||||
name: '同比增长率(%)',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: []
|
||||
|
@ -173,7 +172,9 @@ const loadData = async () => {
|
|||
(s: StatisticsPerformanceRespVO) => s.lastMonthCount
|
||||
)
|
||||
echartsOption.series[3]['data'] = performanceList.map((s: StatisticsPerformanceRespVO) =>
|
||||
s.lastMonthCount !== 0 ? ((s.currentMonthCount / s.lastMonthCount) * 100).toFixed(2) : 'NULL'
|
||||
s.lastMonthCount !== 0
|
||||
? (((s.currentMonthCount - s.lastMonthCount) / s.lastMonthCount) * 100).toFixed(2)
|
||||
: 'NULL'
|
||||
)
|
||||
}
|
||||
if (echartsOption.series && echartsOption.series[2] && echartsOption.series[2]['data']) {
|
||||
|
@ -181,7 +182,9 @@ const loadData = async () => {
|
|||
(s: StatisticsPerformanceRespVO) => s.lastYearCount
|
||||
)
|
||||
echartsOption.series[4]['data'] = performanceList.map((s: StatisticsPerformanceRespVO) =>
|
||||
s.lastYearCount !== 0 ? ((s.currentMonthCount / s.lastYearCount) * 100).toFixed(2) : 'NULL'
|
||||
s.lastYearCount !== 0
|
||||
? (((s.currentMonthCount - s.lastYearCount) / s.lastYearCount) * 100).toFixed(2)
|
||||
: 'NULL'
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -197,8 +200,8 @@ const tableData = reactive([
|
|||
{ title: '当月合同金额统计(元)' },
|
||||
{ title: '上月合同金额统计(元)' },
|
||||
{ title: '去年当月合同金额统计(元)' },
|
||||
{ title: '同比增长率(%)' },
|
||||
{ title: '环比增长率(%)' }
|
||||
{ title: '环比增长率(%)' },
|
||||
{ title: '同比增长率(%)' }
|
||||
])
|
||||
|
||||
// 定义 init 方法
|
||||
|
@ -214,9 +217,13 @@ const convertListData = () => {
|
|||
tableData[1]['prop' + index] = item.lastMonthCount
|
||||
tableData[2]['prop' + index] = item.lastYearCount
|
||||
tableData[3]['prop' + index] =
|
||||
item.lastMonthCount !== 0 ? (item.currentMonthCount / item.lastMonthCount).toFixed(2) : 'NULL'
|
||||
item.lastMonthCount !== 0
|
||||
? (((item.currentMonthCount - item.lastMonthCount) / item.lastMonthCount) * 100).toFixed(2)
|
||||
: 'NULL'
|
||||
tableData[4]['prop' + index] =
|
||||
item.lastYearCount !== 0 ? (item.currentMonthCount / item.lastYearCount).toFixed(2) : 'NULL'
|
||||
item.lastYearCount !== 0
|
||||
? (((item.currentMonthCount - item.lastYearCount) / item.lastYearCount) * 100).toFixed(2)
|
||||
: 'NULL'
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
:prop="item.prop"
|
||||
align="center"
|
||||
>
|
||||
<!-- TODO @scholar:IDEA 爆红的处理 -->
|
||||
<template #default="scope">
|
||||
{{ scope.row[item.prop] }}
|
||||
</template>
|
||||
|
@ -64,13 +63,13 @@ const echartsOption = reactive<EChartsOption>({
|
|||
data: []
|
||||
},
|
||||
{
|
||||
name: '同比增长率(%)',
|
||||
name: '环比增长率(%)',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: []
|
||||
},
|
||||
{
|
||||
name: '环比增长率(%)',
|
||||
name: '同比增长率(%)',
|
||||
type: 'line',
|
||||
yAxisIndex: 1,
|
||||
data: []
|
||||
|
@ -121,7 +120,6 @@ const echartsOption = reactive<EChartsOption>({
|
|||
type: 'value',
|
||||
name: '',
|
||||
axisTick: {
|
||||
// TODO @scholar:IDEA 爆红的处理
|
||||
alignWithLabel: true,
|
||||
lineStyle: {
|
||||
width: 0
|
||||
|
@ -174,7 +172,9 @@ const loadData = async () => {
|
|||
(s: StatisticsPerformanceRespVO) => s.lastMonthCount
|
||||
)
|
||||
echartsOption.series[3]['data'] = performanceList.map((s: StatisticsPerformanceRespVO) =>
|
||||
s.lastMonthCount !== 0 ? ((s.currentMonthCount / s.lastMonthCount) * 100).toFixed(2) : 'NULL'
|
||||
s.lastMonthCount !== 0
|
||||
? (((s.currentMonthCount - s.lastMonthCount) / s.lastMonthCount) * 100).toFixed(2)
|
||||
: 'NULL'
|
||||
)
|
||||
}
|
||||
if (echartsOption.series && echartsOption.series[2] && echartsOption.series[1]['data']) {
|
||||
|
@ -182,7 +182,9 @@ const loadData = async () => {
|
|||
(s: StatisticsPerformanceRespVO) => s.lastYearCount
|
||||
)
|
||||
echartsOption.series[4]['data'] = performanceList.map((s: StatisticsPerformanceRespVO) =>
|
||||
s.lastYearCount !== 0 ? ((s.currentMonthCount / s.lastYearCount) * 100).toFixed(2) : 'NULL'
|
||||
s.lastYearCount !== 0
|
||||
? (((s.currentMonthCount - s.lastYearCount) / s.lastYearCount) * 100).toFixed(2)
|
||||
: 'NULL'
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -193,14 +195,13 @@ const loadData = async () => {
|
|||
}
|
||||
|
||||
// 初始化数据
|
||||
// TODO @scholar:加个 as any[],避免 idea 爆红
|
||||
const columnsData = reactive([] as any[])
|
||||
const columnsData = reactive([])
|
||||
const tableData = reactive([
|
||||
{ title: '当月回款金额统计(元)' },
|
||||
{ title: '上月回款金额统计(元)' },
|
||||
{ title: '去年当月回款金额统计(元)' },
|
||||
{ title: '同比增长率(%)' },
|
||||
{ title: '环比增长率(%)' }
|
||||
{ title: '环比增长率(%)' },
|
||||
{ title: '同比增长率(%)' }
|
||||
])
|
||||
|
||||
// 定义 init 方法
|
||||
|
@ -215,11 +216,14 @@ const convertListData = () => {
|
|||
tableData[0]['prop' + index] = item.currentMonthCount
|
||||
tableData[1]['prop' + index] = item.lastMonthCount
|
||||
tableData[2]['prop' + index] = item.lastYearCount
|
||||
// TODO @scholar:百分比,使用 erpCalculatePercentage 直接计算;如果是 0,则返回 0,统一就好哈;
|
||||
tableData[3]['prop' + index] =
|
||||
item.lastMonthCount !== 0 ? (item.currentMonthCount / item.lastMonthCount).toFixed(2) : 'NULL'
|
||||
item.lastMonthCount !== 0
|
||||
? (((item.currentMonthCount - item.lastMonthCount) / item.lastMonthCount) * 100).toFixed(2)
|
||||
: 'NULL'
|
||||
tableData[4]['prop' + index] =
|
||||
item.lastYearCount !== 0 ? (item.currentMonthCount / item.lastYearCount).toFixed(2) : 'NULL'
|
||||
item.lastYearCount !== 0
|
||||
? (((item.currentMonthCount - item.lastYearCount) / item.lastYearCount) * 100).toFixed(2)
|
||||
: 'NULL'
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -73,7 +73,7 @@
|
|||
import * as DeptApi from '@/api/system/dept'
|
||||
import * as UserApi from '@/api/system/user'
|
||||
import { useUserStore } from '@/store/modules/user'
|
||||
import { beginOfDay, formatDate } from '@/utils/formatTime'
|
||||
import { beginOfDay, endOfDay, formatDate } from '@/utils/formatTime'
|
||||
import { defaultProps, handleTree } from '@/utils/tree'
|
||||
import ContractCountPerformance from './components/ContractCountPerformance.vue'
|
||||
import ContractPricePerformance from './components/ContractPricePerformance.vue'
|
||||
|
@ -85,8 +85,8 @@ const queryParams = reactive({
|
|||
deptId: useUserStore().getUser.deptId,
|
||||
userId: undefined,
|
||||
times: [
|
||||
// 默认显示当年的数据
|
||||
formatDate(beginOfDay(new Date(new Date().getTime() - 3600 * 1000 * 24 * 7)))
|
||||
formatDate(beginOfDay(new Date(new Date().getFullYear(), 0, 1))),
|
||||
formatDate(endOfDay(new Date(new Date().getFullYear(), 11, 31)))
|
||||
]
|
||||
})
|
||||
|
||||
|
@ -100,31 +100,20 @@ const userListByDeptId = computed(() =>
|
|||
: []
|
||||
)
|
||||
|
||||
// TODO @scholar:改成尾注释,保证 vue 内容短一点;变量名小写
|
||||
// 活跃标签
|
||||
const activeTab = ref('ContractCountPerformance')
|
||||
// 1.员工合同数量统计
|
||||
const ContractCountPerformanceRef = ref()
|
||||
// 2.员工合同金额统计
|
||||
const ContractPricePerformanceRef = ref()
|
||||
// 3.员工回款金额统计
|
||||
const ReceivablePricePerformanceRef = ref()
|
||||
const ContractCountPerformanceRef = ref() // 员工合同数量统计
|
||||
const ContractPricePerformanceRef = ref() // 员工合同金额统计
|
||||
const ReceivablePricePerformanceRef = ref() // 员工回款金额统计
|
||||
|
||||
/** 搜索按钮操作 */
|
||||
const handleQuery = () => {
|
||||
// 从 queryParams.times[0] 中获取到了年份
|
||||
const selectYear = parseInt(queryParams.times[0])
|
||||
queryParams.times[0] = formatDate(beginOfDay(new Date(selectYear, 0, 1)))
|
||||
queryParams.times[1] = formatDate(endOfDay(new Date(selectYear, 11, 31)))
|
||||
|
||||
// 创建一个新的 Date 对象,设置为指定的年份的第一天
|
||||
const fullDate = new Date(selectYear, 0, 1, 0, 0, 0)
|
||||
|
||||
// 将完整的日期时间格式化为需要的字符串形式,比如 2004-01-01 00:00:00
|
||||
// TODO @scholar:看看,是不是可以使用 year 哈
|
||||
queryParams.times[0] = `${fullDate.getFullYear()}-${String(fullDate.getMonth() + 1).padStart(
|
||||
2,
|
||||
'0'
|
||||
)}-${String(fullDate.getDate()).padStart(2, '0')} ${String(fullDate.getHours()).padStart(2, '0')}:${String(fullDate.getMinutes()).padStart(2, '0')}:${String(fullDate.getSeconds()).padStart(2, '0')}`
|
||||
|
||||
// 执行查询
|
||||
switch (activeTab.value) {
|
||||
case 'ContractCountPerformance':
|
||||
ContractCountPerformanceRef.value?.loadData?.()
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
:inline="true"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form-item label="部门名称" prop="title">
|
||||
<el-form-item label="部门名称" prop="name">
|
||||
<el-input
|
||||
v-model="queryParams.name"
|
||||
placeholder="请输入部门名称"
|
||||
|
|
Loading…
Reference in New Issue