Merge remote-tracking branch 'refs/remotes/yudao/dev' into dev-crm
commit
1abfec766e
|
@ -47,7 +47,7 @@
|
|||
"driver.js": "^1.3.1",
|
||||
"echarts": "^5.5.0",
|
||||
"echarts-wordcloud": "^2.1.0",
|
||||
"element-plus": "2.6.1",
|
||||
"element-plus": "2.7.0",
|
||||
"fast-xml-parser": "^4.3.2",
|
||||
"highlight.js": "^11.9.0",
|
||||
"jsencrypt": "^3.3.2",
|
||||
|
|
|
@ -17,6 +17,9 @@ dependencies:
|
|||
'@iconify/iconify':
|
||||
specifier: ^3.1.1
|
||||
version: 3.1.1
|
||||
'@microsoft/fetch-event-source':
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.1
|
||||
'@videojs-player/vue':
|
||||
specifier: ^1.0.0
|
||||
version: 1.0.0(@types/video.js@7.3.58)(video.js@7.21.5)(vue@3.4.21)
|
||||
|
@ -69,8 +72,8 @@ dependencies:
|
|||
specifier: ^2.1.0
|
||||
version: 2.1.0(echarts@5.5.0)
|
||||
element-plus:
|
||||
specifier: 2.6.1
|
||||
version: 2.6.1(vue@3.4.21)
|
||||
specifier: 2.7.0
|
||||
version: 2.7.0(vue@3.4.21)
|
||||
fast-xml-parser:
|
||||
specifier: ^4.3.2
|
||||
version: 4.3.6
|
||||
|
@ -83,6 +86,9 @@ dependencies:
|
|||
lodash-es:
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
marked:
|
||||
specifier: ^12.0.2
|
||||
version: 12.0.2
|
||||
min-dash:
|
||||
specifier: ^4.1.1
|
||||
version: 4.2.1
|
||||
|
@ -2360,6 +2366,10 @@ packages:
|
|||
'@jridgewell/sourcemap-codec': 1.4.15
|
||||
dev: true
|
||||
|
||||
/@microsoft/fetch-event-source@2.0.1:
|
||||
resolution: {integrity: sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==, tarball: https://registry.npmmirror.com/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz}
|
||||
dev: false
|
||||
|
||||
/@nodelib/fs.scandir@2.1.5:
|
||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==, tarball: https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz}
|
||||
engines: {node: '>= 8'}
|
||||
|
@ -5082,8 +5092,8 @@ packages:
|
|||
resolution: {integrity: sha512-9ItEpeu15hW5m8jKdriL+BQrgwDTXEL9pn4SkillWFu73ZNNNQ2BKKLS+ZHv2vC9UkNhosAeyfxOf/5OSeTCPA==, tarball: https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.4.750.tgz}
|
||||
dev: true
|
||||
|
||||
/element-plus@2.6.1(vue@3.4.21):
|
||||
resolution: {integrity: sha512-6VRpLjwtIVdtUuITJPPKtpOH1NM6nuAkRE3q5O4Lrx0N1bYMhTkiqb2Jy7zfQuDPbOIkkF2OABTzegpNnzgsnQ==, tarball: https://registry.npmmirror.com/element-plus/-/element-plus-2.6.1.tgz}
|
||||
/element-plus@2.7.0(vue@3.4.21):
|
||||
resolution: {integrity: sha512-WAiaFLavuWFxof9qwkC27jvkh9nRcNnB506g1vvJSiVaVqjCBWUFCIyJKeN11M1qcv2cS5VV5PfSLjTIkrw87A==, tarball: https://registry.npmmirror.com/element-plus/-/element-plus-2.7.0.tgz}
|
||||
peerDependencies:
|
||||
vue: ^3.2.0
|
||||
dependencies:
|
||||
|
@ -6917,6 +6927,12 @@ packages:
|
|||
object-visit: 1.0.1
|
||||
dev: true
|
||||
|
||||
/marked@12.0.2:
|
||||
resolution: {integrity: sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==, tarball: https://registry.npmmirror.com/marked/-/marked-12.0.2.tgz}
|
||||
engines: {node: '>= 18'}
|
||||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/matches-selector@1.2.0:
|
||||
resolution: {integrity: sha512-c4vLwYWyl+Ji+U43eU/G5FwxWd4ZH0ePUsFs5y0uwD9HUEFBXUQ1zUUan+78IpRD+y4pUfG0nAzNM292K7ItvA==, tarball: https://registry.npmmirror.com/matches-selector/-/matches-selector-1.2.0.tgz}
|
||||
dev: true
|
||||
|
|
|
@ -54,4 +54,8 @@ body {
|
|||
.#{$prefix-cls}-grey-mode {
|
||||
filter: grayscale(100%);
|
||||
}
|
||||
|
||||
.scrollbar__view {
|
||||
height: 99%!important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import request from '@/config/axios'
|
||||
|
||||
// AI 聊天会话 VO
|
||||
// AI 聊天对话 VO
|
||||
export interface ChatConversationVO {
|
||||
id: number // ID 编号
|
||||
id: string // ID 编号
|
||||
userId: number // 用户编号
|
||||
title: string // 会话标题
|
||||
title: string // 对话标题
|
||||
pinned: boolean // 是否置顶
|
||||
roleId: number // 角色编号
|
||||
modelId: number // 模型编号
|
||||
|
@ -12,33 +12,33 @@ export interface ChatConversationVO {
|
|||
temperature: number // 温度参数
|
||||
maxTokens: number // 单条回复的最大 Token 数量
|
||||
maxContexts: number // 上下文的最大 Message 数量
|
||||
updateTime: number // 更新时间
|
||||
// 额外字段
|
||||
systemMessage?: string // 角色设定
|
||||
modelName?: string // 模型名字
|
||||
roleAvatar?: string // 角色头像
|
||||
modelMaxTokens?: string // 模型的单条回复的最大 Token 数量
|
||||
modelMaxContexts?: string // 模型的上下文的最大 Message 数量
|
||||
}
|
||||
|
||||
// AI 聊天会话 API
|
||||
// AI 聊天对话 API
|
||||
export const ChatConversationApi = {
|
||||
// 获得【我的】聊天会话
|
||||
getChatConversationMy: async (id: number) => {
|
||||
// 获得【我的】聊天对话
|
||||
getChatConversationMy: async (id: string) => {
|
||||
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) => {
|
||||
// 删除【我的】聊天对话
|
||||
deleteChatConversationMy: async (id: string) => {
|
||||
return await request.delete({ url: `/ai/chat/conversation/delete-my?id=${id}` })
|
||||
},
|
||||
|
||||
|
@ -47,8 +47,18 @@ export const ChatConversationApi = {
|
|||
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` })
|
||||
},
|
||||
|
||||
// 获得对话分页
|
||||
getChatConversationPage: async (params: any) => {
|
||||
return await request.get({ url: `/ai/chat/conversation/page`, params })
|
||||
},
|
||||
|
||||
// 管理员删除消息
|
||||
deleteChatConversationByAdmin: async (id: number) => {
|
||||
return await request.delete({ url: `/ai/chat/conversation/delete-by-admin?id=${id}` })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ import { config } from '@/config/axios/config'
|
|||
// 聊天VO
|
||||
export interface ChatMessageVO {
|
||||
id: number // 编号
|
||||
conversationId: number // 会话编号
|
||||
conversationId: number // 对话编号
|
||||
type: string // 消息类型
|
||||
userId: string // 用户编号
|
||||
roleId: string // 角色编号
|
||||
|
@ -15,17 +15,19 @@ export interface ChatMessageVO {
|
|||
content: string // 聊天内容
|
||||
tokens: number // 消耗 Token 数量
|
||||
createTime: Date // 创建时间
|
||||
roleAvatar: string // 角色头像
|
||||
userAvatar: string // 创建时间
|
||||
}
|
||||
|
||||
export interface ChatMessageSendVO {
|
||||
conversationId: string // 会话编号
|
||||
conversationId: string // 对话编号
|
||||
content: number // 聊天内容
|
||||
}
|
||||
|
||||
// AI chat 聊天
|
||||
export const ChatMessageApi = {
|
||||
// 消息列表
|
||||
messageList: async (conversationId: number | null) => {
|
||||
messageList: async (conversationId: string | null) => {
|
||||
return await request.get({
|
||||
url: `/ai/chat/message/list-by-conversation-id?conversationId=${conversationId}`
|
||||
})
|
||||
|
@ -37,6 +39,7 @@ export const ChatMessageApi = {
|
|||
conversationId: number,
|
||||
content: string,
|
||||
ctrl,
|
||||
enableContext: boolean,
|
||||
onMessage,
|
||||
onError,
|
||||
onClose
|
||||
|
@ -51,7 +54,8 @@ export const ChatMessageApi = {
|
|||
openWhenHidden: true,
|
||||
body: JSON.stringify({
|
||||
conversationId,
|
||||
content
|
||||
content,
|
||||
useContext: enableContext
|
||||
}),
|
||||
onmessage: onMessage,
|
||||
onerror: onError,
|
||||
|
@ -60,8 +64,25 @@ export const ChatMessageApi = {
|
|||
})
|
||||
},
|
||||
|
||||
// 发送 send 消息
|
||||
// 删除消息
|
||||
delete: async (id: string) => {
|
||||
return await request.delete({ url: `/ai/chat/message/delete?id=${id}` })
|
||||
},
|
||||
|
||||
// 删除消息 - 对话所有消息
|
||||
deleteByConversationId: async (conversationId: string) => {
|
||||
return await request.delete({
|
||||
url: `/ai/chat/message/delete-by-conversation-id?conversationId=${conversationId}`
|
||||
})
|
||||
},
|
||||
|
||||
// 获得消息分页
|
||||
getChatMessagePage: async (params: any) => {
|
||||
return await request.get({ url: '/ai/chat/message/page', params })
|
||||
},
|
||||
|
||||
// 管理员删除消息
|
||||
deleteChatMessageByAdmin: async (id: number) => {
|
||||
return await request.delete({ url: `/ai/chat/message/delete-by-admin?id=${id}` })
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
import request from '@/config/axios'
|
||||
|
||||
// AI 绘图 VO
|
||||
export interface ImageVO {
|
||||
id: number // 编号
|
||||
platform: string // 平台
|
||||
model: string // 模型
|
||||
prompt: string // 提示词
|
||||
width: number // 图片宽度
|
||||
height: number // 图片高度
|
||||
status: number // 状态
|
||||
publicStatus: boolean // 公开状态
|
||||
picUrl: string // 任务地址
|
||||
errorMessage: string // 错误信息
|
||||
options: object // 配置 Map<string, string>
|
||||
taskId: number // 任务编号
|
||||
buttons: ImageMjButtonsVO[] // mj 操作按钮
|
||||
createTime: string // 创建时间
|
||||
}
|
||||
|
||||
export interface ImagePageReqVO {
|
||||
pageNo: number // 分页编号
|
||||
pageSize: number // 分页大小
|
||||
}
|
||||
|
||||
export interface ImageDrawReqVO {
|
||||
platform: string // 平台
|
||||
prompt: string // 提示词
|
||||
model: string // 模型
|
||||
style: string // 图像生成的风格
|
||||
width: string // 图片宽度
|
||||
height: string // 图片高度
|
||||
options: object // 绘制参数,Map<String, String>
|
||||
}
|
||||
|
||||
export interface ImageMidjourneyImagineReqVO {
|
||||
prompt: string // 提示词
|
||||
model: string // 模型 mj nijj
|
||||
base64Array: string[] // size不能为空
|
||||
width: string // 图片宽度
|
||||
height: string // 图片高度
|
||||
version: string // 版本
|
||||
}
|
||||
|
||||
export interface ImageMjActionVO {
|
||||
id: number // 图片编号
|
||||
customId: string // MJ::JOB::upsample::1::85a4b4c1-8835-46c5-a15c-aea34fad1862 动作标识
|
||||
}
|
||||
|
||||
export interface ImageMjButtonsVO {
|
||||
customId: string // MJ::JOB::upsample::1::85a4b4c1-8835-46c5-a15c-aea34fad1862 动作标识
|
||||
emoji: string // 图标 emoji
|
||||
label: string // Make Variations 文本
|
||||
style: number // 样式: 2(Primary)、3(Green)
|
||||
}
|
||||
|
||||
// AI API 密钥 API
|
||||
export const ImageApi = {
|
||||
// 获取【我的】绘图分页
|
||||
getImagePageMy: async (params: ImagePageReqVO) => {
|
||||
return await request.get({ url: `/ai/image/my-page`, params })
|
||||
},
|
||||
// 获取【我的】绘图记录
|
||||
getImageMy: async (id: number) => {
|
||||
return await request.get({ url: `/ai/image/get-my?id=${id}` })
|
||||
},
|
||||
// 获取【我的】绘图记录列表
|
||||
getImageListMyByIds: async (ids: number[]) => {
|
||||
return await request.get({ url: `/ai/image/my-list-by-ids`, params: { ids: ids.join(',') } })
|
||||
},
|
||||
// 生成图片
|
||||
drawImage: async (data: ImageDrawReqVO) => {
|
||||
return await request.post({ url: `/ai/image/draw`, data })
|
||||
},
|
||||
// 删除【我的】绘画记录
|
||||
deleteImageMy: async (id: number) => {
|
||||
return await request.delete({ url: `/ai/image/delete-my?id=${id}` })
|
||||
},
|
||||
|
||||
// ================ midjourney 专属 ================
|
||||
|
||||
// 【Midjourney】生成图片
|
||||
midjourneyImagine: async (data: ImageMidjourneyImagineReqVO) => {
|
||||
return await request.post({ url: `/ai/image/midjourney/imagine`, data })
|
||||
},
|
||||
// 【Midjourney】Action 操作(二次生成图片)
|
||||
midjourneyAction: async (data: ImageMjActionVO) => {
|
||||
return await request.post({ url: `/ai/image/midjourney/action`, data })
|
||||
},
|
||||
|
||||
// ================ 绘图管理 ================
|
||||
|
||||
// 查询绘画分页
|
||||
getImagePage: async (params: any) => {
|
||||
return await request.get({ url: `/ai/image/page`, params })
|
||||
},
|
||||
|
||||
// 更新绘画发布状态
|
||||
updateImage: async (data: any) => {
|
||||
return await request.put({ url: '/ai/image/update-public-status', data })
|
||||
},
|
||||
|
||||
// 删除绘画
|
||||
deleteImage: async (id: number) => {
|
||||
return await request.delete({ url: `/ai/image/delete?id=` + id })
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import request from '@/config/axios'
|
||||
|
||||
// AI 音乐 VO
|
||||
export interface MusicVO {
|
||||
id: number // 编号
|
||||
userId: number // 用户编号
|
||||
title: string // 音乐名称
|
||||
lyric: string // 歌词
|
||||
imageUrl: string // 图片地址
|
||||
audioUrl: string // 音频地址
|
||||
videoUrl: string // 视频地址
|
||||
status: number // 音乐状态
|
||||
gptDescriptionPrompt: string // 描述词
|
||||
prompt: string // 提示词
|
||||
platform: string // 模型平台
|
||||
model: string // 模型
|
||||
generateMode: number // 生成模式
|
||||
tags: string // 音乐风格标签
|
||||
duration: number // 音乐时长
|
||||
publicStatus: boolean // 是否发布
|
||||
taskId: string // 任务id
|
||||
errorMessage: string // 错误信息
|
||||
}
|
||||
|
||||
// AI 音乐 API
|
||||
export const MusicApi = {
|
||||
// 查询音乐分页
|
||||
getMusicPage: async (params: any) => {
|
||||
return await request.get({ url: `/ai/music/page`, params })
|
||||
},
|
||||
|
||||
// 更新音乐
|
||||
updateMusic: async (data: any) => {
|
||||
return await request.put({ url: '/ai/music/update', data })
|
||||
},
|
||||
|
||||
// 删除音乐
|
||||
deleteMusic: async (id: number) => {
|
||||
return await request.delete({ url: `/ai/music/delete?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="1716342375293" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2604" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M899.1 869.6l-53-305.6H864c14.4 0 26-11.6 26-26V346c0-14.4-11.6-26-26-26H618V138c0-14.4-11.6-26-26-26H432c-14.4 0-26 11.6-26 26v182H160c-14.4 0-26 11.6-26 26v192c0 14.4 11.6 26 26 26h17.9l-53 305.6c-0.3 1.5-0.4 3-0.4 4.4 0 14.4 11.6 26 26 26h723c1.5 0 3-0.1 4.4-0.4 14.2-2.4 23.7-15.9 21.2-30zM204 390h272V182h72v208h272v104H204V390z m468 440V674c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8v156H416V674c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8v156H202.8l45.1-260H776l45.1 260H672z" p-id="2605" fill="#8a8a8a"></path></svg>
|
After Width: | Height: | Size: 844 B |
|
@ -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="1716345268026" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5622" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M956.408445 419.226665a250.670939 250.670939 0 0 0-22.425219-209.609236A263.163526 263.163526 0 0 0 652.490412 85.715535 259.784384 259.784384 0 0 0 457.728923 0.008192a261.422756 261.422756 0 0 0-249.44216 178.582564 258.453206 258.453206 0 0 0-172.848261 123.901894c-57.03583 96.868753-44.031251 219.132275 32.153053 302.279661a250.670939 250.670939 0 0 0 22.32282 209.609237 263.163526 263.163526 0 0 0 281.595213 123.901893A259.067596 259.067596 0 0 0 566.271077 1023.990784a260.60357 260.60357 0 0 0 249.339762-178.889759 258.453206 258.453206 0 0 0 172.848261-123.901893c57.445423-96.868753 44.13365-218.82508-32.050655-302.074865zM566.578272 957.124721c-45.362429 0-89.496079-15.666934-124.516283-44.543243 1.638372-0.921584 4.198329-2.150363 6.143895-3.481541l206.537289-117.757998a32.35785 32.35785 0 0 0 16.895713-29.081105V474.82892l87.243317 49.97035c1.023983 0.307195 1.638372 1.228779 1.638372 2.252762v238.075953c0 105.8798-86.936122 191.689541-193.942303 191.996736zM148.588578 781.102113a189.846373 189.846373 0 0 1-23.346803-128.612213c1.535974 1.023983 4.09593 2.559956 6.143895 3.48154L337.922959 773.729439c10.444622 6.143896 23.346803 6.143896 34.098621 0l252.30931-143.664758v99.531108c0 1.023983-0.307195 1.945567-1.331177 2.559956l-208.892449 118.986778a196.297463 196.297463 0 0 1-265.518686-70.04041zM94.112704 335.97688c22.630015-39.013737 58.367008-68.81163 101.16948-84.171369V494.591784c0 11.7758 6.45109 22.93721 16.793315 28.978707l252.30931 143.767156L377.141493 716.796006a3.174346 3.174346 0 0 1-2.867152 0.307195l-208.892448-118.986777A190.870355 190.870355 0 0 1 94.215102 335.874482z m717.607001 164.861198L559.410394 357.070922 646.653711 307.20297a3.174346 3.174346 0 0 1 2.969549-0.307195l208.892449 118.986777a190.358364 190.358364 0 0 1 70.961994 262.139544 194.556693 194.556693 0 0 1-101.16948 84.171369V529.407192a31.538664 31.538664 0 0 0-16.588518-28.671513z m87.03852-129.329002c-1.74077-1.023983-4.300727-2.559956-6.246294-3.48154l-206.639687-117.757999a34.09862 34.09862 0 0 0-33.996222 0L399.566711 393.934295v-99.531108c0-1.023983 0.307195-1.945567 1.331178-2.559956l208.892449-119.089176a195.990268 195.990268 0 0 1 265.518686 70.450003c22.732414 38.706542 31.129071 84.171369 23.346803 128.305018zM352.258716 548.862861l-87.243317-49.560757a2.457558 2.457558 0 0 1-1.638372-2.252762V258.870991c0-105.8798 87.243317-191.996736 194.556692-191.689541a194.556693 194.556693 0 0 1 124.209089 44.543243c-1.638372 0.921584-4.198329 2.252762-6.143896 3.48154l-206.639687 117.757999a31.948257 31.948257 0 0 0-16.793315 29.081105l-0.307194 286.715126z m47.307995-100.759887L512 384.001664l112.535687 63.998912v127.997824l-112.228492 63.998912-112.535687-63.998912-0.307195-127.997824z" p-id="5623" fill="#707070"></path></svg>
|
After Width: | Height: | Size: 3.0 KiB |
|
@ -10,12 +10,13 @@ const prefixCls = getPrefixCls('content-wrap')
|
|||
|
||||
defineProps({
|
||||
title: propTypes.string.def(''),
|
||||
message: propTypes.string.def('')
|
||||
message: propTypes.string.def(''),
|
||||
bodyStyle: propTypes.object.def({ padding: '20px' })
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ElCard :class="[prefixCls, 'mb-15px']" shadow="never">
|
||||
<ElCard :body-style="bodyStyle" :class="[prefixCls, 'mb-15px']" shadow="never">
|
||||
<template v-if="title" #header>
|
||||
<div class="flex items-center">
|
||||
<span class="text-16px font-700">{{ title }}</span>
|
||||
|
@ -30,8 +31,6 @@ defineProps({
|
|||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
<slot></slot>
|
||||
</ElCard>
|
||||
</template>
|
||||
|
|
|
@ -22,7 +22,7 @@ const props = defineProps({
|
|||
|
||||
const elRef = ref<ElRef>(null)
|
||||
|
||||
const isLocal = computed(() => props.icon.startsWith('svg-icon:'))
|
||||
const isLocal = computed(() => props.icon?.startsWith('svg-icon:'))
|
||||
|
||||
const symbolId = computed(() => {
|
||||
return unref(isLocal) ? `#icon-${props.icon.split('svg-icon:')[1]}` : props.icon
|
||||
|
|
|
@ -1,23 +1,33 @@
|
|||
|
||||
<template>
|
||||
<div v-html="contentHtml"></div>
|
||||
<div ref="contentRef" class="markdown-view" v-html="contentHtml"></div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {useClipboard} from "@vueuse/core";
|
||||
|
||||
import {marked} from 'marked'
|
||||
import 'highlight.js/styles/vs2015.min.css'
|
||||
import hljs from 'highlight.js'
|
||||
import {ref} from "vue";
|
||||
|
||||
const {copy} = useClipboard() // 初始化 copy 到粘贴板
|
||||
const contentRef = ref()
|
||||
|
||||
// 代码高亮:https://highlightjs.org/
|
||||
// 转换 markdown:marked
|
||||
|
||||
// marked 渲染器
|
||||
const renderer = {
|
||||
code(code, language, c) {
|
||||
const highlightHtml = hljs.highlight(code, {language: language, ignoreIllegals: true}).value
|
||||
let highlightHtml
|
||||
try {
|
||||
highlightHtml = hljs.highlight(code, {language: language, ignoreIllegals: true}).value
|
||||
} catch (e) {
|
||||
// skip
|
||||
}
|
||||
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>`
|
||||
return `<pre style="position: relative;">${copyHtml}<code class="hljs">${highlightHtml}</code></pre>`
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -54,11 +64,23 @@ const renderMarkdown = async (content: string) => {
|
|||
onMounted(async () => {
|
||||
// 解析转换 markdown
|
||||
await renderMarkdown(props.content as string);
|
||||
//
|
||||
// 添加 copy 监听
|
||||
contentRef.value.addEventListener('click', (e: any) => {
|
||||
console.log(e)
|
||||
if (e.target.id === 'copy') {
|
||||
copy(e.target?.dataset?.copy)
|
||||
ElMessage({
|
||||
message: '复制成功!',
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<style scoped lang="scss">
|
||||
<style lang="scss">
|
||||
.markdown-view {
|
||||
font-family: PingFang SC;
|
||||
font-size: 0.95rem;
|
||||
|
|
|
@ -72,7 +72,7 @@ $prefix-cls: #{$namespace}-layout;
|
|||
.#{$prefix-cls} {
|
||||
background-color: var(--app-content-bg-color);
|
||||
:deep(.#{$elNamespace}-scrollbar__view) {
|
||||
height: 100% !important;
|
||||
height: 99% !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -38,24 +38,24 @@ provide('reload', reload)
|
|||
:class="[
|
||||
'p-[var(--app-content-padding)] w-[calc(100%-var(--app-content-padding)-var(--app-content-padding))] bg-[var(--app-content-bg-color)] dark:bg-[var(--el-bg-color)]',
|
||||
{
|
||||
'!min-h-[calc(100%-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height))]':
|
||||
'!h-[calc(100%-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height))]':
|
||||
(fixedHeader &&
|
||||
(layout === 'classic' || layout === 'topLeft' || layout === 'top') &&
|
||||
footer) ||
|
||||
(!tagsView && layout === 'top' && footer),
|
||||
'!min-h-[calc(100%-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height)-var(--tags-view-height))]':
|
||||
'!h-[calc(100%-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height)-var(--tags-view-height))]':
|
||||
tagsView && layout === 'top' && footer,
|
||||
|
||||
'!min-h-[calc(100%-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-var(--top-tool-height)-var(--app-footer-height))]':
|
||||
'!h-[calc(100%-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-var(--top-tool-height)-var(--app-footer-height))]':
|
||||
!fixedHeader && layout === 'classic' && footer,
|
||||
|
||||
'!min-h-[calc(100%-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height))]':
|
||||
'!h-[calc(100%-var(--tags-view-height)-var(--app-content-padding)-var(--app-content-padding)-var(--app-footer-height))]':
|
||||
!fixedHeader && layout === 'topLeft' && footer,
|
||||
|
||||
'!min-h-[calc(100%-var(--top-tool-height)-var(--app-content-padding)-var(--app-content-padding))]':
|
||||
'!h-[calc(100%-var(--top-tool-height)-var(--app-content-padding)-var(--app-content-padding))]':
|
||||
fixedHeader && layout === 'cutMenu' && footer,
|
||||
|
||||
'!min-h-[calc(100%-var(--top-tool-height)-var(--app-content-padding)-var(--app-content-padding)-var(--tags-view-height))]':
|
||||
'!h-[calc(100%-var(--top-tool-height)-var(--app-content-padding)-var(--app-content-padding)-var(--tags-view-height))]':
|
||||
!fixedHeader && layout === 'cutMenu' && footer
|
||||
}
|
||||
]"
|
||||
|
|
|
@ -70,6 +70,26 @@ const remainingRouter: AppRouteRecordRaw[] = [
|
|||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/ai/music',
|
||||
component: Layout,
|
||||
redirect: '/index',
|
||||
name: 'AIMusic',
|
||||
meta: {},
|
||||
children: [
|
||||
{
|
||||
path: 'index',
|
||||
component: () => import('@/views/ai/music/components/index.vue'),
|
||||
name: 'AIMusicIndex',
|
||||
meta: {
|
||||
title: 'AI 音乐',
|
||||
icon: 'ep:home-filled',
|
||||
noCache: false,
|
||||
affix: true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: '/user',
|
||||
component: Layout,
|
||||
|
|
|
@ -24,6 +24,10 @@ export interface NumberDictDataType extends DictDataType {
|
|||
value: number
|
||||
}
|
||||
|
||||
export interface StringDictDataType extends DictDataType {
|
||||
value: string
|
||||
}
|
||||
|
||||
export const getDictOptions = (dictType: string) => {
|
||||
return dictStore.getDictByType(dictType) || []
|
||||
}
|
||||
|
@ -44,8 +48,11 @@ export const getIntDictOptions = (dictType: string): NumberDictDataType[] => {
|
|||
}
|
||||
|
||||
export const getStrDictOptions = (dictType: string) => {
|
||||
const dictOption: DictDataType[] = []
|
||||
// 获得通用的 DictDataType 列表
|
||||
const dictOptions: DictDataType[] = getDictOptions(dictType)
|
||||
// 转换成 string 类型的 StringDictDataType 类型
|
||||
// why 需要特殊转换:避免 IDEA 在 v-for="dict in getStrDictOptions(...)" 时,el-option 的 key 会告警
|
||||
const dictOption: StringDictDataType[] = []
|
||||
dictOptions.forEach((dict: DictDataType) => {
|
||||
dictOption.push({
|
||||
...dict,
|
||||
|
@ -212,5 +219,8 @@ export enum DICT_TYPE {
|
|||
ERP_STOCK_RECORD_BIZ_TYPE = 'erp_stock_record_biz_type', // 库存明细的业务类型
|
||||
|
||||
// ========== AI - 人工智能模块 ==========
|
||||
AI_PLATFORM = 'ai_platform' // AI 平台
|
||||
AI_PLATFORM = 'ai_platform', // AI 平台
|
||||
AI_IMAGE_STATUS = 'ai_image_status', // AI 图片状态
|
||||
AI_MUSIC_STATUS = 'ai_music_status', // AI 音乐状态
|
||||
AI_GENERATE_MODE = 'ai_generate_mode' // AI 生成模式
|
||||
}
|
||||
|
|
|
@ -0,0 +1,79 @@
|
|||
<template>
|
||||
<div class="chat-empty">
|
||||
|
||||
<!-- title -->
|
||||
<div class="center-container">
|
||||
<div class="title">芋艿 AI</div>
|
||||
<div class="role-list">
|
||||
<div class="role-item" v-for="prompt in promptList" :key="prompt.prompt" @click="handlerPromptClick(prompt)">
|
||||
{{prompt.prompt}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
|
||||
const promptList = ref<any[]>() // 角色列表
|
||||
promptList.value = [
|
||||
{
|
||||
"prompt": "今天气怎么样?",
|
||||
},
|
||||
{
|
||||
"prompt": "写一首好听的诗歌?",
|
||||
}
|
||||
]
|
||||
|
||||
const emits = defineEmits(['onPrompt'])
|
||||
|
||||
const handlerPromptClick = async ({ prompt }) => {
|
||||
emits('onPrompt', prompt)
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.chat-empty {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.center-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.title {
|
||||
font-size: 28px;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.role-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 460px;
|
||||
margin-top: 20px;
|
||||
|
||||
.role-item {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 180px;
|
||||
line-height: 50px;
|
||||
border: 1px solid #e4e4e4;
|
||||
border-radius: 10px;
|
||||
margin: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.role-item:hover {
|
||||
background-color: rgba(243, 243, 243, 0.73);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,521 @@
|
|||
<!-- AI 对话 -->
|
||||
<template>
|
||||
<el-aside width="260px" class="conversation-container" style="height: 100%;">
|
||||
|
||||
<!-- 左顶部:对话 -->
|
||||
<div style="height: 100%;">
|
||||
<el-button class="w-1/1 btn-new-conversation" type="primary" @click="createConversation">
|
||||
<Icon icon="ep:plus" class="mr-5px"/>
|
||||
新建对话
|
||||
</el-button>
|
||||
|
||||
<!-- 左顶部:搜索对话 -->
|
||||
<el-input
|
||||
v-model="searchName"
|
||||
size="large"
|
||||
class="mt-10px search-input"
|
||||
placeholder="搜索历史记录"
|
||||
@keyup="searchConversation"
|
||||
>
|
||||
<template #prefix>
|
||||
<Icon icon="ep:search"/>
|
||||
</template>
|
||||
</el-input>
|
||||
|
||||
<!-- 左中间:对话列表 -->
|
||||
<div class="conversation-list">
|
||||
|
||||
<el-empty v-if="loading" description="." :v-loading="loading" />
|
||||
|
||||
<div v-for="conversationKey in Object.keys(conversationMap)" :key="conversationKey">
|
||||
<div class="conversation-item classify-title" v-if="conversationMap[conversationKey].length">
|
||||
<el-text class="mx-1" size="small" tag="b">{{ conversationKey }}</el-text>
|
||||
</div>
|
||||
<div
|
||||
class="conversation-item"
|
||||
v-for="conversation in conversationMap[conversationKey]"
|
||||
:key="conversation.id"
|
||||
@click="handleConversationClick(conversation.id)"
|
||||
@mouseover="hoverConversationId = conversation.id"
|
||||
@mouseout="hoverConversationId = ''"
|
||||
>
|
||||
<div
|
||||
:class="conversation.id === activeConversationId ? 'conversation active' : 'conversation'"
|
||||
>
|
||||
<div class="title-wrapper">
|
||||
<img class="avatar" :src="conversation.roleAvatar || roleAvatarDefaultImg"/>
|
||||
<span class="title">{{ conversation.title }}</span>
|
||||
</div>
|
||||
<div class="button-wrapper" v-show="hoverConversationId === conversation.id">
|
||||
<el-button class="btn" link @click.stop="handlerTop(conversation)" >
|
||||
<el-icon title="置顶" v-if="!conversation.pinned"><Top /></el-icon>
|
||||
<el-icon title="置顶" v-if="conversation.pinned"><Bottom /></el-icon>
|
||||
</el-button>
|
||||
<el-button class="btn" link @click.stop="updateConversationTitle(conversation)">
|
||||
<el-icon title="编辑" >
|
||||
<Icon icon="ep:edit"/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-button class="btn" link @click.stop="deleteChatConversation(conversation)">
|
||||
<el-icon title="删除对话" >
|
||||
<Icon icon="ep:delete"/>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 底部站位 -->
|
||||
<div style="height: 160px; width: 100%;"></div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- 左底部:工具栏 -->
|
||||
<!-- TODO @fan:下面两个 icon,可以使用类似 <Icon icon="ep:question-filled" /> 替代哈 -->
|
||||
<div class="tool-box">
|
||||
<div @click="handleRoleRepository">
|
||||
<Icon icon="ep:user"/>
|
||||
<el-text size="small">角色仓库</el-text>
|
||||
</div>
|
||||
<div @click="handleClearConversation">
|
||||
<Icon icon="ep:delete"/>
|
||||
<el-text size="small">清空未置顶对话</el-text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ============= 额外组件 ============= -->
|
||||
|
||||
<!-- 角色仓库抽屉 -->
|
||||
<el-drawer v-model="drawer" title="角色仓库" size="754px">
|
||||
<Role/>
|
||||
</el-drawer>
|
||||
|
||||
</el-aside>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ChatConversationApi, ChatConversationVO} from '@/api/ai/chat/conversation'
|
||||
import {ref} from "vue";
|
||||
import Role from "@/views/ai/chat/role/index.vue";
|
||||
import {Bottom, Top} from "@element-plus/icons-vue";
|
||||
import roleAvatarDefaultImg from '@/assets/ai/gpt.svg'
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
// 定义属性
|
||||
const searchName = ref<string>('') // 对话搜索
|
||||
const activeConversationId = ref<string | null>(null) // 选中的对话,默认为 null
|
||||
const hoverConversationId = ref<string | null>(null) // 悬浮上去的对话
|
||||
const conversationList = ref([] as ChatConversationVO[]) // 对话列表
|
||||
const conversationMap = ref<any>({}) // 对话分组 (置顶、今天、三天前、一星期前、一个月前)
|
||||
const drawer = ref<boolean>(false) // 角色仓库抽屉 TODO @fan:roleDrawer 会不会好点哈
|
||||
const loading = ref<boolean>(false) // 加载中
|
||||
const loadingTime = ref<any>() // 加载中定时器
|
||||
|
||||
// 定义组件 props
|
||||
const props = defineProps({
|
||||
activeId: {
|
||||
type: String || null,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
// 定义钩子
|
||||
const emits = defineEmits([
|
||||
'onConversationCreate',
|
||||
'onConversationClick',
|
||||
'onConversationClear',
|
||||
'onConversationDelete'
|
||||
])
|
||||
|
||||
/**
|
||||
* 对话 - 搜索
|
||||
*/
|
||||
const searchConversation = async (e) => {
|
||||
// 恢复数据
|
||||
if (!searchName.value.trim().length) {
|
||||
conversationMap.value = await conversationTimeGroup(conversationList.value)
|
||||
} else {
|
||||
// 过滤
|
||||
const filterValues = conversationList.value.filter(item => {
|
||||
return item.title.includes(searchName.value.trim())
|
||||
})
|
||||
conversationMap.value = await conversationTimeGroup(filterValues)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对话 - 点击
|
||||
*/
|
||||
const handleConversationClick = async (id: string) => {
|
||||
// 过滤出选中的对话
|
||||
const filterConversation = conversationList.value.filter(item => {
|
||||
return item.id === id
|
||||
})
|
||||
// 回调 onConversationClick
|
||||
// TODO @fan: 这里 idea 会报黄色警告,有办法解下么?
|
||||
const res = emits('onConversationClick', filterConversation[0])
|
||||
// 切换对话
|
||||
if (res) {
|
||||
activeConversationId.value = id
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对话 - 获取列表
|
||||
*/
|
||||
const getChatConversationList = async () => {
|
||||
try {
|
||||
// 0. 加载中
|
||||
loadingTime.value = setTimeout(() => {
|
||||
loading.value = true
|
||||
}, 50)
|
||||
// 1. 获取 对话数据
|
||||
const res = await ChatConversationApi.getChatConversationMyList()
|
||||
// 2. 排序
|
||||
res.sort((a, b) => {
|
||||
return b.createTime - a.createTime
|
||||
})
|
||||
conversationList.value = res
|
||||
// 3. 默认选中
|
||||
if (!activeId?.value) {
|
||||
// await handleConversationClick(res[0].id)
|
||||
} else {
|
||||
// tip: 删除的刚好是选中的,那么需要重新挑选一个来进行选中
|
||||
// const filterConversationList = conversationList.value.filter(item => {
|
||||
// return item.id === activeId.value
|
||||
// })
|
||||
// if (filterConversationList.length <= 0) {
|
||||
// await handleConversationClick(res[0].id)
|
||||
// }
|
||||
}
|
||||
// 4. 没有任何对话情况
|
||||
if (conversationList.value.length === 0) {
|
||||
activeConversationId.value = null
|
||||
conversationMap.value = {}
|
||||
return
|
||||
}
|
||||
// 5. 对话根据时间分组(置顶、今天、一天前、三天前、七天前、30天前)
|
||||
conversationMap.value = await conversationTimeGroup(conversationList.value)
|
||||
} finally {
|
||||
// 清理定时器
|
||||
if (loadingTime.value) {
|
||||
clearTimeout(loadingTime.value)
|
||||
}
|
||||
// 加载完成
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
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)
|
||||
}
|
||||
}
|
||||
console.log('----groupMap', groupMap)
|
||||
return groupMap
|
||||
}
|
||||
|
||||
/**
|
||||
* 对话 - 新建
|
||||
*/
|
||||
const createConversation = async () => {
|
||||
// 1. 新建对话
|
||||
const conversationId = await ChatConversationApi.createChatConversationMy(
|
||||
{} as unknown as ChatConversationVO
|
||||
)
|
||||
// 2. 获取对话内容
|
||||
await getChatConversationList()
|
||||
// 3. 选中对话
|
||||
await handleConversationClick(conversationId)
|
||||
// 4. 回调
|
||||
emits('onConversationCreate')
|
||||
}
|
||||
|
||||
/**
|
||||
* 对话 - 更新标题
|
||||
*/
|
||||
const updateConversationTitle = async (conversation: ChatConversationVO) => {
|
||||
// 1. 二次确认
|
||||
const {value} = await ElMessageBox.prompt('修改标题', {
|
||||
inputPattern: /^[\s\S]*.*\S[\s\S]*$/, // 判断非空,且非空格
|
||||
inputErrorMessage: '标题不能为空',
|
||||
inputValue: conversation.title
|
||||
})
|
||||
// 2. 发起修改
|
||||
await ChatConversationApi.updateChatConversationMy({
|
||||
id: conversation.id,
|
||||
title: value
|
||||
} as ChatConversationVO)
|
||||
message.success('重命名成功')
|
||||
// 3. 刷新列表
|
||||
await getChatConversationList()
|
||||
// 4. 过滤当前切换的
|
||||
const filterConversationList = conversationList.value.filter(item => {
|
||||
return item.id === conversation.id
|
||||
})
|
||||
if (filterConversationList.length > 0) {
|
||||
// tip:避免切换对话
|
||||
if (activeConversationId.value === filterConversationList[0].id) {
|
||||
emits('onConversationClick', filterConversationList[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除聊天对话
|
||||
*/
|
||||
const deleteChatConversation = async (conversation: ChatConversationVO) => {
|
||||
try {
|
||||
// 删除的二次确认
|
||||
await message.delConfirm(`是否确认删除对话 - ${conversation.title}?`)
|
||||
// 发起删除
|
||||
await ChatConversationApi.deleteChatConversationMy(conversation.id)
|
||||
message.success('对话已删除')
|
||||
// 刷新列表
|
||||
await getChatConversationList()
|
||||
// 回调
|
||||
emits('onConversationDelete', conversation)
|
||||
} catch {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 对话置顶
|
||||
*/
|
||||
// TODO @fan:应该是 handleXXX,handler 是名词哈
|
||||
const handlerTop = async (conversation: ChatConversationVO) => {
|
||||
// 更新对话置顶
|
||||
conversation.pinned = !conversation.pinned
|
||||
await ChatConversationApi.updateChatConversationMy(conversation)
|
||||
// 刷新对话
|
||||
await getChatConversationList()
|
||||
}
|
||||
|
||||
// TODO @fan:类似 ============ 分块的,最后后面也有 ============ 哈
|
||||
// ============ 角色仓库
|
||||
|
||||
/**
|
||||
* 角色仓库抽屉
|
||||
*/
|
||||
const handleRoleRepository = async () => {
|
||||
drawer.value = !drawer.value
|
||||
}
|
||||
|
||||
// ============= 清空对话
|
||||
|
||||
/**
|
||||
* 清空对话
|
||||
*/
|
||||
const handleClearConversation = async () => {
|
||||
// TODO @fan:可以使用 await message.confirm( 简化,然后使用 await 改成同步的逻辑,会更简洁
|
||||
ElMessageBox.confirm(
|
||||
'确认后对话会全部清空,置顶的对话除外。',
|
||||
'确认提示',
|
||||
{
|
||||
confirmButtonText: '确认',
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning',
|
||||
})
|
||||
.then(async () => {
|
||||
await ChatConversationApi.deleteMyAllExceptPinned()
|
||||
ElMessage({
|
||||
message: '操作成功!',
|
||||
type: 'success'
|
||||
})
|
||||
// 清空 对话 和 对话内容
|
||||
activeConversationId.value = null
|
||||
// 获取 对话列表
|
||||
await getChatConversationList()
|
||||
// 回调 方法
|
||||
emits('onConversationClear')
|
||||
})
|
||||
.catch(() => {
|
||||
})
|
||||
}
|
||||
|
||||
// ============ 组件 onMounted
|
||||
|
||||
const { activeId } = toRefs(props)
|
||||
watch(activeId, async (newValue, oldValue) => {
|
||||
// 更新选中
|
||||
activeConversationId.value = newValue as string
|
||||
})
|
||||
|
||||
// 定义 public 方法
|
||||
defineExpose({createConversation})
|
||||
|
||||
onMounted(async () => {
|
||||
// 获取 对话列表
|
||||
await getChatConversationList()
|
||||
// 默认选中
|
||||
if (props.activeId != null) {
|
||||
activeConversationId.value = props.activeId
|
||||
} else {
|
||||
// 首次默认选中第一个
|
||||
if (conversationList.value.length) {
|
||||
activeConversationId.value = conversationList.value[0].id
|
||||
// 回调 onConversationClick
|
||||
await emits('onConversationClick', conversationList.value[0])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.conversation-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
padding: 0 10px;
|
||||
padding-top: 10px;
|
||||
overflow: hidden;
|
||||
|
||||
.btn-new-conversation {
|
||||
padding: 18px 0;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.conversation-list {
|
||||
overflow: auto;
|
||||
height: 100%;
|
||||
|
||||
.classify-title {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.conversation-item {
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.conversation {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
flex: 1;
|
||||
padding: 0 5px;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
align-items: center;
|
||||
line-height: 30px;
|
||||
|
||||
&.active {
|
||||
background-color: #e6e6e6;
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.title-wrapper {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
padding: 2px 10px;
|
||||
max-width: 220px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: rgba(0, 0, 0, 0.77);
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
border-radius: 5px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
// 对话编辑、删除
|
||||
.button-wrapper {
|
||||
right: 2px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-items: center;
|
||||
color: #606266;
|
||||
|
||||
.btn {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 角色仓库、清空未设置对话
|
||||
.tool-box {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
//width: 100%;
|
||||
padding: 0 20px;
|
||||
background-color: #f4f4f4;
|
||||
box-shadow: 0 0 1px 1px rgba(228, 228, 228, 0.8);
|
||||
line-height: 35px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
color: var(--el-text-color);
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #606266;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
|
||||
> span {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,322 @@
|
|||
<template>
|
||||
<div ref="messageContainer" style="height: 100%; overflow-y: auto; position: relative">
|
||||
<div class="chat-list" v-for="(item, index) in list" :key="index">
|
||||
<!-- 靠左 message -->
|
||||
<div class="left-message message-item" v-if="item.type !== 'user'">
|
||||
<div class="avatar">
|
||||
<el-avatar :src="roleAvatar" />
|
||||
</div>
|
||||
<div class="message">
|
||||
<div>
|
||||
<el-text class="time">{{ formatDate(item.createTime) }}</el-text>
|
||||
</div>
|
||||
<div class="left-text-container" ref="markdownViewRef">
|
||||
<MarkdownView class="left-text" :content="item.content" />
|
||||
</div>
|
||||
<div class="left-btns">
|
||||
<el-button class="btn-cus" link @click="noCopy(item.content)">
|
||||
<img class="btn-image" src="@/assets/ai/copy.svg" />
|
||||
</el-button>
|
||||
<el-button class="btn-cus" link @click="onDelete(item.id)">
|
||||
<img class="btn-image" src="@/assets/ai/delete.svg" style="height: 17px" />
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 靠右 message -->
|
||||
<div class="right-message message-item" v-if="item.type === 'user'">
|
||||
<div class="avatar">
|
||||
<el-avatar :src="userAvatar" />
|
||||
</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>
|
||||
</div>
|
||||
<div class="right-btns">
|
||||
<el-button class="btn-cus" link @click="noCopy(item.content)">
|
||||
<img class="btn-image" src="@/assets/ai/copy.svg" />
|
||||
</el-button>
|
||||
<el-button class="btn-cus" link @click="onDelete(item.id)">
|
||||
<img
|
||||
class="btn-image"
|
||||
src="@/assets/ai/delete.svg"
|
||||
style="height: 17px; margin-right: 12px"
|
||||
/>
|
||||
</el-button>
|
||||
<el-button class="btn-cus" link @click="onRefresh(item)">
|
||||
<el-icon size="17"><RefreshRight /></el-icon>
|
||||
</el-button>
|
||||
<el-button class="btn-cus" link @click="onEdit(item)">
|
||||
<el-icon size="17"><Edit /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 回到底部 -->
|
||||
<div v-if="isScrolling" class="to-bottom" @click="handleGoBottom">
|
||||
<el-button :icon="ArrowDownBold" circle />
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { formatDate } from '@/utils/formatTime'
|
||||
import MarkdownView from '@/components/MarkdownView/index.vue'
|
||||
import { ChatMessageApi, ChatMessageVO } from '@/api/ai/chat/message'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
import { PropType } from 'vue'
|
||||
import { ArrowDownBold, Edit, RefreshRight } from '@element-plus/icons-vue'
|
||||
import { ChatConversationVO } from '@/api/ai/chat/conversation'
|
||||
import {useUserStore} from '@/store/modules/user';
|
||||
import userAvatarDefaultImg from '@/assets/imgs/avatar.gif'
|
||||
import roleAvatarDefaultImg from '@/assets/ai/gpt.svg'
|
||||
|
||||
const { copy } = useClipboard() // 初始化 copy 到粘贴板
|
||||
// 判断 消息列表 滚动的位置(用于判断是否需要滚动到消息最下方)
|
||||
const messageContainer: any = ref(null)
|
||||
const isScrolling = ref(false) //用于判断用户是否在滚动
|
||||
|
||||
const userStore = useUserStore()
|
||||
const userAvatar = computed(() => userStore.user.avatar ?? userAvatarDefaultImg)
|
||||
const roleAvatar = computed(() => props.conversation.roleAvatar ?? roleAvatarDefaultImg)
|
||||
|
||||
// 定义 props
|
||||
const props = defineProps({
|
||||
conversation: {
|
||||
type: Object as PropType<ChatConversationVO>,
|
||||
required: true
|
||||
},
|
||||
list: {
|
||||
type: Array as PropType<ChatMessageVO[]>,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
// ============ 处理对话滚动 ==============
|
||||
|
||||
const scrollToBottom = async (isIgnore?: boolean) => {
|
||||
await nextTick(() => {
|
||||
// TODO @fan:中文写作习惯,中英文之间要有空格;另外,nextick 哈,idea 如果有绿色波兰线,可以关注下
|
||||
//注意要使用nexttick以免获取不到dom
|
||||
if (isIgnore || !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 - 100) {
|
||||
// 用户开始滚动并在最底部之上,取消保持在最底部的效果
|
||||
isScrolling.value = true
|
||||
} else {
|
||||
// 用户停止滚动并滚动到最底部,开启保持到最底部的效果
|
||||
isScrolling.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制
|
||||
*/
|
||||
const noCopy = async (content) => {
|
||||
copy(content)
|
||||
ElMessage({
|
||||
message: '复制成功!',
|
||||
type: 'success'
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*/
|
||||
const onDelete = async (id) => {
|
||||
// 删除 message
|
||||
await ChatMessageApi.delete(id)
|
||||
ElMessage({
|
||||
message: '删除成功!',
|
||||
type: 'success'
|
||||
})
|
||||
// 回调
|
||||
emits('onDeleteSuccess')
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新
|
||||
*/
|
||||
const onRefresh = async (message: ChatMessageVO) => {
|
||||
emits('onRefresh', message)
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑
|
||||
*/
|
||||
const onEdit = async (message: ChatMessageVO) => {
|
||||
emits('onEdit', message)
|
||||
}
|
||||
|
||||
/**
|
||||
* 回到底部
|
||||
*/
|
||||
const handleGoBottom = async () => {
|
||||
const scrollContainer = messageContainer.value
|
||||
scrollContainer.scrollTop = scrollContainer.scrollHeight
|
||||
}
|
||||
|
||||
/**
|
||||
* 回到顶部
|
||||
*/
|
||||
const handlerGoTop = async () => {
|
||||
const scrollContainer = messageContainer.value
|
||||
scrollContainer.scrollTop = 0
|
||||
}
|
||||
|
||||
// 监听 list
|
||||
// TODO @fan:这个木有,是不是删除啦
|
||||
const { list, conversationId } = toRefs(props)
|
||||
watch(list, async (newValue, oldValue) => {
|
||||
console.log('watch list', list)
|
||||
})
|
||||
|
||||
// 提供方法给 parent 调用
|
||||
defineExpose({ scrollToBottom, handlerGoTop })
|
||||
|
||||
// 定义 emits
|
||||
const emits = defineEmits(['onDeleteSuccess', 'onRefresh', 'onEdit'])
|
||||
|
||||
// onMounted
|
||||
onMounted(async () => {
|
||||
messageContainer.value.addEventListener('scroll', handleScroll)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.message-container {
|
||||
position: relative;
|
||||
//top: 0;
|
||||
//bottom: 0;
|
||||
//left: 0;
|
||||
//right: 0;
|
||||
//width: 100%;
|
||||
//height: 100%;
|
||||
overflow-y: scroll;
|
||||
//padding: 0 15px;
|
||||
//z-index: -1;
|
||||
}
|
||||
|
||||
// 中间
|
||||
.chat-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: hidden;
|
||||
padding: 0 20px;
|
||||
.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 {
|
||||
position: relative;
|
||||
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;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.left-btns {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.right-btns {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
// 复制、删除按钮
|
||||
.btn-cus {
|
||||
display: flex;
|
||||
background-color: transparent;
|
||||
align-items: center;
|
||||
|
||||
.btn-image {
|
||||
height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-cus:hover {
|
||||
cursor: pointer;
|
||||
background-color: #f6f6f6;
|
||||
}
|
||||
}
|
||||
|
||||
// 回到底部
|
||||
.to-bottom {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
bottom: 0;
|
||||
right: 50%;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,15 @@
|
|||
<!-- message 加载页面 -->
|
||||
<template>
|
||||
<div class="message-loading" >
|
||||
<el-skeleton animated />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.message-loading {
|
||||
padding: 30px 30px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,50 @@
|
|||
<!-- message 新增对话 -->
|
||||
<template>
|
||||
<div class="new-chat" >
|
||||
<div class="box-center">
|
||||
<div class="tip">点击下方按钮,开始你的对话吧</div>
|
||||
<div class="btns"><el-button type="primary" round @click="handlerNewChat">新建对话</el-button></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
// 定义钩子
|
||||
const emits = defineEmits(['onNewChat'])
|
||||
|
||||
/**
|
||||
* 新建 chat
|
||||
*/
|
||||
const handlerNewChat = async () => {
|
||||
await emits('onNewChat')
|
||||
}
|
||||
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.new-chat {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
.box-center {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
.tip {
|
||||
font-size: 14px;
|
||||
color: #858585;
|
||||
}
|
||||
|
||||
.btns {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -8,7 +8,7 @@
|
|||
v-loading="formLoading"
|
||||
>
|
||||
<el-form-item label="角色设定" prop="systemMessage">
|
||||
<el-input type="textarea" v-model="formData.systemMessage" placeholder="请输入角色设定" />
|
||||
<el-input type="textarea" v-model="formData.systemMessage" rows="4" placeholder="请输入角色设定" />
|
||||
</el-form-item>
|
||||
<el-form-item label="模型" prop="modelId">
|
||||
<el-select v-model="formData.modelId" placeholder="请选择模型">
|
||||
|
|
|
@ -45,6 +45,4 @@ defineProps({
|
|||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
|
|
@ -1,156 +0,0 @@
|
|||
|
||||
<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>
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,163 @@
|
|||
<template>
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form-item label="用户编号" prop="userId">
|
||||
<el-select
|
||||
v-model="queryParams.userId"
|
||||
clearable
|
||||
placeholder="请输入用户编号"
|
||||
class="!w-240px"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in userList"
|
||||
:key="item.id"
|
||||
:label="item.nickname"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="聊天编号" prop="title">
|
||||
<el-input
|
||||
v-model="queryParams.title"
|
||||
placeholder="请输入聊天编号"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建时间" prop="createTime">
|
||||
<el-date-picker
|
||||
v-model="queryParams.createTime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="daterange"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
|
||||
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-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="id" width="180" fixed="left" />
|
||||
<el-table-column label="对话标题" align="center" prop="title" width="180" fixed="left" />
|
||||
<el-table-column label="用户" align="center" prop="userId" width="180">
|
||||
<template #default="scope">
|
||||
<span>{{ userList.find((item) => item.id === scope.row.userId)?.nickname }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="角色" align="center" prop="roleName" width="180" />
|
||||
<el-table-column label="模型标识" align="center" prop="model" width="180" />
|
||||
<el-table-column label="消息数" align="center" prop="messageCount" />
|
||||
<el-table-column
|
||||
label="创建时间"
|
||||
align="center"
|
||||
prop="createTime"
|
||||
:formatter="dateFormatter"
|
||||
width="180px"
|
||||
/>
|
||||
<el-table-column label="温度参数" align="center" prop="temperature" />
|
||||
<el-table-column label="回复 Token 数" align="center" prop="maxTokens" width="120" />
|
||||
<el-table-column label="上下文数量" align="center" prop="maxContexts" width="120" />
|
||||
<el-table-column label="操作" align="center" width="180" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row.id)"
|
||||
v-hasPermi="['ai:chat-conversation: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>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation'
|
||||
import * as UserApi from '@/api/system/user'
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<ChatConversationVO[]>([]) // 列表的数据
|
||||
const total = ref(0) // 列表的总页数
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
userId: undefined,
|
||||
title: undefined,
|
||||
createTime: []
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
const userList = ref<UserApi.UserVO[]>([]) // 用户列表
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await ChatConversationApi.getChatConversationPage(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 handleDelete = async (id: number) => {
|
||||
try {
|
||||
// 删除的二次确认
|
||||
await message.delConfirm()
|
||||
// 发起删除
|
||||
await ChatConversationApi.deleteChatConversationByAdmin(id)
|
||||
message.success(t('common.delSuccess'))
|
||||
// 刷新列表
|
||||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(async () => {
|
||||
getList()
|
||||
// 获得用户列表
|
||||
userList.value = await UserApi.getSimpleUserList()
|
||||
})
|
||||
</script>
|
|
@ -0,0 +1,175 @@
|
|||
<template>
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form-item label="对话编号" prop="conversationId">
|
||||
<el-input
|
||||
v-model="queryParams.conversationId"
|
||||
placeholder="请输入对话编号"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="用户编号" prop="userId">
|
||||
<el-select
|
||||
v-model="queryParams.userId"
|
||||
clearable
|
||||
placeholder="请输入用户编号"
|
||||
class="!w-240px"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in userList"
|
||||
:key="item.id"
|
||||
:label="item.nickname"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建时间" prop="createTime">
|
||||
<el-date-picker
|
||||
v-model="queryParams.createTime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="daterange"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
|
||||
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-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="id" width="180" fixed="left" />
|
||||
<el-table-column
|
||||
label="对话编号"
|
||||
align="center"
|
||||
prop="conversationId"
|
||||
width="180"
|
||||
fixed="left"
|
||||
/>
|
||||
<el-table-column label="用户" align="center" prop="userId" width="180">
|
||||
<template #default="scope">
|
||||
<span>{{ userList.find((item) => item.id === scope.row.userId)?.nickname }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="角色" align="center" prop="roleName" width="180" />
|
||||
<el-table-column label="消息类型" align="center" prop="type" width="100" />
|
||||
<el-table-column label="模型标识" align="center" prop="model" width="180" />
|
||||
<el-table-column label="消息内容" align="center" prop="content" width="300" />
|
||||
<el-table-column
|
||||
label="创建时间"
|
||||
align="center"
|
||||
prop="createTime"
|
||||
:formatter="dateFormatter"
|
||||
width="180px"
|
||||
/>
|
||||
<el-table-column label="回复消息编号" align="center" prop="replyId" width="180" />
|
||||
<el-table-column label="携带上下文" align="center" prop="useContext" width="100">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.useContext" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="操作" align="center" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row.id)"
|
||||
v-hasPermi="['ai:chat-message: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>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import { ChatMessageApi, ChatMessageVO } from '@/api/ai/chat/message'
|
||||
import * as UserApi from '@/api/system/user'
|
||||
import { DICT_TYPE } from '@/utils/dict'
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<ChatMessageVO[]>([]) // 列表的数据
|
||||
const total = ref(0) // 列表的总页数
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
conversationId: undefined,
|
||||
userId: undefined,
|
||||
content: undefined,
|
||||
createTime: []
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
const userList = ref<UserApi.UserVO[]>([]) // 用户列表
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await ChatMessageApi.getChatMessagePage(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 handleDelete = async (id: number) => {
|
||||
try {
|
||||
// 删除的二次确认
|
||||
await message.delConfirm()
|
||||
// 发起删除
|
||||
await ChatMessageApi.deleteChatMessageByAdmin(id)
|
||||
message.success(t('common.delSuccess'))
|
||||
// 刷新列表
|
||||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(async () => {
|
||||
getList()
|
||||
// 获得用户列表
|
||||
userList.value = await UserApi.getSimpleUserList()
|
||||
})
|
||||
</script>
|
|
@ -0,0 +1,20 @@
|
|||
<template>
|
||||
<ContentWrap>
|
||||
<el-tabs>
|
||||
<el-tab-pane label="对话列表">
|
||||
<ChatConversationList />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane label="消息列表">
|
||||
<ChatMessageList />
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import ChatConversationList from './ChatConversationList.vue'
|
||||
import ChatMessageList from './ChatMessageList.vue'
|
||||
|
||||
/** AI 聊天对话 列表 */
|
||||
defineOptions({ name: 'AiChatManager' })
|
||||
</script>
|
|
@ -1,8 +1,8 @@
|
|||
<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 class="category" v-for="(category) in categoryList" :key="category">
|
||||
<el-button plain round size="small" v-if="category !== active" @click="handleCategoryClick(category)">{{ category }}</el-button>
|
||||
<el-button plain round size="small" v-else type="primary" @click="handleCategoryClick(category)">{{ category }}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -17,7 +17,8 @@ defineProps({
|
|||
},
|
||||
active: {
|
||||
type: String,
|
||||
required: false
|
||||
required: false,
|
||||
default: '全部'
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -40,7 +41,7 @@ const handleCategoryClick = async (category) => {
|
|||
.category {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-right: 20px;
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,59 +1,72 @@
|
|||
<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">
|
||||
<div class="card-list" ref="tabsRef" @scroll="handleTabsScroll">
|
||||
<div class="card-item" v-for="role in roleList" :key="role.id">
|
||||
<el-card class="card" body-class="card-body">
|
||||
<!-- 更多 -->
|
||||
<div class="more-container" v-if="showMore">
|
||||
<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>
|
||||
|
||||
<!-- TODO @fan:下面两个 icon,可以使用类似 <Icon icon="ep:question-filled" /> 替代哈 -->
|
||||
<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 class="btn-container">
|
||||
<el-button type="primary" size="small" @click="handleUseClick(role)">使用</el-button>
|
||||
<!-- 头像 -->
|
||||
<div>
|
||||
<img class="avatar" :src="role.avatar"/>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
<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>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ChatRoleVO} from '@/api/ai/model/chatRole'
|
||||
import {PropType} from "vue";
|
||||
import {PropType, ref} from "vue";
|
||||
import {Delete, EditPen, More} from "@element-plus/icons-vue";
|
||||
|
||||
const tabsRef = ref<any>() // tabs ref
|
||||
|
||||
// 定义属性
|
||||
const props = defineProps({
|
||||
loading: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
roleList: {
|
||||
type: Array as PropType<ChatRoleVO[]>,
|
||||
required: true
|
||||
},
|
||||
showMore: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
// 定义钩子
|
||||
const emits = defineEmits(['onDelete', 'onEdit', 'onUse'])
|
||||
const emits = defineEmits(['onDelete', 'onEdit', 'onUse', 'onPage'])
|
||||
|
||||
// more 点击
|
||||
const handleMoreClick = async (data) => {
|
||||
|
@ -71,6 +84,19 @@ const handleUseClick = (role) => {
|
|||
emits('onUse', role)
|
||||
}
|
||||
|
||||
const handleTabsScroll = async () => {
|
||||
if (tabsRef.value) {
|
||||
const { scrollTop, scrollHeight, clientHeight } = tabsRef.value;
|
||||
console.log('scrollTop', scrollTop)
|
||||
if (scrollTop + clientHeight >= scrollHeight - 20 && !props.loading) {
|
||||
console.log('分页')
|
||||
// page.value++;
|
||||
// fetchData(page.value);
|
||||
await emits('onPage')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
console.log('props', props.roleList)
|
||||
})
|
||||
|
@ -80,9 +106,9 @@ onMounted(() => {
|
|||
<style lang="scss">
|
||||
// 重写 card 组件 body 样式
|
||||
.card-body {
|
||||
max-width: 300px;
|
||||
width: 300px;
|
||||
padding: 15px;
|
||||
max-width: 240px;
|
||||
width: 240px;
|
||||
padding: 15px 15px 10px 15px;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
@ -99,17 +125,25 @@ onMounted(() => {
|
|||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 0px 25px;
|
||||
padding-bottom: 140px;
|
||||
align-items: start;
|
||||
align-content: flex-start;
|
||||
justify-content: start;
|
||||
|
||||
.card {
|
||||
display: inline-block;
|
||||
margin-right: 20px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 30px;
|
||||
margin-bottom: 20px;
|
||||
position: relative;
|
||||
|
||||
.more-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 12px;
|
||||
top: 0px;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
|
@ -126,7 +160,6 @@ onMounted(() => {
|
|||
|
||||
.content-container {
|
||||
height: 85px;
|
||||
overflow: hidden;
|
||||
|
||||
.title {
|
||||
font-size: 18px;
|
||||
|
@ -144,7 +177,7 @@ onMounted(() => {
|
|||
.btn-container {
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
margin-top: 15px;
|
||||
margin-top: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
<!-- chat 角色仓库 -->
|
||||
<template>
|
||||
<el-container class="role-container">
|
||||
<ChatRoleForm ref="formRef" @success="handlerAddRoleSuccess" />
|
||||
|
||||
<Header title="角色仓库"/>
|
||||
<ChatRoleForm ref="formRef" @success="handlerAddRoleSuccess" />
|
||||
<!-- header -->
|
||||
<Header title="角色仓库" style="position: relative" />
|
||||
<!-- main -->
|
||||
<el-main class="role-main">
|
||||
<div class="search-container">
|
||||
<!-- 搜索按钮 -->
|
||||
<el-input
|
||||
:loading="loading"
|
||||
v-model="search"
|
||||
class="search-input"
|
||||
size="default"
|
||||
|
@ -15,119 +17,161 @@
|
|||
:suffix-icon="Search"
|
||||
@change="getActiveTabsRole"
|
||||
/>
|
||||
<el-button type="primary" @click="handlerAddRole" style="margin-left: 20px;">
|
||||
<el-icon><User /></el-icon>
|
||||
<el-button
|
||||
v-if="activeRole == 'my-role'"
|
||||
type="primary"
|
||||
@click="handlerAddRole"
|
||||
style="margin-left: 20px"
|
||||
>
|
||||
<!-- TODO @fan:下面两个 icon,可以使用类似 <Icon icon="ep:question-filled" /> 替代哈 -->
|
||||
<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;" />
|
||||
<RoleList
|
||||
:loading="loading"
|
||||
:role-list="myRoleList"
|
||||
:show-more="true"
|
||||
@on-delete="handlerCardDelete"
|
||||
@on-edit="handlerCardEdit"
|
||||
@on-use="handlerCardUse"
|
||||
@on-page="handlerCardPage('my')"
|
||||
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;" />
|
||||
<RoleCategoryList
|
||||
class="role-category-list"
|
||||
:category-list="categoryList"
|
||||
:active="activeCategory"
|
||||
@on-category-click="handlerCategoryClick"
|
||||
/>
|
||||
<RoleList
|
||||
:role-list="publicRoleList"
|
||||
@on-delete="handlerCardDelete"
|
||||
@on-edit="handlerCardEdit"
|
||||
@on-use="handlerCardUse"
|
||||
@on-page="handlerCardPage('public')"
|
||||
style="margin-top: 20px"
|
||||
loading
|
||||
/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-main>
|
||||
</el-container>
|
||||
|
||||
</template>
|
||||
|
||||
<!-- setup -->
|
||||
<script setup lang="ts">
|
||||
import {ref} from "vue";
|
||||
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";
|
||||
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 router = useRouter() // 路由对象
|
||||
|
||||
// 属性定义
|
||||
const activeRole = ref<string>('my-role') // 选中的角色
|
||||
const loadding = ref<boolean>(true) // 加载中
|
||||
const loading = ref<boolean>(false) // 加载中
|
||||
const activeRole = ref<string>('my-role') // 选中的角色 TODO @fan:是不是叫 activeTab 会更明确一点哈。选中的角色,会以为是某个角色
|
||||
const search = ref<string>('') // 加载中
|
||||
// TODO @fan:要不 myPage、pubPage,搞成类似 const queryParams = reactive({ ,分别搞成两个大的参数哈?
|
||||
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 activeCategory = ref<string>('全部') // 选择中的分类
|
||||
const categoryList = ref<string[]>([]) // 角色分类类别
|
||||
/** 添加/修改操作 */
|
||||
const formRef = ref()
|
||||
// tabs 点击
|
||||
|
||||
/** tabs 点击 */
|
||||
const handleTabsClick = async (tab: TabsPaneContext) => {
|
||||
// 设置切换状态
|
||||
const activeTabs = tab.paneName + ''
|
||||
activeRole.value = activeTabs;
|
||||
activeRole.value = tab.paneName + ''
|
||||
// 切换的时候重新加载数据
|
||||
await getActiveTabsRole()
|
||||
}
|
||||
|
||||
// 获取 my role
|
||||
const getMyRole = async () => {
|
||||
const params:ChatRolePageReqVO = {
|
||||
/** 获取 my role 我的角色 */
|
||||
const getMyRole = async (append?: boolean) => {
|
||||
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
|
||||
if (append) {
|
||||
myRoleList.value.push.apply(myRoleList.value, list)
|
||||
} else {
|
||||
myRoleList.value = list
|
||||
}
|
||||
}
|
||||
|
||||
// 获取 public role
|
||||
const getPublicRole = async () => {
|
||||
const params:ChatRolePageReqVO = {
|
||||
/** 获取 public role 公共角色 */
|
||||
const getPublicRole = async (append?: boolean) => {
|
||||
const params: ChatRolePageReqVO = {
|
||||
pageNo: publicPageNo.value,
|
||||
pageSize: publicPageSize.value,
|
||||
category: activeCategory.value,
|
||||
category: activeCategory.value === '全部' ? '' : activeCategory.value,
|
||||
name: search.value,
|
||||
publicStatus: true
|
||||
}
|
||||
const { total, list } = await ChatRoleApi.getMyPage(params)
|
||||
publicRoleList.value = list
|
||||
if (append) {
|
||||
publicRoleList.value.push.apply(publicRoleList.value, list)
|
||||
} else {
|
||||
publicRoleList.value = list
|
||||
}
|
||||
}
|
||||
|
||||
// 获取选中的 tabs 角色
|
||||
/** 获取选中的 tabs 角色 */
|
||||
const getActiveTabsRole = async () => {
|
||||
if (activeRole.value === 'my-role') {
|
||||
myPageNo.value = 1
|
||||
await getMyRole()
|
||||
} else {
|
||||
publicPageNo.value = 1
|
||||
await getPublicRole()
|
||||
}
|
||||
}
|
||||
|
||||
// 获取角色分类列表
|
||||
/** 获取角色分类列表 */
|
||||
const getRoleCategoryList = async () => {
|
||||
categoryList.value = await ChatRoleApi.getCategoryList()
|
||||
const res = await ChatRoleApi.getCategoryList()
|
||||
const defaultRole = ['全部']
|
||||
categoryList.value = [...defaultRole, ...res]
|
||||
}
|
||||
|
||||
// 处理分类点击
|
||||
/** 处理分类点击 */
|
||||
const handlerCategoryClick = async (category: string) => {
|
||||
if (activeCategory.value === category) {
|
||||
activeCategory.value = ''
|
||||
} else {
|
||||
activeCategory.value = category
|
||||
}
|
||||
// 切换选择的分类
|
||||
activeCategory.value = category
|
||||
// 筛选
|
||||
await getActiveTabsRole()
|
||||
}
|
||||
|
||||
// 添加角色
|
||||
/** 添加/修改操作 */
|
||||
const formRef = ref()
|
||||
const handlerAddRole = async () => {
|
||||
formRef.value.open('my-create', null, '添加角色')
|
||||
}
|
||||
|
||||
/** 添加角色成功 */
|
||||
const handlerAddRoleSuccess = async (e) => {
|
||||
// 刷新数据
|
||||
await getActiveTabsRole()
|
||||
}
|
||||
|
||||
// card 删除
|
||||
const handlerCardDelete = async (role) => {
|
||||
await ChatRoleApi.deleteMy(role.id)
|
||||
|
@ -140,64 +184,91 @@ const handlerCardEdit = async (role) => {
|
|||
formRef.value.open('my-update', role.id, '编辑角色')
|
||||
}
|
||||
|
||||
// card 使用
|
||||
/** card 分页:获取下一页 */
|
||||
const handlerCardPage = async (type) => {
|
||||
console.log('handlerCardPage', type)
|
||||
try {
|
||||
loading.value = true
|
||||
if (type === 'public') {
|
||||
publicPageNo.value++
|
||||
await getPublicRole(true)
|
||||
} else {
|
||||
myPageNo.value++
|
||||
await getMyRole(true)
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/** 选择 card 角色:新建聊天对话 */
|
||||
const handlerCardUse = async (role) => {
|
||||
const data : ChatConversationVO = {
|
||||
// 1. 创建对话
|
||||
const data: ChatConversationVO = {
|
||||
roleId: role.id
|
||||
} as unknown as ChatConversationVO
|
||||
// 创建对话
|
||||
const conversation = await ChatConversationApi.createChatConversationMy(data)
|
||||
// 调整页面
|
||||
router.push({
|
||||
path: `/ai/chat/index`,
|
||||
const conversationId = await ChatConversationApi.createChatConversationMy(data)
|
||||
// 2. 跳转页面
|
||||
// TODO @fan:最好用 name,后续可能会改~~~
|
||||
await router.push({
|
||||
path: `/ai/chat`,
|
||||
query: {
|
||||
conversationId: conversation,
|
||||
conversationId: conversationId
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 添加角色成功
|
||||
const handlerAddRoleSuccess = async (e) => {
|
||||
console.log(e)
|
||||
// 刷新数据
|
||||
await getActiveTabsRole()
|
||||
}
|
||||
|
||||
//
|
||||
onMounted( async () => {
|
||||
/** 初始化 **/
|
||||
onMounted(async () => {
|
||||
// 获取分类
|
||||
await getRoleCategoryList()
|
||||
// 获取 role 数据
|
||||
await getActiveTabsRole()
|
||||
})
|
||||
// TODO @fan:css 是不是可以融合到 scss 里面呀?
|
||||
</script>
|
||||
<style lang="css">
|
||||
.el-tabs__content {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.el-tabs__nav-scroll {
|
||||
margin: 10px 20px;
|
||||
}
|
||||
</style>
|
||||
<!-- 样式 -->
|
||||
<style scoped lang="scss">
|
||||
|
||||
// 跟容器
|
||||
.role-container {
|
||||
position: absolute;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
background-color: #ffffff;
|
||||
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.role-main {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
|
||||
.search-container {
|
||||
//position: absolute;
|
||||
//right: 20px;
|
||||
//top: 10px;
|
||||
//z-index: 100;
|
||||
margin: 20px 20px 0px 20px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: -5px;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.search-input {
|
||||
|
@ -206,14 +277,20 @@ onMounted( async () => {
|
|||
|
||||
.tabs {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
.role-category-list {
|
||||
margin: 0 27px;
|
||||
}
|
||||
}
|
||||
|
||||
.role-pane {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,141 @@
|
|||
<template>
|
||||
<el-drawer
|
||||
v-model="showDrawer"
|
||||
title="图片详细"
|
||||
@close="handlerDrawerClose"
|
||||
custom-class="drawer-class"
|
||||
>
|
||||
<!-- 图片 -->
|
||||
<div class="item">
|
||||
<!-- <div class="header">-->
|
||||
<!-- <div>图片</div>-->
|
||||
<!-- <div>-->
|
||||
<!-- </div>-->
|
||||
<!-- </div>-->
|
||||
<div class="body">
|
||||
<!-- TODO @fan: 要不,这里只展示图片???不用 ImageTaskCard -->
|
||||
<ImageTaskCard :image-detail="imageDetail" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 时间 -->
|
||||
<div class="item">
|
||||
<div class="tip">时间</div>
|
||||
<div class="body">
|
||||
<div>提交时间:{{ imageDetail.createTime }}</div>
|
||||
<!-- TODO @fan:要不加个完成时间的字段 finishTime?updateTime 不算特别合理哈 -->
|
||||
<div>生成时间:{{ imageDetail.updateTime }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 模型 -->
|
||||
<div class="item">
|
||||
<div class="tip">模型</div>
|
||||
<div class="body">
|
||||
{{ imageDetail.model }}({{ imageDetail.height }}x{{ imageDetail.width }})
|
||||
</div>
|
||||
</div>
|
||||
<!-- 提示词 -->
|
||||
<div class="item">
|
||||
<div class="tip">提示词</div>
|
||||
<div class="body">
|
||||
{{ imageDetail.prompt }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- 地址 -->
|
||||
<div class="item">
|
||||
<div class="tip">图片地址</div>
|
||||
<div class="body">
|
||||
{{ imageDetail.picUrl }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- 风格 -->
|
||||
<div class="item" v-if="imageDetail?.options?.style">
|
||||
<div class="tip">风格</div>
|
||||
<div class="body">
|
||||
<!-- TODO @fan:貌似需要把 imageStyleList 搞到 api/image/index.ts 枚举起来? -->
|
||||
<!-- TODO @fan:这里的展示,可能需要按照平台做区分 -->
|
||||
{{ imageDetail?.options?.style }}
|
||||
</div>
|
||||
</div>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ImageApi, ImageVO } from '@/api/ai/image'
|
||||
import ImageTaskCard from './ImageTaskCard.vue'
|
||||
|
||||
const showDrawer = ref<boolean>(false) // 是否显示
|
||||
const imageDetail = ref<ImageVO>({} as ImageVO) // 图片详细信息
|
||||
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
require: true,
|
||||
default: false
|
||||
},
|
||||
id: {
|
||||
type: Number,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
/** 抽屉 - close */
|
||||
const handlerDrawerClose = async () => {
|
||||
emits('handlerDrawerClose')
|
||||
}
|
||||
|
||||
/** 获取 - 图片 detail */
|
||||
const getImageDetail = async (id) => {
|
||||
// 获取图片详细
|
||||
imageDetail.value = await ImageApi.getImageMy(id)
|
||||
}
|
||||
|
||||
/** 任务 - detail */
|
||||
const handlerTaskDetail = async () => {
|
||||
showDrawer.value = true
|
||||
}
|
||||
|
||||
// watch show
|
||||
const { show } = toRefs(props)
|
||||
watch(show, async (newValue, oldValue) => {
|
||||
showDrawer.value = newValue as boolean
|
||||
})
|
||||
// watch id
|
||||
const { id } = toRefs(props)
|
||||
watch(id, async (newVal, oldVal) => {
|
||||
if (newVal) {
|
||||
await getImageDetail(newVal)
|
||||
}
|
||||
})
|
||||
//
|
||||
const emits = defineEmits(['handlerDrawerClose'])
|
||||
//
|
||||
onMounted(async () => {})
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
.item {
|
||||
margin-bottom: 20px;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
word-wrap: break-word;
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.tip {
|
||||
font-weight: bold;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.body {
|
||||
margin-top: 10px;
|
||||
color: #616161;
|
||||
|
||||
.taskImage {
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,244 @@
|
|||
<template>
|
||||
<el-card class="dr-task" body-class="task-card" shadow="never">
|
||||
<template #header>绘画任务</template>
|
||||
<div class="task-image-list" ref="imageTaskRef">
|
||||
<ImageTaskCard
|
||||
v-for="image in imageList"
|
||||
:key="image"
|
||||
:image-detail="image"
|
||||
@on-btn-click="handlerImageBtnClick"
|
||||
@on-mj-btn-click="handlerImageMjBtnClick"
|
||||
/>
|
||||
</div>
|
||||
<div class="task-image-pagination">
|
||||
<el-pagination
|
||||
background
|
||||
layout="prev, pager, next"
|
||||
:default-page-size="pageSize"
|
||||
:total="pageTotal"
|
||||
@change="handlerPageChange"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
<!-- 图片 detail 抽屉 -->
|
||||
<ImageDetailDrawer
|
||||
:show="isShowImageDetail"
|
||||
:id="showImageDetailId"
|
||||
@handler-drawer-close="handlerDrawerClose"
|
||||
/>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ImageApi, ImageVO, ImageMjActionVO, ImageMjButtonsVO } from '@/api/ai/image'
|
||||
import ImageDetailDrawer from './ImageDetailDrawer.vue'
|
||||
import ImageTaskCard from './ImageTaskCard.vue'
|
||||
import { ElLoading, LoadingOptionsResolved } from 'element-plus'
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const imageList = ref<ImageVO[]>([]) // image 列表
|
||||
const inProgressImageMap = ref<{}>({}) // 监听的 image 映射,一般是生成中(需要轮询),key 为 image 编号,value 为 image
|
||||
const imageListInterval = ref<any>() // image 列表定时器,刷新列表
|
||||
const isShowImageDetail = ref<boolean>(false) // 是否显示 task 详情
|
||||
const showImageDetailId = ref<number>(0) // 是否显示 task 详情
|
||||
const imageTaskRef = ref<any>() // ref
|
||||
const imageTaskLoadingInstance = ref<any>() // loading
|
||||
const imageTaskLoading = ref<boolean>(false) // loading
|
||||
const pageNo = ref<number>(1) // page no
|
||||
const pageSize = ref<number>(10) // page size
|
||||
const pageTotal = ref<number>(0) // page size
|
||||
|
||||
/** 抽屉 - close */
|
||||
const handlerDrawerClose = async () => {
|
||||
isShowImageDetail.value = false
|
||||
}
|
||||
|
||||
/** 任务 - detail */
|
||||
const handlerDrawerOpen = async () => {
|
||||
isShowImageDetail.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 - image 列表
|
||||
*/
|
||||
const getImageList = async (apply: boolean = false) => {
|
||||
imageTaskLoading.value = true
|
||||
try {
|
||||
imageTaskLoadingInstance.value = ElLoading.service({
|
||||
target: imageTaskRef.value,
|
||||
text: '加载中...'
|
||||
} as LoadingOptionsResolved)
|
||||
const { list, total } = await ImageApi.getImagePageMy({
|
||||
pageNo: pageNo.value,
|
||||
pageSize: pageSize.value
|
||||
})
|
||||
if (apply) {
|
||||
imageList.value = [...imageList.value, ...list]
|
||||
} else {
|
||||
imageList.value = list
|
||||
}
|
||||
pageTotal.value = total
|
||||
// 需要 watch 的数据
|
||||
const newWatImages = {}
|
||||
imageList.value.forEach((item) => {
|
||||
if (item.status === 10) {
|
||||
newWatImages[item.id] = item
|
||||
}
|
||||
})
|
||||
inProgressImageMap.value = newWatImages
|
||||
} finally {
|
||||
if (imageTaskLoadingInstance.value) {
|
||||
imageTaskLoadingInstance.value.close()
|
||||
imageTaskLoadingInstance.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 轮询生成中的 image 列表 */
|
||||
const refreshWatchImages = async () => {
|
||||
const imageIds = Object.keys(inProgressImageMap.value).map(Number)
|
||||
if (imageIds.length == 0) {
|
||||
return
|
||||
}
|
||||
const list = (await ImageApi.getImageListMyByIds(imageIds)) as ImageVO[]
|
||||
const newWatchImages = {}
|
||||
list.forEach((image) => {
|
||||
if (image.status === 10) {
|
||||
newWatchImages[image.id] = image
|
||||
} else {
|
||||
const index = imageList.value.findIndex((oldImage) => image.id === oldImage.id)
|
||||
if (index >= 0) {
|
||||
// 更新 imageList
|
||||
imageList.value[index] = image
|
||||
}
|
||||
}
|
||||
})
|
||||
inProgressImageMap.value = newWatchImages
|
||||
}
|
||||
|
||||
/** 图片 - btn click */
|
||||
const handlerImageBtnClick = async (type: string, imageDetail: ImageVO) => {
|
||||
// 获取 image detail id
|
||||
showImageDetailId.value = imageDetail.id
|
||||
// 处理不用 btn
|
||||
if (type === 'more') {
|
||||
await handlerDrawerOpen()
|
||||
} else if (type === 'delete') {
|
||||
await message.confirm(`是否删除照片?`)
|
||||
await ImageApi.deleteImageMy(imageDetail.id)
|
||||
await getImageList()
|
||||
message.success('删除成功!')
|
||||
} else if (type === 'download') {
|
||||
await downloadImage(imageDetail.picUrl)
|
||||
}
|
||||
}
|
||||
|
||||
/** 图片 - mj btn click */
|
||||
const handlerImageMjBtnClick = async (button: ImageMjButtonsVO, imageDetail: ImageVO) => {
|
||||
// 1、构建 params 参数
|
||||
const data = {
|
||||
id: imageDetail.id,
|
||||
customId: button.customId
|
||||
} as ImageMjActionVO
|
||||
// 2、发送 action
|
||||
await ImageApi.midjourneyAction(data)
|
||||
// 3、刷新列表
|
||||
await getImageList()
|
||||
}
|
||||
|
||||
/** 下载 - image */
|
||||
// TODO @fan:貌似可以考虑抽到 download 里面,作为一个方法
|
||||
const downloadImage = async (imageUrl) => {
|
||||
const image = new Image()
|
||||
image.setAttribute('crossOrigin', 'anonymous')
|
||||
image.src = imageUrl
|
||||
image.onload = () => {
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = image.width
|
||||
canvas.height = image.height
|
||||
const ctx = canvas.getContext('2d') as CanvasDrawImage
|
||||
ctx.drawImage(image, 0, 0, image.width, image.height)
|
||||
const url = canvas.toDataURL('image/png')
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = 'image.png'
|
||||
a.click()
|
||||
}
|
||||
}
|
||||
|
||||
// page change
|
||||
const handlerPageChange = async (page) => {
|
||||
pageNo.value = page
|
||||
await getImageList(false)
|
||||
}
|
||||
|
||||
/** 暴露组件方法 */
|
||||
defineExpose({ getImageList })
|
||||
|
||||
/** 组件挂在的时候 */
|
||||
onMounted(async () => {
|
||||
// 获取 image 列表
|
||||
await getImageList()
|
||||
// 自动刷新 image 列表
|
||||
imageListInterval.value = setInterval(async () => {
|
||||
await refreshWatchImages()
|
||||
}, 1000 * 3)
|
||||
})
|
||||
|
||||
/** 组件取消挂在的时候 */
|
||||
onUnmounted(async () => {
|
||||
if (imageListInterval.value) {
|
||||
clearInterval(imageListInterval.value)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.task-card {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.task-image-list {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
align-content: flex-start;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
padding: 20px;
|
||||
padding-bottom: 140px;
|
||||
box-sizing: border-box; /* 确保内边距不会增加高度 */
|
||||
|
||||
> div {
|
||||
margin-right: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
> div:last-of-type {
|
||||
//margin-bottom: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.task-image-pagination {
|
||||
position: absolute;
|
||||
bottom: 60px;
|
||||
height: 50px;
|
||||
line-height: 90px;
|
||||
width: 100%;
|
||||
z-index: 999;
|
||||
background-color: #ffffff;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.dr-task {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,145 @@
|
|||
<template>
|
||||
<el-card body-class="" class="image-card">
|
||||
<div class="image-operation">
|
||||
<div>
|
||||
<el-button type="primary" text bg v-if="imageDetail?.status === 10">生成中</el-button>
|
||||
<el-button text bg v-else-if="imageDetail?.status === 20">已完成</el-button>
|
||||
<el-button type="danger" text bg v-else-if="imageDetail?.status === 30">异常</el-button>
|
||||
</div>
|
||||
<!-- TODO @fan:1)按钮要不调整成详情、下载、再次生成、删除?;2)如果是再次生成,就把当前的参数填写到左侧的框框里? -->
|
||||
<div>
|
||||
<el-button
|
||||
class="btn"
|
||||
text
|
||||
:icon="Download"
|
||||
@click="handlerBtnClick('download', imageDetail)"
|
||||
/>
|
||||
<el-button
|
||||
class="btn"
|
||||
text
|
||||
:icon="Delete"
|
||||
@click="handlerBtnClick('delete', imageDetail)"
|
||||
/>
|
||||
<el-button class="btn" text :icon="More" @click="handlerBtnClick('more', imageDetail)" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="image-wrapper" ref="cardImageRef">
|
||||
<!-- TODO @fan:要不加个点击,大图预览? -->
|
||||
<img class="image" :src="imageDetail?.picUrl" />
|
||||
<div v-if="imageDetail?.status === 30">{{ imageDetail?.errorMessage }}</div>
|
||||
</div>
|
||||
<!-- TODO @fan:style 使用 unocss 替代下 -->
|
||||
<div class="image-mj-btns">
|
||||
<el-button
|
||||
size="small"
|
||||
v-for="button in imageDetail?.buttons"
|
||||
:key="button"
|
||||
style="min-width: 40px; margin-left: 0; margin-right: 10px; margin-top: 5px"
|
||||
@click="handlerMjBtnClick(button)"
|
||||
>
|
||||
{{ button.label }}{{ button.emoji }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { Delete, Download, More } from '@element-plus/icons-vue'
|
||||
import { ImageVO, ImageMjButtonsVO } from '@/api/ai/image'
|
||||
import { PropType } from 'vue'
|
||||
import { ElLoading, ElMessageBox } from 'element-plus'
|
||||
|
||||
const cardImageRef = ref<any>() // 卡片 image ref
|
||||
const cardImageLoadingInstance = ref<any>() // 卡片 image ref
|
||||
const message = useMessage()
|
||||
const props = defineProps({
|
||||
imageDetail: {
|
||||
type: Object as PropType<ImageVO>,
|
||||
require: true
|
||||
}
|
||||
})
|
||||
|
||||
/** 按钮 - 点击事件 */
|
||||
const handlerBtnClick = async (type, imageDetail: ImageVO) => {
|
||||
emits('onBtnClick', type, imageDetail)
|
||||
}
|
||||
|
||||
const handlerLoading = async (status: number) => {
|
||||
// TODO @fan:这个搞成 Loading 组件,然后通过数据驱动,这样搞可以哇?
|
||||
if (status === 10) {
|
||||
cardImageLoadingInstance.value = ElLoading.service({
|
||||
target: cardImageRef.value,
|
||||
text: '生成中...'
|
||||
})
|
||||
} else {
|
||||
if (cardImageLoadingInstance.value) {
|
||||
cardImageLoadingInstance.value.close()
|
||||
cardImageLoadingInstance.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** mj 按钮 click */
|
||||
const handlerMjBtnClick = async (button: ImageMjButtonsVO) => {
|
||||
// 确认窗体
|
||||
await message.confirm(`确认操作 "${button.label} ${button.emoji}" ?`)
|
||||
emits('onMjBtnClick', button, props.imageDetail)
|
||||
}
|
||||
|
||||
// watch
|
||||
const { imageDetail } = toRefs(props)
|
||||
watch(imageDetail, async (newVal, oldVal) => {
|
||||
await handlerLoading(newVal.status as string)
|
||||
})
|
||||
|
||||
// emits
|
||||
const emits = defineEmits(['onBtnClick', 'onMjBtnClick'])
|
||||
|
||||
//
|
||||
onMounted(async () => {
|
||||
await handlerLoading(props.imageDetail.status as string)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.image-card {
|
||||
width: 320px;
|
||||
height: auto;
|
||||
border-radius: 10px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.image-operation {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
|
||||
.btn {
|
||||
//border: 1px solid red;
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.image-wrapper {
|
||||
overflow: hidden;
|
||||
margin-top: 20px;
|
||||
height: 280px;
|
||||
flex: 1;
|
||||
|
||||
.image {
|
||||
width: 100%;
|
||||
border-radius: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.image-mj-btns {
|
||||
margin-top: 5px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,398 @@
|
|||
<!-- dall3 -->
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<el-text tag="b">画面描述</el-text>
|
||||
<el-text tag="p">建议使用“形容词+动词+风格”的格式,使用“,”隔开</el-text>
|
||||
<!-- TODO @fan:style 看看能不能哟 unocss 替代 -->
|
||||
<el-input
|
||||
v-model="prompt"
|
||||
maxlength="1024"
|
||||
rows="5"
|
||||
style="width: 100%; margin-top: 15px;"
|
||||
input-style="border-radius: 7px;"
|
||||
placeholder="例如:童话里的小屋应该是什么样子?"
|
||||
show-word-limit
|
||||
type="textarea"
|
||||
/>
|
||||
</div>
|
||||
<div class="hot-words">
|
||||
<div>
|
||||
<el-text tag="b">随机热词</el-text>
|
||||
</div>
|
||||
<el-space wrap class="word-list">
|
||||
<el-button round
|
||||
class="btn"
|
||||
:type="(selectHotWord === hotWord ? 'primary' : 'default')"
|
||||
v-for="hotWord in hotWords"
|
||||
:key="hotWord"
|
||||
@click="handlerHotWordClick(hotWord)"
|
||||
>
|
||||
{{ hotWord }}
|
||||
</el-button>
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="model">
|
||||
<div>
|
||||
<el-text tag="b">模型选择</el-text>
|
||||
</div>
|
||||
<el-space wrap class="model-list">
|
||||
<div
|
||||
:class="selectModel === model ? 'modal-item selectModel' : 'modal-item'"
|
||||
v-for="model in models"
|
||||
:key="model.key"
|
||||
|
||||
>
|
||||
<el-image
|
||||
:src="model.image"
|
||||
fit="contain"
|
||||
@click="handlerModelClick(model)"
|
||||
/>
|
||||
<div class="model-font">{{model.name}}</div>
|
||||
</div>
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="image-style">
|
||||
<div>
|
||||
<el-text tag="b">风格选择</el-text>
|
||||
</div>
|
||||
<el-space wrap class="image-style-list">
|
||||
<div
|
||||
:class="selectImageStyle === imageStyle ? 'image-style-item selectImageStyle' : 'image-style-item'"
|
||||
v-for="imageStyle in imageStyleList"
|
||||
:key="imageStyle.key"
|
||||
>
|
||||
<el-image
|
||||
:src="imageStyle.image"
|
||||
fit="contain"
|
||||
@click="handlerStyleClick(imageStyle)"
|
||||
/>
|
||||
<div class="style-font">{{imageStyle.name}}</div>
|
||||
</div>
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="image-size">
|
||||
<div>
|
||||
<el-text tag="b">画面比例</el-text>
|
||||
</div>
|
||||
<el-space wrap class="size-list">
|
||||
<div class="size-item"
|
||||
v-for="imageSize in imageSizeList"
|
||||
:key="imageSize.key"
|
||||
@click="handlerSizeClick(imageSize)">
|
||||
<div :class="selectImageSize === imageSize ? 'size-wrapper selectImageSize' : 'size-wrapper'">
|
||||
<div :style="imageSize.style"></div>
|
||||
</div>
|
||||
<div class="size-font">{{ imageSize.name }}</div>
|
||||
</div>
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="btns">
|
||||
<el-button type="primary"
|
||||
size="large"
|
||||
round
|
||||
:loading="drawIn"
|
||||
@click="handlerGenerateImage">
|
||||
{{drawIn ? '生成中' : '生成内容'}}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {ImageApi, ImageDrawReqVO} from '@/api/ai/image';
|
||||
|
||||
// image 模型
|
||||
interface ImageModelVO {
|
||||
key: string
|
||||
name: string
|
||||
image: string
|
||||
}
|
||||
|
||||
// image 大小
|
||||
interface ImageSizeVO {
|
||||
key: string
|
||||
name: string,
|
||||
style: string,
|
||||
width: string,
|
||||
height: string,
|
||||
}
|
||||
|
||||
// 定义属性
|
||||
const prompt = ref<string>('') // 提示词
|
||||
const drawIn = ref<boolean>(false) // 生成中
|
||||
const selectHotWord = ref<string>('') // 选中的热词
|
||||
const hotWords = ref<string[]>(['中国旗袍', '古装美女', '卡通头像', '机甲战士', '童话小屋', '中国长城']) // 热词
|
||||
const selectModel = ref<any>({}) // 模型
|
||||
// message
|
||||
const message = useMessage()
|
||||
// TODO @fan:image 改成项目里自己的哈
|
||||
// TODO @fan:这个 image,要不看看网上有没合适的图片,作为占位符,啊哈哈
|
||||
const models = ref<ImageModelVO[]>([
|
||||
{
|
||||
key: 'dall-e-3',
|
||||
name: 'DALL·E 3',
|
||||
image: 'https://h5.cxyhub.com/images/model_2.png',
|
||||
},
|
||||
{
|
||||
key: 'dall-e-2',
|
||||
name: 'DALL·E 2',
|
||||
image: 'https://h5.cxyhub.com/images/model_1.png',
|
||||
},
|
||||
]) // 模型
|
||||
selectModel.value = models.value[0]
|
||||
|
||||
const selectImageStyle = ref<any>({}) // style 样式
|
||||
// TODO @fan:image 改成项目里自己的哈
|
||||
const imageStyleList = ref<ImageModelVO[]>([
|
||||
{
|
||||
key: 'vivid',
|
||||
name: '清晰',
|
||||
image: 'https://h5.cxyhub.com/images/model_1.png',
|
||||
},
|
||||
{
|
||||
key: 'natural',
|
||||
name: '自然',
|
||||
image: 'https://h5.cxyhub.com/images/model_2.png',
|
||||
},
|
||||
]) // style
|
||||
selectImageStyle.value = imageStyleList.value[0]
|
||||
|
||||
const selectImageSize = ref<ImageSizeVO>({} as ImageSizeVO) // 选中 size
|
||||
const imageSizeList = ref<ImageSizeVO[]>([
|
||||
{
|
||||
key: '1024x1024',
|
||||
name: '1:1',
|
||||
width: '1024',
|
||||
height: '1024',
|
||||
style: 'width: 30px; height: 30px;background-color: #dcdcdc;',
|
||||
},
|
||||
{
|
||||
key: '1024x1792',
|
||||
name: '3:5',
|
||||
width: '1024',
|
||||
height: '1792',
|
||||
style: 'width: 30px; height: 50px;background-color: #dcdcdc;',
|
||||
},
|
||||
{
|
||||
key: '1792x1024',
|
||||
name: '5:3',
|
||||
width: '1792',
|
||||
height: '1024',
|
||||
style: 'width: 50px; height: 30px;background-color: #dcdcdc;',
|
||||
}
|
||||
]) // size
|
||||
selectImageSize.value = imageSizeList.value[0]
|
||||
|
||||
// 定义 Props
|
||||
const props = defineProps({})
|
||||
// 定义 emits
|
||||
const emits = defineEmits(['onDrawStart', 'onDrawComplete'])
|
||||
|
||||
// TODO @fan:如果是简单注释,建议用 /** */,主要是现在项目里是这种风格哈,保持一致好点~
|
||||
// TODO @fan:handler 应该改成 handle 哈
|
||||
/** 热词 - click */
|
||||
const handlerHotWordClick = async (hotWord: string) => {
|
||||
// 取消选中
|
||||
if (selectHotWord.value == hotWord) {
|
||||
selectHotWord.value = ''
|
||||
return
|
||||
}
|
||||
// 选中
|
||||
selectHotWord.value = hotWord
|
||||
// 替换提示词
|
||||
prompt.value = hotWord
|
||||
}
|
||||
|
||||
/** 模型 - click */
|
||||
const handlerModelClick = async (model: ImageModelVO) => {
|
||||
if (selectModel.value === model) {
|
||||
selectModel.value = {} as ImageModelVO
|
||||
return
|
||||
}
|
||||
selectModel.value = model
|
||||
}
|
||||
|
||||
/** 样式 - click */
|
||||
const handlerStyleClick = async (imageStyle: ImageModelVO) => {
|
||||
if (selectImageStyle.value === imageStyle) {
|
||||
selectImageStyle.value = {} as ImageModelVO
|
||||
return
|
||||
}
|
||||
selectImageStyle.value = imageStyle
|
||||
}
|
||||
|
||||
/** size - click */
|
||||
const handlerSizeClick = async (imageSize: ImageSizeVO) => {
|
||||
if (selectImageSize.value === imageSize) {
|
||||
selectImageSize.value = {} as ImageSizeVO
|
||||
return
|
||||
}
|
||||
selectImageSize.value = imageSize
|
||||
}
|
||||
|
||||
/** 图片生产 */
|
||||
const handlerGenerateImage = async () => {
|
||||
// 二次确认
|
||||
await message.confirm(`确认生成内容?`)
|
||||
try {
|
||||
// 加载中
|
||||
drawIn.value = true
|
||||
// 回调
|
||||
emits('onDrawStart', selectModel.value.key)
|
||||
const form = {
|
||||
platform: 'OpenAI',
|
||||
prompt: prompt.value, // 提示词
|
||||
model: selectModel.value.key, // 模型
|
||||
width: selectImageSize.value.width, // size 不能为空
|
||||
height: selectImageSize.value.height, // size 不能为空
|
||||
options: {
|
||||
style: selectImageStyle.value.key, // 图像生成的风格
|
||||
}
|
||||
} as ImageDrawReqVO
|
||||
// 发送请求
|
||||
await ImageApi.drawImage(form)
|
||||
} finally {
|
||||
// 回调
|
||||
emits('onDrawComplete', selectModel.value.key)
|
||||
// 加载结束
|
||||
drawIn.value = false
|
||||
}
|
||||
}
|
||||
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
|
||||
// 提示词
|
||||
.prompt {
|
||||
}
|
||||
|
||||
// 热词
|
||||
.hot-words {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 30px;
|
||||
|
||||
.word-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: start;
|
||||
margin-top: 15px;
|
||||
|
||||
.btn {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 模型
|
||||
.model {
|
||||
margin-top: 30px;
|
||||
|
||||
.model-list {
|
||||
margin-top: 15px;
|
||||
|
||||
.modal-item {
|
||||
width: 110px;
|
||||
//outline: 1px solid blue;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
border: 3px solid transparent;
|
||||
cursor: pointer;
|
||||
|
||||
.model-font {
|
||||
font-size: 14px;
|
||||
color: #3e3e3e;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.selectModel {
|
||||
border: 3px solid #1293ff;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 样式 style
|
||||
.image-style {
|
||||
margin-top: 30px;
|
||||
|
||||
.image-style-list {
|
||||
margin-top: 15px;
|
||||
|
||||
.image-style-item {
|
||||
width: 110px;
|
||||
//outline: 1px solid blue;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
border: 3px solid transparent;
|
||||
cursor: pointer;
|
||||
|
||||
.style-font {
|
||||
font-size: 14px;
|
||||
color: #3e3e3e;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.selectImageStyle {
|
||||
border: 3px solid #1293ff;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 尺寸
|
||||
.image-size {
|
||||
width: 100%;
|
||||
margin-top: 30px;
|
||||
|
||||
.size-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
|
||||
.size-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
.size-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 7px;
|
||||
padding: 4px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #fff;
|
||||
}
|
||||
|
||||
.size-font {
|
||||
font-size: 14px;
|
||||
color: #3e3e3e;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.selectImageSize {
|
||||
border: 1px solid #1293ff !important;
|
||||
}
|
||||
}
|
||||
|
||||
.btns {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 50px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,105 @@
|
|||
<!-- image -->
|
||||
<template>
|
||||
<div class="ai-image">
|
||||
<div class="left">
|
||||
<div class="segmented">
|
||||
<el-segmented v-model="selectModel" :options="modelOptions" />
|
||||
</div>
|
||||
<div class="modal-switch-container">
|
||||
<!-- TODO @fan:1)建议 Dall3 改成 OpenAI 绘图。因为 dall3 其实本质是模型;2)涉及到中英文的地方,中文和英文之间,有个空格哈 -->
|
||||
<Dall3 v-if="selectModel === 'DALL3绘画'"
|
||||
@on-draw-start="handlerDrawStart"
|
||||
@on-draw-complete="handlerDrawComplete" />
|
||||
<Midjourney v-if="selectModel === 'MJ绘画'" />
|
||||
<StableDiffusion v-if="selectModel === 'Stable Diffusion'" @on-draw-complete="handlerDrawComplete" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="main">
|
||||
<ImageTask ref="imageTaskRef" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// TODO @fan:在整个挪到 /views/ai/image/index 目录。因为我想在 /views/ai/image/manager 做管理的功能,进行下区分!
|
||||
import Dall3 from './dall3/index.vue'
|
||||
import Midjourney from './midjourney/index.vue'
|
||||
import StableDiffusion from './stable-diffusion/index.vue'
|
||||
import ImageTask from './ImageTask.vue'
|
||||
|
||||
// ref
|
||||
const imageTaskRef = ref<any>() // image task ref
|
||||
|
||||
// 定义属性
|
||||
const selectModel = ref('Stable Diffusion')
|
||||
const modelOptions = ['DALL3绘画', 'MJ绘画', 'Stable Diffusion']
|
||||
const drawIn = ref<boolean>(false) // 生成中
|
||||
|
||||
/** 绘画 - start */
|
||||
const handlerDrawStart = async (type) => {
|
||||
// todo
|
||||
drawIn.value = true
|
||||
}
|
||||
|
||||
/** 绘画 - complete */
|
||||
const handlerDrawComplete = async (type) => {
|
||||
drawIn.value = false
|
||||
// todo
|
||||
await imageTaskRef.value.getImageList()
|
||||
}
|
||||
|
||||
//
|
||||
onMounted( async () => {
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
.ai-image {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
top: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
|
||||
.left {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 20px;
|
||||
width: 350px;
|
||||
|
||||
.segmented {
|
||||
}
|
||||
|
||||
.segmented .el-segmented {
|
||||
--el-border-radius-base: 16px;
|
||||
--el-segmented-item-selected-color: #fff;
|
||||
background-color: #ececec;
|
||||
width: 350px;
|
||||
}
|
||||
|
||||
.modal-switch-container {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
margin-top: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.main {
|
||||
flex: 1;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.right {
|
||||
width: 350px;
|
||||
background-color: #f7f8fa;
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
|
@ -0,0 +1,250 @@
|
|||
<template>
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form-item label="用户编号" prop="userId">
|
||||
<el-select
|
||||
v-model="queryParams.userId"
|
||||
clearable
|
||||
placeholder="请输入用户编号"
|
||||
class="!w-240px"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in userList"
|
||||
:key="item.id"
|
||||
:label="item.nickname"
|
||||
:value="item.id"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="平台" prop="platform">
|
||||
<el-select v-model="queryParams.status" placeholder="请选择平台" clearable class="!w-240px">
|
||||
<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="绘画状态" prop="status">
|
||||
<el-select
|
||||
v-model="queryParams.status"
|
||||
placeholder="请选择绘画状态"
|
||||
clearable
|
||||
class="!w-240px"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.AI_IMAGE_STATUS)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</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 label="创建时间" prop="createTime">
|
||||
<el-date-picker
|
||||
v-model="queryParams.createTime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="daterange"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
|
||||
class="!w-220px"
|
||||
/>
|
||||
</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-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="id" width="180" fixed="left" />
|
||||
<el-table-column label="图片" align="center" prop="picUrl" width="110px" fixed="left">
|
||||
<template #default="{ row }">
|
||||
<el-image
|
||||
class="h-80px w-80px"
|
||||
lazy
|
||||
:src="row.picUrl"
|
||||
:preview-src-list="[row.picUrl]"
|
||||
preview-teleported
|
||||
fit="cover"
|
||||
v-if="row.picUrl?.length > 0"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="用户" align="center" prop="userId" width="180">
|
||||
<template #default="scope">
|
||||
<span>{{ userList.find((item) => item.id === scope.row.userId)?.nickname }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="平台" align="center" prop="platform" width="120">
|
||||
<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="model" width="180" />
|
||||
<el-table-column label="绘画状态" align="center" prop="status" width="100">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.AI_IMAGE_STATUS" :value="scope.row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="是否发布" align="center" prop="publicStatus">
|
||||
<template #default="scope">
|
||||
<el-switch
|
||||
v-model="scope.row.publicStatus"
|
||||
:active-value="true"
|
||||
:inactive-value="false"
|
||||
@change="handleUpdatePublicStatusChange(scope.row)"
|
||||
:disabled="scope.row.status !== 20"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="提示词" align="center" prop="prompt" width="180" />
|
||||
<el-table-column
|
||||
label="创建时间"
|
||||
align="center"
|
||||
prop="createTime"
|
||||
:formatter="dateFormatter"
|
||||
width="180px"
|
||||
/>
|
||||
<el-table-column label="宽度" align="center" prop="width" />
|
||||
<el-table-column label="高度" align="center" prop="height" />
|
||||
<el-table-column label="错误信息" align="center" prop="errorMessage" />
|
||||
<el-table-column label="任务编号" align="center" prop="taskId" />
|
||||
<el-table-column label="操作" align="center" width="100" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row.id)"
|
||||
v-hasPermi="['ai:image: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>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getIntDictOptions, DICT_TYPE, getStrDictOptions, getBoolDictOptions } from '@/utils/dict'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import { ImageApi, ImageVO } from '@/api/ai/image'
|
||||
import * as UserApi from '@/api/system/user'
|
||||
|
||||
/** AI 绘画 列表 */
|
||||
defineOptions({ name: 'AiImageManager' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<ImageVO[]>([]) // 列表的数据
|
||||
const total = ref(0) // 列表的总页数
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
userId: undefined,
|
||||
platform: undefined,
|
||||
status: undefined,
|
||||
publicStatus: undefined,
|
||||
createTime: []
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
const userList = ref<UserApi.UserVO[]>([]) // 用户列表
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await ImageApi.getImagePage(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 handleDelete = async (id: number) => {
|
||||
try {
|
||||
// 删除的二次确认
|
||||
await message.delConfirm()
|
||||
// 发起删除
|
||||
await ImageApi.deleteImage(id)
|
||||
message.success(t('common.delSuccess'))
|
||||
// 刷新列表
|
||||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 修改是否发布 */
|
||||
const handleUpdatePublicStatusChange = async (row: ImageVO) => {
|
||||
try {
|
||||
// 修改状态的二次确认
|
||||
const text = row.publicStatus ? '公开' : '私有'
|
||||
await message.confirm('确认要"' + text + '"该图片吗?')
|
||||
// 发起修改状态
|
||||
await ImageApi.updateImage({
|
||||
id: row.id,
|
||||
publicStatus: row.publicStatus
|
||||
})
|
||||
await getList()
|
||||
} catch {
|
||||
row.publicStatus = !row.publicStatus
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(async () => {
|
||||
getList()
|
||||
// 获得用户列表
|
||||
userList.value = await UserApi.getSimpleUserList()
|
||||
})
|
||||
</script>
|
|
@ -0,0 +1,384 @@
|
|||
<!-- dall3 -->
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<el-text tag="b">画面描述</el-text>
|
||||
<el-text tag="p">建议使用“形容词+动词+风格”的格式,使用“,”隔开.</el-text>
|
||||
<el-input
|
||||
v-model="prompt"
|
||||
maxlength="1024"
|
||||
rows="5"
|
||||
style="width: 100%; margin-top: 15px;"
|
||||
input-style="border-radius: 7px;"
|
||||
placeholder="例如:童话里的小屋应该是什么样子?"
|
||||
show-word-limit
|
||||
type="textarea"
|
||||
/>
|
||||
</div>
|
||||
<div class="hot-words">
|
||||
<div>
|
||||
<el-text tag="b">随机热词</el-text>
|
||||
</div>
|
||||
<el-space wrap class="word-list">
|
||||
<el-button round
|
||||
class="btn"
|
||||
:type="(selectHotWord === hotWord ? 'primary' : 'default')"
|
||||
v-for="hotWord in hotWords"
|
||||
:key="hotWord"
|
||||
@click="handlerHotWordClick(hotWord)"
|
||||
>
|
||||
{{ hotWord }}
|
||||
</el-button>
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="image-size">
|
||||
<div>
|
||||
<el-text tag="b">尺寸</el-text>
|
||||
</div>
|
||||
<el-space wrap class="size-list">
|
||||
<div class="size-item"
|
||||
v-for="imageSize in imageSizeList"
|
||||
:key="imageSize.key"
|
||||
@click="handlerSizeClick(imageSize)">
|
||||
<div :class="selectImageSize === imageSize ? 'size-wrapper selectImageSize' : 'size-wrapper'">
|
||||
<div :style="imageSize.style"></div>
|
||||
</div>
|
||||
<div class="size-font">{{ imageSize.key }}</div>
|
||||
</div>
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="version">
|
||||
<div>
|
||||
<el-text tag="b">版本</el-text>
|
||||
</div>
|
||||
<el-space wrap class="version-list">
|
||||
<el-select
|
||||
v-model="selectVersion"
|
||||
class="version-select"
|
||||
clearable
|
||||
placeholder="请选择版本"
|
||||
style="width: 350px"
|
||||
@change="handlerChangeVersion"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in versionList"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="model">
|
||||
<div>
|
||||
<el-text tag="b">模型</el-text>
|
||||
</div>
|
||||
<el-space wrap class="model-list">
|
||||
<div
|
||||
:class="selectModel === model ? 'modal-item selectModel' : 'modal-item'"
|
||||
v-for="model in models"
|
||||
:key="model.key"
|
||||
|
||||
>
|
||||
<el-image
|
||||
:src="model.image"
|
||||
fit="contain"
|
||||
@click="handlerModelClick(model)"
|
||||
/>
|
||||
<div class="model-font">{{model.name}}</div>
|
||||
</div>
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="btns">
|
||||
<!-- <el-button size="large" round>重置内容</el-button>-->
|
||||
<el-button type="primary" size="large" round @click="handlerGenerateImage">生成内容</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
|
||||
// image 模型
|
||||
import {ImageApi, ImageMidjourneyImagineReqVO} from "@/api/ai/image";
|
||||
// message
|
||||
const message = useMessage()
|
||||
// 定义 emits
|
||||
const emits = defineEmits(['onDrawStart', 'onDrawComplete'])
|
||||
|
||||
interface ImageModelVO {
|
||||
key: string
|
||||
name: string
|
||||
image: string
|
||||
}
|
||||
|
||||
// image 大小
|
||||
interface ImageSizeVO {
|
||||
key: string
|
||||
style: string,
|
||||
width: string,
|
||||
height: string,
|
||||
}
|
||||
|
||||
// 定义属性
|
||||
const prompt = ref<string>('') // 提示词
|
||||
const selectHotWord = ref<string>('') // 选中的热词
|
||||
const hotWords = ref<string[]>(['中国旗袍', '古装美女', '卡通头像', '机甲战士', '童话小屋', '中国长城']) // 热词
|
||||
const selectModel = ref<any>() // 选中的热词
|
||||
const models = ref<ImageModelVO[]>([
|
||||
{
|
||||
key: 'midjourney',
|
||||
name: 'MJ',
|
||||
image: 'https://bigpt8.com/pc/_nuxt/mj.34a61377.png',
|
||||
},
|
||||
{
|
||||
key: 'niji',
|
||||
name: 'NIJI',
|
||||
image: 'https://bigpt8.com/pc/_nuxt/nj.ca79b143.png',
|
||||
},
|
||||
]) // 模型
|
||||
selectModel.value = models.value[0] // 默认选中
|
||||
|
||||
const selectImageSize = ref<ImageSizeVO>({} as ImageSizeVO) // 选中 size
|
||||
const imageSizeList = ref<ImageSizeVO[]>([
|
||||
{
|
||||
key: '1:1',
|
||||
width: "1",
|
||||
height: "1",
|
||||
style: 'width: 30px; height: 30px;background-color: #dcdcdc;',
|
||||
},
|
||||
{
|
||||
key: '3:4',
|
||||
width: "3",
|
||||
height: "4",
|
||||
style: 'width: 30px; height: 40px;background-color: #dcdcdc;',
|
||||
},
|
||||
{
|
||||
key: '4:3',
|
||||
width: "4",
|
||||
height: "3",
|
||||
style: 'width: 40px; height: 30px;background-color: #dcdcdc;',
|
||||
},
|
||||
{
|
||||
key: '9:16',
|
||||
width: "9",
|
||||
height: "16",
|
||||
style: 'width: 30px; height: 50px;background-color: #dcdcdc;',
|
||||
},
|
||||
{
|
||||
key: '16:9',
|
||||
width: "16",
|
||||
height: "9",
|
||||
style: 'width: 50px; height: 30px;background-color: #dcdcdc;',
|
||||
},
|
||||
]) // size
|
||||
selectImageSize.value = imageSizeList.value[0]
|
||||
|
||||
// version
|
||||
let versionList = ref<any>([]) // version 列表
|
||||
const midjourneyVersionList = ref<any>([
|
||||
{
|
||||
value: '6.0',
|
||||
label: 'v6.0',
|
||||
},
|
||||
{
|
||||
value: '5.2',
|
||||
label: 'v5.2',
|
||||
},
|
||||
{
|
||||
value: '5.1',
|
||||
label: 'v5.1',
|
||||
},
|
||||
{
|
||||
value: '5.0',
|
||||
label: 'v5.0',
|
||||
},
|
||||
{
|
||||
value: '4.0',
|
||||
label: 'v4.0',
|
||||
},
|
||||
])
|
||||
const nijiVersionList = ref<any>([
|
||||
{
|
||||
value: '5',
|
||||
label: 'v5',
|
||||
},
|
||||
])
|
||||
const selectVersion = ref<any>('6.0') // 选中的 version
|
||||
versionList.value = midjourneyVersionList.value // 默认选择 midjourney
|
||||
|
||||
/** 热词 - click */
|
||||
const handlerHotWordClick = async (hotWord: string) => {
|
||||
// 取消
|
||||
if (selectHotWord.value == hotWord) {
|
||||
selectHotWord.value = ''
|
||||
return
|
||||
}
|
||||
// 选中
|
||||
selectHotWord.value = hotWord
|
||||
// 设置提示次
|
||||
prompt.value = hotWord
|
||||
}
|
||||
|
||||
/** size - click */
|
||||
const handlerSizeClick = async (imageSize: ImageSizeVO) => {
|
||||
if (selectImageSize.value === imageSize) {
|
||||
selectImageSize.value = {} as ImageSizeVO
|
||||
return
|
||||
}
|
||||
selectImageSize.value = imageSize
|
||||
}
|
||||
|
||||
/** 模型 - click */
|
||||
const handlerModelClick = async (model: ImageModelVO) => {
|
||||
selectModel.value = model
|
||||
if (model.key === 'niji') {
|
||||
versionList.value = nijiVersionList.value // 默认选择 niji
|
||||
} else {
|
||||
versionList.value = midjourneyVersionList.value // 默认选择 midjourney
|
||||
}
|
||||
selectVersion.value = versionList.value[0].value
|
||||
}
|
||||
|
||||
/** version - click */
|
||||
const handlerChangeVersion = async (version) => {
|
||||
console.log('version', version)
|
||||
}
|
||||
|
||||
/** 图片生产 */
|
||||
const handlerGenerateImage = async () => {
|
||||
// 二次确认
|
||||
await message.confirm(`确认生成内容?`)
|
||||
// todo @范 图片生产逻辑
|
||||
try {
|
||||
// 回调
|
||||
emits('onDrawStart', selectModel.value.key)
|
||||
// 发送请求
|
||||
const req = {
|
||||
prompt: prompt.value,
|
||||
model: selectModel.value.key,
|
||||
width: selectImageSize.value.width,
|
||||
height: selectImageSize.value.height,
|
||||
version: selectVersion.value,
|
||||
base64Array: [],
|
||||
} as ImageMidjourneyImagineReqVO
|
||||
await ImageApi.midjourneyImagine(req)
|
||||
} finally {
|
||||
// 回调
|
||||
emits('onDrawComplete', selectModel.value.key)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
|
||||
// 提示词
|
||||
.prompt {
|
||||
}
|
||||
|
||||
// 热词
|
||||
.hot-words {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 30px;
|
||||
|
||||
.word-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: start;
|
||||
margin-top: 15px;
|
||||
|
||||
.btn {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// version
|
||||
.version {
|
||||
margin-top: 20px;
|
||||
|
||||
.version-list {
|
||||
margin-top: 20px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
// 模型
|
||||
.model {
|
||||
margin-top: 30px;
|
||||
|
||||
.model-list {
|
||||
margin-top: 15px;
|
||||
|
||||
.modal-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: 150px;
|
||||
//outline: 1px solid blue;
|
||||
overflow: hidden;
|
||||
border: 3px solid transparent;
|
||||
cursor: pointer;
|
||||
|
||||
.model-font {
|
||||
font-size: 14px;
|
||||
color: #3e3e3e;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
|
||||
.selectModel {
|
||||
border: 3px solid #1293ff;
|
||||
border-radius: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 尺寸
|
||||
.image-size {
|
||||
width: 100%;
|
||||
margin-top: 30px;
|
||||
|
||||
.size-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
|
||||
.size-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
.size-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 7px;
|
||||
padding: 4px;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
background-color: #fff;
|
||||
border: 1px solid #fff;
|
||||
}
|
||||
|
||||
.size-font {
|
||||
font-size: 14px;
|
||||
color: #3e3e3e;
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.selectImageSize {
|
||||
border: 1px solid #1293ff !important;
|
||||
}
|
||||
}
|
||||
|
||||
.btns {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 50px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,405 @@
|
|||
<!-- dall3 -->
|
||||
<template>
|
||||
<div class="prompt">
|
||||
<el-text tag="b">画面描述</el-text>
|
||||
<el-text tag="p">建议使用“形容词+动词+风格”的格式,使用“,”隔开</el-text>
|
||||
<!-- TODO @fan:style 看看能不能哟 unocss 替代 -->
|
||||
<el-input
|
||||
v-model="prompt"
|
||||
maxlength="1024"
|
||||
rows="5"
|
||||
style="width: 100%; margin-top: 15px"
|
||||
input-style="border-radius: 7px;"
|
||||
placeholder="例如:童话里的小屋应该是什么样子?"
|
||||
show-word-limit
|
||||
type="textarea"
|
||||
/>
|
||||
</div>
|
||||
<div class="hot-words">
|
||||
<div>
|
||||
<el-text tag="b">随机热词</el-text>
|
||||
</div>
|
||||
<el-space wrap class="word-list">
|
||||
<el-button
|
||||
round
|
||||
class="btn"
|
||||
:type="selectHotWord === hotWord ? 'primary' : 'default'"
|
||||
v-for="hotWord in hotWords"
|
||||
:key="hotWord"
|
||||
@click="handleHotWordClick(hotWord)"
|
||||
>
|
||||
{{ hotWord }}
|
||||
</el-button>
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="group-item">
|
||||
<div>
|
||||
<el-text tag="b">采样方法</el-text>
|
||||
</div>
|
||||
<el-space wrap class="group-item-body">
|
||||
<el-select v-model="selectSampler" placeholder="Select" size="large" style="width: 350px">
|
||||
<el-option v-for="item in sampler" :key="item.key" :label="item.name" :value="item.key" />
|
||||
</el-select>
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="group-item">
|
||||
<div>
|
||||
<el-text tag="b">CLIP</el-text>
|
||||
</div>
|
||||
<el-space wrap class="group-item-body">
|
||||
<el-select v-model="selectClipGuidancePreset" placeholder="Select" size="large" style="width: 350px">
|
||||
<el-option v-for="item in clipGuidancePresets" :key="item.key" :label="item.name" :value="item.key" />
|
||||
</el-select>
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="group-item">
|
||||
<div>
|
||||
<el-text tag="b">风格</el-text>
|
||||
</div>
|
||||
<el-space wrap class="group-item-body">
|
||||
<el-select v-model="selectStylePreset" placeholder="Select" size="large" style="width: 350px">
|
||||
<el-option v-for="item in stylePresets" :key="item.key" :label="item.name" :value="item.key" />
|
||||
</el-select>
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="group-item">
|
||||
<div>
|
||||
<el-text tag="b">图片尺寸</el-text>
|
||||
</div>
|
||||
<el-space wrap class="group-item-body">
|
||||
<el-input v-model="imageWidth" style="width: 170px" placeholder="图片宽度" />
|
||||
<el-input v-model="imageHeight" style="width: 170px" placeholder="图片高度" />
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="group-item">
|
||||
<div>
|
||||
<el-text tag="b">迭代步数</el-text>
|
||||
</div>
|
||||
<el-space wrap class="group-item-body">
|
||||
<el-input
|
||||
v-model="steps"
|
||||
type="number"
|
||||
size="large"
|
||||
style="width: 350px"
|
||||
placeholder="Please input"
|
||||
/>
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="group-item">
|
||||
<div>
|
||||
<el-text tag="b">引导系数</el-text>
|
||||
</div>
|
||||
<el-space wrap class="group-item-body">
|
||||
<el-input
|
||||
v-model="scale"
|
||||
type="number"
|
||||
size="large"
|
||||
style="width: 350px"
|
||||
placeholder="Please input"
|
||||
/>
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="group-item">
|
||||
<div>
|
||||
<el-text tag="b">随机因子</el-text>
|
||||
</div>
|
||||
<el-space wrap class="group-item-body">
|
||||
<el-input
|
||||
v-model="seed"
|
||||
type="number"
|
||||
size="large"
|
||||
style="width: 350px"
|
||||
placeholder="Please input"
|
||||
/>
|
||||
</el-space>
|
||||
</div>
|
||||
<div class="btns">
|
||||
<el-button type="primary" size="large" round :loading="drawIn" @click="handleGenerateImage">
|
||||
{{ drawIn ? '生成中' : '生成内容' }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import {ImageApi, ImageDrawReqVO} from '@/api/ai/image'
|
||||
|
||||
// image 模型
|
||||
interface ImageModelVO {
|
||||
key: string
|
||||
name: string
|
||||
}
|
||||
|
||||
// 定义属性
|
||||
const prompt = ref<string>('') // 提示词
|
||||
const drawIn = ref<boolean>(false) // 生成中
|
||||
const selectHotWord = ref<string>('') // 选中的热词
|
||||
const imageWidth = ref<number>(512) // 图片宽度
|
||||
const imageHeight = ref<number>(512) // 图片高度
|
||||
|
||||
const hotWords = ref<string[]>([
|
||||
'中国旗袍',
|
||||
'古装美女',
|
||||
'卡通头像',
|
||||
'机甲战士',
|
||||
'童话小屋',
|
||||
'中国长城'
|
||||
]) // 热词
|
||||
// message
|
||||
const message = useMessage()
|
||||
|
||||
// 采样方法 TODO @fan:有 Euler a;DPM++ 2S a;DPM++ 2M;DPM++ SDE;DPM++ 2M SDE;UniPC;Restart;另外,要不这种枚举,我们都放到 image 里?写成 stableDiffusionSampler ?
|
||||
const selectSampler = ref<any>({}) // 模型
|
||||
// DDIM DDPM K_DPMPP_2M K_DPMPP_2S_ANCESTRAL K_DPM_2 K_DPM_2_ANCESTRAL K_EULER K_EULER_ANCESTRAL K_HEUN K_LMS
|
||||
const sampler = ref<ImageModelVO[]>([
|
||||
{
|
||||
key: 'DDIM',
|
||||
name: 'DDIM'
|
||||
},
|
||||
{
|
||||
key: 'DDPM',
|
||||
name: 'DDPM'
|
||||
},
|
||||
{
|
||||
key: 'K_DPMPP_2M',
|
||||
name: 'K_DPMPP_2M'
|
||||
},
|
||||
{
|
||||
key: 'K_DPMPP_2S_ANCESTRAL',
|
||||
name: 'K_DPMPP_2S_ANCESTRAL'
|
||||
},
|
||||
{
|
||||
key: 'K_DPM_2',
|
||||
name: 'K_DPM_2'
|
||||
},
|
||||
{
|
||||
key: 'K_DPM_2_ANCESTRAL',
|
||||
name: 'K_DPM_2_ANCESTRAL'
|
||||
},
|
||||
{
|
||||
key: 'K_EULER',
|
||||
name: 'K_EULER'
|
||||
},
|
||||
{
|
||||
key: 'K_EULER_ANCESTRAL',
|
||||
name: 'K_EULER_ANCESTRAL'
|
||||
},
|
||||
{
|
||||
key: 'K_HEUN',
|
||||
name: 'K_HEUN'
|
||||
},
|
||||
{
|
||||
key: 'K_LMS',
|
||||
name: 'K_LMS'
|
||||
},
|
||||
])
|
||||
selectSampler.value = sampler.value[0]
|
||||
|
||||
// 风格
|
||||
// 3d-model analog-film anime cinematic comic-book digital-art enhance fantasy-art isometric
|
||||
// line-art low-poly modeling-compound neon-punk origami photographic pixel-art tile-texture
|
||||
const selectStylePreset = ref<any>({}) // 模型
|
||||
const stylePresets = ref<ImageModelVO[]>([
|
||||
{
|
||||
key: '3d-model',
|
||||
name: '3d-model'
|
||||
},
|
||||
{
|
||||
key: 'analog-film',
|
||||
name: 'analog-film'
|
||||
},
|
||||
{
|
||||
key: 'anime',
|
||||
name: 'anime'
|
||||
},
|
||||
{
|
||||
key: 'cinematic',
|
||||
name: 'cinematic'
|
||||
},
|
||||
{
|
||||
key: 'comic-book',
|
||||
name: 'comic-book'
|
||||
},
|
||||
{
|
||||
key: 'digital-art',
|
||||
name: 'digital-art'
|
||||
},
|
||||
{
|
||||
key: 'enhance',
|
||||
name: 'enhance'
|
||||
},
|
||||
{
|
||||
key: 'fantasy-art',
|
||||
name: 'fantasy-art'
|
||||
},
|
||||
{
|
||||
key: 'isometric',
|
||||
name: 'isometric'
|
||||
},
|
||||
{
|
||||
key: 'line-art',
|
||||
name: 'line-art'
|
||||
},
|
||||
{
|
||||
key: 'low-poly',
|
||||
name: 'low-poly'
|
||||
},
|
||||
{
|
||||
key: 'modeling-compound',
|
||||
name: 'modeling-compound'
|
||||
},
|
||||
// neon-punk origami photographic pixel-art tile-texture
|
||||
{
|
||||
key: 'neon-punk',
|
||||
name: 'neon-punk'
|
||||
},
|
||||
{
|
||||
key: 'origami',
|
||||
name: 'origami'
|
||||
},
|
||||
{
|
||||
key: 'photographic',
|
||||
name: 'photographic'
|
||||
},
|
||||
{
|
||||
key: 'pixel-art',
|
||||
name: 'pixel-art'
|
||||
},
|
||||
{
|
||||
key: 'tile-texture',
|
||||
name: 'tile-texture'
|
||||
},
|
||||
])
|
||||
selectStylePreset.value = stylePresets.value[0]
|
||||
|
||||
|
||||
// 文本提示相匹配的图像(clip_guidance_preset) 简称 CLIP
|
||||
// https://platform.stability.ai/docs/api-reference#tag/SDXL-and-SD1.6/operation/textToImage
|
||||
// FAST_BLUE FAST_GREEN NONE SIMPLE SLOW SLOWER SLOWEST
|
||||
const selectClipGuidancePreset = ref<any>({}) // 模型
|
||||
const clipGuidancePresets = ref<ImageModelVO[]>([
|
||||
{
|
||||
key: 'NONE',
|
||||
name: 'NONE'
|
||||
},
|
||||
{
|
||||
key: 'FAST_BLUE',
|
||||
name: 'FAST_BLUE'
|
||||
},
|
||||
{
|
||||
key: 'FAST_GREEN',
|
||||
name: 'FAST_GREEN'
|
||||
},
|
||||
{
|
||||
key: 'SIMPLE',
|
||||
name: 'SIMPLE'
|
||||
},
|
||||
{
|
||||
key: 'SLOW',
|
||||
name: 'SLOW'
|
||||
},
|
||||
{
|
||||
key: 'SLOWER',
|
||||
name: 'SLOWER'
|
||||
},
|
||||
{
|
||||
key: 'SLOWEST',
|
||||
name: 'SLOWEST'
|
||||
},
|
||||
])
|
||||
selectClipGuidancePreset.value = clipGuidancePresets.value[0]
|
||||
|
||||
const steps = ref<number>(20) // 迭代步数
|
||||
const seed = ref<number>(42) // 控制生成图像的随机性
|
||||
const scale = ref<number>(7.5) // 引导系数
|
||||
|
||||
// 定义 Props
|
||||
const props = defineProps({})
|
||||
// 定义 emits
|
||||
const emits = defineEmits(['onDrawStart', 'onDrawComplete'])
|
||||
|
||||
/** 热词 - click */
|
||||
const handleHotWordClick = async (hotWord: string) => {
|
||||
// 取消选中
|
||||
if (selectHotWord.value == hotWord) {
|
||||
selectHotWord.value = ''
|
||||
return
|
||||
}
|
||||
// 选中
|
||||
selectHotWord.value = hotWord
|
||||
// 替换提示词
|
||||
prompt.value = hotWord
|
||||
}
|
||||
|
||||
/** 图片生产 */
|
||||
const handleGenerateImage = async () => {
|
||||
// 二次确认
|
||||
await message.confirm(`确认生成内容?`)
|
||||
try {
|
||||
// 加载中
|
||||
drawIn.value = true
|
||||
// 回调
|
||||
emits('onDrawStart', 'StableDiffusion')
|
||||
// 发送请求
|
||||
const form = {
|
||||
platform: 'StableDiffusion',
|
||||
model: 'stable-diffusion-v1-6',
|
||||
prompt: prompt.value, // 提示词
|
||||
width: imageWidth.value, // 图片宽度
|
||||
height: imageHeight.value, // 图片高度
|
||||
options: {
|
||||
seed: seed.value, // 随机种子
|
||||
steps: steps.value, // 图片生成步数
|
||||
scale: scale.value, // 引导系数
|
||||
sampler: selectSampler.value.key, // 采样算法
|
||||
clipGuidancePreset: selectClipGuidancePreset.value.key, // 文本提示相匹配的图像 CLIP
|
||||
stylePreset: selectStylePreset.value.key, // 风格
|
||||
}
|
||||
} as ImageDrawReqVO
|
||||
await ImageApi.drawImage(form)
|
||||
} finally {
|
||||
// 回调
|
||||
emits('onDrawComplete', 'StableDiffusion')
|
||||
// 加载结束
|
||||
drawIn.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
// 提示词
|
||||
.prompt {
|
||||
}
|
||||
|
||||
// 热词
|
||||
.hot-words {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 30px;
|
||||
|
||||
.word-list {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: start;
|
||||
margin-top: 15px;
|
||||
|
||||
.btn {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 模型
|
||||
.group-item {
|
||||
margin-top: 30px;
|
||||
|
||||
.group-item-body {
|
||||
margin-top: 15px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.btns {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-top: 50px;
|
||||
}
|
||||
</style>
|
|
@ -48,13 +48,29 @@
|
|||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item label="温度参数" prop="temperature">
|
||||
<el-input v-model="formData.temperature" placeholder="请输入温度参数" />
|
||||
<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 v-model="formData.maxTokens" placeholder="请输入回复数 Token 数" />
|
||||
<el-input-number
|
||||
v-model="formData.maxTokens"
|
||||
placeholder="请输入回复数 Token 数"
|
||||
:min="0"
|
||||
:max="4096"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="上下文数量" prop="maxContexts">
|
||||
<el-input v-model="formData.maxContexts" placeholder="请输入上下文数量" />
|
||||
<el-input-number
|
||||
v-model="formData.maxContexts"
|
||||
placeholder="请输入上下文数量"
|
||||
:min="0"
|
||||
:max="20"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
|
|
|
@ -8,10 +8,10 @@
|
|||
v-loading="formLoading"
|
||||
>
|
||||
<el-form-item label="角色名称" prop="name">
|
||||
<el-input v-model="formData.name" placeholder="请输入角色名称"/>
|
||||
<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"/>
|
||||
<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>
|
||||
|
@ -24,17 +24,14 @@
|
|||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色类别" prop="category" v-if="!isUser(formType)">
|
||||
<el-input v-model="formData.category" placeholder="请输入角色类别"/>
|
||||
<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-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
|
||||
|
@ -47,7 +44,7 @@
|
|||
</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-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">
|
||||
|
@ -68,15 +65,15 @@
|
|||
</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'
|
||||
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'})
|
||||
defineOptions({ name: 'ChatRoleForm' })
|
||||
|
||||
const {t} = useI18n() // 国际化
|
||||
const { t } = useI18n() // 国际化
|
||||
const message = useMessage() // 消息弹窗
|
||||
|
||||
const dialogVisible = ref(false) // 弹窗的是否展示
|
||||
|
@ -92,44 +89,36 @@ const formData = ref({
|
|||
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[]) // 聊天模型列表
|
||||
|
||||
/** 是否【我】自己创建,私有角色 */
|
||||
// TODO @fan:建议改成计算函数 computed
|
||||
const isUser = (type: string) => {
|
||||
return type === 'my-create' || type === 'my-update'
|
||||
}
|
||||
|
||||
// TODO @fan:直接使用 formRules;只要隐藏掉的字段,它是不会校验的哈;
|
||||
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'
|
||||
}]
|
||||
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' }],
|
||||
publicStatus: [{ required: true, message: '是否公开不能为空', trigger: 'blur' }]
|
||||
}
|
||||
|
||||
formRules.value = reactive(formRulesObj)
|
||||
}
|
||||
|
||||
/** 打开弹窗 */
|
||||
// TODO @fan:title 是不是收敛到 type 判断生成 title,会更合理
|
||||
const open = async (type: string, id?: number, title?: string) => {
|
||||
dialogVisible.value = true
|
||||
dialogTitle.value = title || t('action.' + type)
|
||||
|
@ -148,7 +137,7 @@ const open = async (type: string, id?: number, title?: string) => {
|
|||
// 获得下拉数据
|
||||
chatModelList.value = await ChatModelApi.getChatModelSimpleList(CommonStatusEnum.ENABLE)
|
||||
}
|
||||
defineExpose({open}) // 提供 open 方法,用于打开弹窗
|
||||
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
|
||||
|
||||
/** 提交表单 */
|
||||
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
|
||||
|
@ -159,10 +148,8 @@ const submitForm = async () => {
|
|||
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'))
|
||||
|
@ -184,7 +171,6 @@ const submitForm = async () => {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/** 重置表单 */
|
||||
const resetForm = () => {
|
||||
formData.value = {
|
||||
|
@ -196,7 +182,6 @@ const resetForm = () => {
|
|||
sort: undefined,
|
||||
description: undefined,
|
||||
systemMessage: undefined,
|
||||
welcomeMessage: undefined,
|
||||
publicStatus: true,
|
||||
status: CommonStatusEnum.ENABLE
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
<template>
|
||||
<div class="flex h-1/1">
|
||||
<!-- 模式 -->
|
||||
<Mode class="flex-none" @generate-music="generateMusic"/>
|
||||
<!-- 音频列表 -->
|
||||
<List ref="listRef" class="flex-auto"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Mode from './mode/index.vue'
|
||||
import List from './list/index.vue'
|
||||
|
||||
defineOptions({ name: 'Index' })
|
||||
|
||||
const listRef = ref<{generateMusic: (...args) => void} | null>(null)
|
||||
|
||||
function generateMusic (args: {formData: Recordable}) {
|
||||
unref(listRef)?.generateMusic(args.formData)
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,9 @@
|
|||
<template>
|
||||
<div class="h-72px bg-[var(--el-bg-color-overlay)] b-solid b-1 b-[var(--el-border-color)] b-l-none">播放器</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
defineOptions({ name: 'Index' })
|
||||
|
||||
</script>
|
|
@ -0,0 +1,94 @@
|
|||
<template>
|
||||
<div class="flex flex-col h-full">
|
||||
<div class="flex-auto flex overflow-hidden">
|
||||
<el-tabs v-model="currentType" class="flex-auto px-[var(--app-content-padding)]">
|
||||
<!-- 我的创作 -->
|
||||
<el-tab-pane label="我的创作" v-loading="loading" name="mine">
|
||||
<el-row v-if="mySongList.length" :gutter="12">
|
||||
<el-col v-for="song in mySongList" :key="song.id" :span="24">
|
||||
<songCard v-bind="song"/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-empty v-else description="暂无音乐"/>
|
||||
</el-tab-pane>
|
||||
|
||||
<!-- 试听广场 -->
|
||||
<el-tab-pane label="试听广场" v-loading="loading" name="square">
|
||||
<el-row v-if="squareSongList.length" v-loading="loading" :gutter="12">
|
||||
<el-col v-for="song in squareSongList" :key="song.id" :span="24">
|
||||
<songCard v-bind="song"/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-empty v-else description="暂无音乐"/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
<!-- songInfo -->
|
||||
<songInfo v-bind="squareSongList[0]" class="flex-none"/>
|
||||
</div>
|
||||
<audioBar class="flex-none"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import songCard from './songCard/index.vue'
|
||||
import songInfo from './songInfo/index.vue'
|
||||
import audioBar from './audioBar/index.vue'
|
||||
|
||||
defineOptions({ name: 'Index' })
|
||||
|
||||
const currentType = ref('mine')
|
||||
// loading 状态
|
||||
const loading = ref(false)
|
||||
|
||||
const mySongList = ref<Recordable[]>([])
|
||||
const squareSongList = ref<Recordable[]>([])
|
||||
|
||||
/*
|
||||
*@Description: 调接口生成音乐列表
|
||||
*@MethodAuthor: xiaohong
|
||||
*@Date: 2024-06-27 17:06:44
|
||||
*/
|
||||
function generateMusic (formData: Recordable) {
|
||||
console.log(formData);
|
||||
loading.value = true
|
||||
setTimeout(() => {
|
||||
mySongList.value = Array.from({ length: 20 }, (_, index) => {
|
||||
return {
|
||||
id: index,
|
||||
audioUrl: '',
|
||||
videoUrl: '',
|
||||
title: '我走后',
|
||||
imageUrl: 'https://www.carsmp3.com/data/attachment/forum/201909/19/091020q5kgre20fidreqyt.jpg',
|
||||
desc: 'Metal, symphony, film soundtrack, grand, majesticMetal, dtrack, grand, majestic',
|
||||
date: '2024年04月30日 14:02:57',
|
||||
lyric: `<div class="_words_17xen_66"><div>大江东去,浪淘尽,千古风流人物。
|
||||
</div><div>故垒西边,人道是,三国周郎赤壁。
|
||||
</div><div>乱石穿空,惊涛拍岸,卷起千堆雪。
|
||||
</div><div>江山如画,一时多少豪杰。
|
||||
</div><div>
|
||||
</div><div>遥想公瑾当年,小乔初嫁了,雄姿英发。
|
||||
</div><div>羽扇纶巾,谈笑间,樯橹灰飞烟灭。
|
||||
</div><div>故国神游,多情应笑我,早生华发。
|
||||
</div><div>人生如梦,一尊还酹江月。</div></div>`
|
||||
}
|
||||
})
|
||||
loading.value = false
|
||||
}, 3000)
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
generateMusic
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-tabs) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.el-tabs__content{
|
||||
padding: 0 7px;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,29 @@
|
|||
<template>
|
||||
<div class="flex bg-[var(--el-bg-color-overlay)] p-12px mb-12px rounded-1">
|
||||
<el-image :src="imageUrl" class="flex-none w-80px"/>
|
||||
<div class="ml-8px">
|
||||
<div>{{ title }}</div>
|
||||
<div class="mt-8px text-12px text-[var(--el-text-color-secondary)] line-clamp-2">
|
||||
{{ desc }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
defineOptions({ name: 'Index' })
|
||||
|
||||
defineProps({
|
||||
imageUrl: {
|
||||
type: String
|
||||
},
|
||||
title: {
|
||||
type: String
|
||||
},
|
||||
desc: {
|
||||
type: String
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
|
@ -0,0 +1,33 @@
|
|||
<template>
|
||||
<ContentWrap class="w-300px mb-[0!important] line-height-24px">
|
||||
<el-image :src="imageUrl"/>
|
||||
<div class="">{{ title }}</div>
|
||||
<div class="text-[var(--el-text-color-secondary)] text-12px line-clamp-1">{{ desc }}</div>
|
||||
<div class="text-[var(--el-text-color-secondary)] text-12px">{{ date }}</div>
|
||||
<el-button size="small" round class="my-6px">信息复用</el-button>
|
||||
<div class="text-[var(--el-text-color-secondary)] text-12px" v-html="lyric"></div>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
defineOptions({ name: 'Index' })
|
||||
|
||||
defineProps({
|
||||
imageUrl: {
|
||||
type: String
|
||||
},
|
||||
title: {
|
||||
type: String
|
||||
},
|
||||
desc: {
|
||||
type: String
|
||||
},
|
||||
date: {
|
||||
type: String
|
||||
},
|
||||
lyric: {
|
||||
type: String
|
||||
}
|
||||
})
|
||||
</script>
|
|
@ -0,0 +1,55 @@
|
|||
<template>
|
||||
<div>
|
||||
<Title title="音乐/歌词说明" desc="描述您想要的音乐风格和主题,使用流派和氛围而不是特定的艺术家和歌曲">
|
||||
<el-input
|
||||
v-model="formData.desc"
|
||||
:autosize="{ minRows: 6, maxRows: 6}"
|
||||
resize="none"
|
||||
type="textarea"
|
||||
maxlength="1200"
|
||||
show-word-limit
|
||||
placeholder="一首关于糟糕分手的欢快歌曲"
|
||||
/>
|
||||
</Title>
|
||||
|
||||
<Title title="纯音乐" desc="创建一首没有歌词的歌曲">
|
||||
<template #extra>
|
||||
<el-switch v-model="formData.pure" size="small"/>
|
||||
</template>
|
||||
</Title>
|
||||
|
||||
<Title title="版本" desc="描述您想要的音乐风格和主题,使用流派和氛围而不是特定的艺术家和歌曲">
|
||||
<el-select v-model="formData.version" placeholder="请选择">
|
||||
<el-option
|
||||
v-for="item in [{
|
||||
value: '3',
|
||||
label: 'V3'
|
||||
}, {
|
||||
value: '2',
|
||||
label: 'V2'
|
||||
}]"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</Title>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Title from '../title/index.vue'
|
||||
|
||||
defineOptions({ name: 'Desc' })
|
||||
|
||||
const formData = reactive({
|
||||
desc: '',
|
||||
pure: false,
|
||||
version: '3'
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
formData
|
||||
})
|
||||
|
||||
</script>
|
|
@ -0,0 +1,44 @@
|
|||
<template>
|
||||
<ContentWrap class="w-300px h-full">
|
||||
<el-radio-group v-model="generateMode" class="mb-15px">
|
||||
<el-radio-button label="desc">
|
||||
描述模式
|
||||
</el-radio-button>
|
||||
<el-radio-button label="lyric">
|
||||
歌词模式
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
|
||||
<!-- 描述模式/歌词模式 切换 -->
|
||||
<component :is="generateMode === 'desc' ? desc : lyric" ref="modeRef"/>
|
||||
|
||||
<el-button type="primary" round class="w-full" @click="generateMusic">
|
||||
创作音乐
|
||||
</el-button>
|
||||
</ContentWrap>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import desc from './desc.vue'
|
||||
import lyric from './lyric.vue'
|
||||
|
||||
defineOptions({ name: 'Index' })
|
||||
|
||||
const emits = defineEmits(['generate-music'])
|
||||
|
||||
const generateMode = ref('lyric')
|
||||
|
||||
interface ModeRef {
|
||||
formData: Recordable
|
||||
}
|
||||
const modeRef = ref<ModeRef | null>(null)
|
||||
|
||||
/*
|
||||
*@Description: 根据信息生成音乐
|
||||
*@MethodAuthor: xiaohong
|
||||
*@Date: 2024-06-27 16:40:16
|
||||
*/
|
||||
function generateMusic () {
|
||||
emits('generate-music', {formData: unref(modeRef)?.formData.value})
|
||||
}
|
||||
</script>
|
|
@ -0,0 +1,83 @@
|
|||
<template>
|
||||
<div class="">
|
||||
<Title title="歌词" desc="自己编写歌词或使用Ai生成歌词,两节/8行效果最佳">
|
||||
<el-input
|
||||
v-model="formData.lyric"
|
||||
:autosize="{ minRows: 6, maxRows: 6}"
|
||||
resize="none"
|
||||
type="textarea"
|
||||
maxlength="1200"
|
||||
show-word-limit
|
||||
placeholder="请输入您自己的歌词"
|
||||
/>
|
||||
</Title>
|
||||
|
||||
<Title title="音乐风格">
|
||||
<el-space class="flex-wrap">
|
||||
<el-tag v-for="tag in tags" :key="tag" round class="mb-8px">{{tag}}</el-tag>
|
||||
</el-space>
|
||||
|
||||
<el-button
|
||||
:type="showCustom ? 'primary': 'default'"
|
||||
round
|
||||
size="small"
|
||||
class="mb-6px"
|
||||
@click="showCustom = !showCustom"
|
||||
>自定义风格
|
||||
</el-button>
|
||||
</Title>
|
||||
|
||||
<Title v-show="showCustom" desc="描述您想要的音乐风格,Suno无法识别艺术家的名字,但可以理解流派和氛围" class="-mt-12px">
|
||||
<el-input
|
||||
v-model="formData.style"
|
||||
:autosize="{ minRows: 4, maxRows: 4}"
|
||||
resize="none"
|
||||
type="textarea"
|
||||
maxlength="256"
|
||||
show-word-limit
|
||||
placeholder="输入音乐风格(英文)"
|
||||
/>
|
||||
</Title>
|
||||
|
||||
<Title title="音乐/歌曲名称">
|
||||
<el-input v-model="formData.name" placeholder="请输入音乐/歌曲名称"/>
|
||||
</Title>
|
||||
|
||||
<Title title="版本">
|
||||
<el-select v-model="formData.version" placeholder="请选择">
|
||||
<el-option
|
||||
v-for="item in [{
|
||||
value: '3',
|
||||
label: 'V3'
|
||||
}, {
|
||||
value: '2',
|
||||
label: 'V2'
|
||||
}]"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</Title>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import Title from '../title/index.vue'
|
||||
defineOptions({ name: 'Lyric' })
|
||||
|
||||
const tags = ['rock', 'punk', 'jazz', 'soul', 'country', 'kidsmusic', 'pop']
|
||||
|
||||
const showCustom = ref(false)
|
||||
|
||||
const formData = reactive({
|
||||
lyric: '',
|
||||
style: '',
|
||||
name: '',
|
||||
version: ''
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
formData
|
||||
})
|
||||
</script>
|
|
@ -0,0 +1,25 @@
|
|||
<template>
|
||||
<div class="mb-12px">
|
||||
<div class="flex text-[var(--el-text-color-primary)] justify-between items-center">
|
||||
<span>{{title}}</span>
|
||||
<slot name="extra"></slot>
|
||||
</div>
|
||||
<div class="text-[var(--el-text-color-secondary)] text-12px my-8px">
|
||||
{{desc}}
|
||||
</div>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
defineOptions({ name: 'Index' })
|
||||
|
||||
defineProps({
|
||||
title: {
|
||||
type: String
|
||||
},
|
||||
desc: {
|
||||
type: String
|
||||
}
|
||||
})
|
||||
</script>
|
|
@ -0,0 +1,285 @@
|
|||
<template>
|
||||
<ContentWrap>
|
||||
<!-- 搜索工作栏 -->
|
||||
<el-form
|
||||
class="-mb-15px"
|
||||
:model="queryParams"
|
||||
ref="queryFormRef"
|
||||
:inline="true"
|
||||
label-width="68px"
|
||||
>
|
||||
<el-form-item label="用户编号" prop="userId">
|
||||
<el-input
|
||||
v-model="queryParams.userId"
|
||||
placeholder="请输入用户编号"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="音乐名称" prop="title">
|
||||
<el-input
|
||||
v-model="queryParams.title"
|
||||
placeholder="请输入音乐名称"
|
||||
clearable
|
||||
@keyup.enter="handleQuery"
|
||||
class="!w-240px"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="音乐状态" prop="status">
|
||||
<el-select
|
||||
v-model="queryParams.status"
|
||||
placeholder="请选择音乐状态"
|
||||
clearable
|
||||
class="!w-240px"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.AI_MUSIC_STATUS)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="生成模式" prop="generateMode">
|
||||
<el-select
|
||||
v-model="queryParams.generateMode"
|
||||
placeholder="请选择生成模式"
|
||||
clearable
|
||||
class="!w-240px"
|
||||
>
|
||||
<el-option
|
||||
v-for="dict in getIntDictOptions(DICT_TYPE.AI_GENERATE_MODE)"
|
||||
:key="dict.value"
|
||||
:label="dict.label"
|
||||
:value="dict.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="创建时间" prop="createTime">
|
||||
<el-date-picker
|
||||
v-model="queryParams.createTime"
|
||||
value-format="YYYY-MM-DD HH:mm:ss"
|
||||
type="daterange"
|
||||
start-placeholder="开始日期"
|
||||
end-placeholder="结束日期"
|
||||
:default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]"
|
||||
class="!w-220px"
|
||||
/>
|
||||
</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-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="id" width="180" fixed="left" />
|
||||
<el-table-column label="音乐名称" align="center" prop="title" width="180px" fixed="left" />
|
||||
<el-table-column label="用户" align="center" prop="userId" width="180">
|
||||
<template #default="scope">
|
||||
<span>{{ userList.find((item) => item.id === scope.row.userId)?.nickname }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="音乐状态" align="center" prop="status" width="100">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.AI_MUSIC_STATUS" :value="scope.row.status" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="模型" align="center" prop="model" width="180" />
|
||||
<el-table-column label="内容" align="center" width="180">
|
||||
<template #default="{ row }">
|
||||
<el-link
|
||||
v-if="row.audioUrl?.length > 0"
|
||||
type="primary"
|
||||
:href="row.audioUrl"
|
||||
target="_blank"
|
||||
>
|
||||
音乐
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="row.videoUrl?.length > 0"
|
||||
type="primary"
|
||||
:href="row.videoUrl"
|
||||
target="_blank"
|
||||
class="!pl-5px"
|
||||
>
|
||||
视频
|
||||
</el-link>
|
||||
<el-link
|
||||
v-if="row.imageUrl?.length > 0"
|
||||
type="primary"
|
||||
:href="row.imageUrl"
|
||||
target="_blank"
|
||||
class="!pl-5px"
|
||||
>
|
||||
封面
|
||||
</el-link>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="时长(秒)" align="center" prop="duration" width="100" />
|
||||
<el-table-column label="提示词" align="center" prop="prompt" width="180" />
|
||||
<el-table-column label="歌词" align="center" prop="lyric" width="180" />
|
||||
<el-table-column label="描述" align="center" prop="gptDescriptionPrompt" width="180" />
|
||||
<el-table-column label="生成模式" align="center" prop="generateMode" width="100">
|
||||
<template #default="scope">
|
||||
<dict-tag :type="DICT_TYPE.AI_GENERATE_MODE" :value="scope.row.generateMode" />
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="风格标签" align="center" prop="tags" width="180">
|
||||
<template #default="scope">
|
||||
<el-tag v-for="tag in scope.row.tags" :key="tag" round class="ml-2px">
|
||||
{{ tag }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="是否发布" align="center" prop="publicStatus">
|
||||
<template #default="scope">
|
||||
<el-switch
|
||||
v-model="scope.row.publicStatus"
|
||||
:active-value="true"
|
||||
:inactive-value="false"
|
||||
@change="handleUpdatePublicStatusChange(scope.row)"
|
||||
:disabled="scope.row.status !== 20"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="任务编号" align="center" prop="taskId" width="180" />
|
||||
<el-table-column label="错误信息" align="center" prop="errorMessage" />
|
||||
<el-table-column
|
||||
label="创建时间"
|
||||
align="center"
|
||||
prop="createTime"
|
||||
:formatter="dateFormatter"
|
||||
width="180px"
|
||||
/>
|
||||
<el-table-column label="操作" align="center" width="100" fixed="right">
|
||||
<template #default="scope">
|
||||
<el-button
|
||||
link
|
||||
type="danger"
|
||||
@click="handleDelete(scope.row.id)"
|
||||
v-hasPermi="['ai:music: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>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { getIntDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict'
|
||||
import { dateFormatter } from '@/utils/formatTime'
|
||||
import { MusicApi, MusicVO } from '@/api/ai/music'
|
||||
import * as UserApi from '@/api/system/user'
|
||||
|
||||
/** AI 音乐 列表 */
|
||||
defineOptions({ name: 'AiMusicManager' })
|
||||
|
||||
const message = useMessage() // 消息弹窗
|
||||
const { t } = useI18n() // 国际化
|
||||
|
||||
const loading = ref(true) // 列表的加载中
|
||||
const list = ref<MusicVO[]>([]) // 列表的数据
|
||||
const total = ref(0) // 列表的总页数
|
||||
const queryParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 10,
|
||||
userId: undefined,
|
||||
title: undefined,
|
||||
status: undefined,
|
||||
generateMode: undefined,
|
||||
createTime: [],
|
||||
publicStatus: undefined
|
||||
})
|
||||
const queryFormRef = ref() // 搜索的表单
|
||||
const userList = ref<UserApi.UserVO[]>([]) // 用户列表
|
||||
|
||||
/** 查询列表 */
|
||||
const getList = async () => {
|
||||
loading.value = true
|
||||
try {
|
||||
const data = await MusicApi.getMusicPage(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 handleDelete = async (id: number) => {
|
||||
try {
|
||||
// 删除的二次确认
|
||||
await message.delConfirm()
|
||||
// 发起删除
|
||||
await MusicApi.deleteMusic(id)
|
||||
message.success(t('common.delSuccess'))
|
||||
// 刷新列表
|
||||
await getList()
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 修改是否发布 */
|
||||
const handleUpdatePublicStatusChange = async (row: MusicVO) => {
|
||||
try {
|
||||
// 修改状态的二次确认
|
||||
const text = row.publicStatus ? '公开' : '私有'
|
||||
await message.confirm('确认要"' + text + '"该音乐吗?')
|
||||
// 发起修改状态
|
||||
await MusicApi.updateMusic({
|
||||
id: row.id,
|
||||
publicStatus: row.publicStatus
|
||||
})
|
||||
await getList()
|
||||
} catch {
|
||||
row.publicStatus = !row.publicStatus
|
||||
}
|
||||
}
|
||||
|
||||
/** 初始化 **/
|
||||
onMounted(async () => {
|
||||
getList()
|
||||
// 获得用户列表
|
||||
userList.value = await UserApi.getSimpleUserList()
|
||||
})
|
||||
</script>
|
|
@ -235,11 +235,7 @@ const handleChangeStatus = async (row: JobApi.JobVO) => {
|
|||
message.success(text + '成功')
|
||||
// 刷新列表
|
||||
await getList()
|
||||
} catch {
|
||||
// 取消后,进行恢复按钮
|
||||
row.status =
|
||||
row.status === InfraJobStatusEnum.NORMAL ? InfraJobStatusEnum.STOP : InfraJobStatusEnum.NORMAL
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 删除按钮操作 */
|
||||
|
|
Loading…
Reference in New Issue