Commit 5376eb40 by YunaiV

Merge branch 'master' of https://gitee.com/yudaocode/yudao-ui-admin-vue3 into feature/bpm

parents 7cb68da5 a7c37fa2
...@@ -18,3 +18,8 @@ VITE_APP_DOCALERT_ENABLE=true ...@@ -18,3 +18,8 @@ VITE_APP_DOCALERT_ENABLE=true
# 百度统计 # 百度统计
VITE_APP_BAIDU_CODE = a1ff8825baa73c3a78eb96aa40325abc VITE_APP_BAIDU_CODE = a1ff8825baa73c3a78eb96aa40325abc
# 默认账户密码
VITE_APP_DEFAULT_LOGIN_TENANT = 芋道源码
VITE_APP_DEFAULT_LOGIN_USERNAME = admin
VITE_APP_DEFAULT_LOGIN_PASSWORD = admin123
...@@ -2,7 +2,6 @@ node_modules ...@@ -2,7 +2,6 @@ node_modules
.DS_Store .DS_Store
dist dist
dist-ssr dist-ssr
*.local
/dist* /dist*
pnpm-debug pnpm-debug
auto-*.d.ts auto-*.d.ts
......
...@@ -54,17 +54,16 @@ ...@@ -54,17 +54,16 @@
推荐 VS Code 开发,配合插件如下: 推荐 VS Code 开发,配合插件如下:
| 插件名 | 功能 | | 插件名 | 功能 |
|-------------------------------|--------------------------| |-------------------------------|---------------------|
| TypeScript Vue Plugin (Volar) | 用于 TypeScript 的 Vue 插件 | | Vue - Official | Vue 与 TypeScript 支持 |
| Vue Language Features (Volar) | Vue3.0 语法支持 | | unocss | unocss for vscode |
| unocss | unocss for vscode | | Iconify IntelliSense | Iconify 预览和搜索 |
| Iconify IntelliSense | Iconify 预览和搜索 | | i18n Ally | 国际化智能提示 |
| i18n Ally | 国际化智能提示 | | Stylelint | Css 格式化 |
| Stylelint | Css 格式化 | | Prettier | 代码格式化 |
| Prettier | 代码格式化 | | ESLint | 脚本代码检查 |
| ESLint | 脚本代码检查 | | DotENV | env 文件高亮 |
| DotENV | env 文件高亮 |
## 🔥 后端架构 ## 🔥 后端架构
...@@ -192,26 +191,24 @@ ps:核心功能已经实现,正在对接微信小程序中... ...@@ -192,26 +191,24 @@ ps:核心功能已经实现,正在对接微信小程序中...
### 商城系统 ### 商城系统
演示地址:<https://doc.iocoder.cn/mall-preview/>
![功能图](/.image/common/mall-feature.png) ![功能图](/.image/common/mall-feature.png)
![功能图](/.image/common/mall-preview.png) ![功能图](/.image/common/mall-preview.png)
_前端基于 crmeb uniapp 经过授权重构,优化代码实现,接入芋道快速开发平台_
演示地址:<https://doc.iocoder.cn/mall-preview/>
### ERP 系统 ### ERP 系统
![功能图](/.image/common/erp-feature.png)
演示地址:<https://doc.iocoder.cn/erp-preview/> 演示地址:<https://doc.iocoder.cn/erp-preview/>
### CRM 系统 ![功能图](/.image/common/erp-feature.png)
![功能图](/.image/common/crm-feature.png) ### CRM 系统
演示地址:<https://doc.iocoder.cn/crm-preview/> 演示地址:<https://doc.iocoder.cn/crm-preview/>
![功能图](/.image/common/crm-feature.png)
## 🐷 演示图 ## 🐷 演示图
### 系统功能 ### 系统功能
......
...@@ -27,6 +27,12 @@ const include = [ ...@@ -27,6 +27,12 @@ const include = [
'echarts-wordcloud', 'echarts-wordcloud',
'@wangeditor/editor', '@wangeditor/editor',
'@wangeditor/editor-for-vue', '@wangeditor/editor-for-vue',
'@microsoft/fetch-event-source',
'markdown-it',
'markmap-view',
'markmap-lib',
'markmap-toolbar',
'highlight.js',
'element-plus', 'element-plus',
'element-plus/es', 'element-plus/es',
'element-plus/es/locale/lang/zh-cn', 'element-plus/es/locale/lang/zh-cn',
...@@ -104,7 +110,11 @@ const include = [ ...@@ -104,7 +110,11 @@ const include = [
'element-plus/es/components/collapse/style/css', 'element-plus/es/components/collapse/style/css',
'element-plus/es/components/collapse-item/style/css', 'element-plus/es/components/collapse-item/style/css',
'element-plus/es/components/button-group/style/css', 'element-plus/es/components/button-group/style/css',
'element-plus/es/components/text/style/css' 'element-plus/es/components/text/style/css',
'element-plus/es/components/segmented/style/css',
'@element-plus/icons-vue',
'element-plus/es/components/footer/style/css',
'element-plus/es/components/empty/style/css'
] ]
const exclude = ['@iconify/json'] const exclude = ['@iconify/json']
......
{ {
"name": "yudao-ui-admin-vue3", "name": "yudao-ui-admin-vue3",
"version": "2.1.0-snapshot", "version": "2.2.0-snapshot",
"description": "基于vue3、vite4、element-plus、typesScript", "description": "基于vue3、vite4、element-plus、typesScript",
"author": "xingyu", "author": "xingyu",
"private": false, "private": false,
...@@ -52,7 +52,11 @@ ...@@ -52,7 +52,11 @@
"highlight.js": "^11.9.0", "highlight.js": "^11.9.0",
"jsencrypt": "^3.3.2", "jsencrypt": "^3.3.2",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
"marked": "^12.0.2", "markdown-it": "^14.1.0",
"markmap-common": "^0.16.0",
"markmap-lib": "^0.16.1",
"markmap-toolbar": "^0.17.0",
"markmap-view": "^0.16.0",
"min-dash": "^4.1.1", "min-dash": "^4.1.1",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"nprogress": "^0.2.0", "nprogress": "^0.2.0",
...@@ -85,8 +89,8 @@ ...@@ -85,8 +89,8 @@
"@types/qs": "^6.9.12", "@types/qs": "^6.9.12",
"@typescript-eslint/eslint-plugin": "^7.1.0", "@typescript-eslint/eslint-plugin": "^7.1.0",
"@typescript-eslint/parser": "^7.1.0", "@typescript-eslint/parser": "^7.1.0",
"@unocss/transformer-variant-group": "^0.58.5",
"@unocss/eslint-config": "^0.57.4", "@unocss/eslint-config": "^0.57.4",
"@unocss/transformer-variant-group": "^0.58.5",
"@vitejs/plugin-legacy": "^5.3.1", "@vitejs/plugin-legacy": "^5.3.1",
"@vitejs/plugin-vue": "^5.0.4", "@vitejs/plugin-vue": "^5.0.4",
"@vitejs/plugin-vue-jsx": "^3.1.0", "@vitejs/plugin-vue-jsx": "^3.1.0",
......
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -2,7 +2,7 @@ import request from '@/config/axios' ...@@ -2,7 +2,7 @@ import request from '@/config/axios'
// AI 聊天对话 VO // AI 聊天对话 VO
export interface ChatConversationVO { export interface ChatConversationVO {
id: string // ID 编号 id: number // ID 编号
userId: number // 用户编号 userId: number // 用户编号
title: string // 对话标题 title: string // 对话标题
pinned: boolean // 是否置顶 pinned: boolean // 是否置顶
...@@ -12,6 +12,7 @@ export interface ChatConversationVO { ...@@ -12,6 +12,7 @@ export interface ChatConversationVO {
temperature: number // 温度参数 temperature: number // 温度参数
maxTokens: number // 单条回复的最大 Token 数量 maxTokens: number // 单条回复的最大 Token 数量
maxContexts: number // 上下文的最大 Message 数量 maxContexts: number // 上下文的最大 Message 数量
createTime?: Date // 创建时间
// 额外字段 // 额外字段
systemMessage?: string // 角色设定 systemMessage?: string // 角色设定
modelName?: string // 模型名字 modelName?: string // 模型名字
...@@ -23,7 +24,7 @@ export interface ChatConversationVO { ...@@ -23,7 +24,7 @@ export interface ChatConversationVO {
// AI 聊天对话 API // AI 聊天对话 API
export const ChatConversationApi = { export const ChatConversationApi = {
// 获得【我的】聊天对话 // 获得【我的】聊天对话
getChatConversationMy: async (id: string) => { getChatConversationMy: async (id: number) => {
return await request.get({ url: `/ai/chat/conversation/get-my?id=${id}` }) return await request.get({ url: `/ai/chat/conversation/get-my?id=${id}` })
}, },
...@@ -43,8 +44,8 @@ export const ChatConversationApi = { ...@@ -43,8 +44,8 @@ export const ChatConversationApi = {
}, },
// 删除【我的】所有对话,置顶除外 // 删除【我的】所有对话,置顶除外
deleteMyAllExceptPinned: async () => { deleteChatConversationMyByUnpinned: async () => {
return await request.delete({ url: `/ai/chat/conversation/delete-my-all-except-pinned` }) return await request.delete({ url: `/ai/chat/conversation/delete-by-unpinned` })
}, },
// 获得【我的】聊天对话列表 // 获得【我的】聊天对话列表
......
...@@ -19,23 +19,18 @@ export interface ChatMessageVO { ...@@ -19,23 +19,18 @@ export interface ChatMessageVO {
userAvatar: string // 创建时间 userAvatar: string // 创建时间
} }
export interface ChatMessageSendVO {
conversationId: string // 对话编号
content: number // 聊天内容
}
// AI chat 聊天 // AI chat 聊天
export const ChatMessageApi = { export const ChatMessageApi = {
// 消息列表 // 消息列表
messageList: async (conversationId: string | null) => { getChatMessageListByConversationId: async (conversationId: number | null) => {
return await request.get({ return await request.get({
url: `/ai/chat/message/list-by-conversation-id?conversationId=${conversationId}` url: `/ai/chat/message/list-by-conversation-id?conversationId=${conversationId}`
}) })
}, },
// 发送 send stream 消息 // 发送 Stream 消息
// TODO axios 可以么? https://apifox.com/apiskills/how-to-create-axios-stream/ // 为什么不用 axios 呢?因为它不支持 SSE 调用
sendStream: async ( sendChatMessageStream: async (
conversationId: number, conversationId: number,
content: string, content: string,
ctrl, ctrl,
...@@ -65,12 +60,12 @@ export const ChatMessageApi = { ...@@ -65,12 +60,12 @@ export const ChatMessageApi = {
}, },
// 删除消息 // 删除消息
delete: async (id: string) => { deleteChatMessage: async (id: string) => {
return await request.delete({ url: `/ai/chat/message/delete?id=${id}` }) return await request.delete({ url: `/ai/chat/message/delete?id=${id}` })
}, },
// 删除消息 - 对话所有消息 // 删除指定对话的消息
deleteByConversationId: async (conversationId: string) => { deleteByConversationId: async (conversationId: number) => {
return await request.delete({ return await request.delete({
url: `/ai/chat/message/delete-by-conversation-id?conversationId=${conversationId}` url: `/ai/chat/message/delete-by-conversation-id?conversationId=${conversationId}`
}) })
......
import request from '@/config/axios' import request from '@/config/axios'
// AI API 密钥 VO // AI 绘图 VO
// TODO @fan:要不前端不弄太多 VO,就用这个 ImageDetailVO?! export interface ImageVO {
export interface ImageDetailVO {
id: number // 编号 id: number // 编号
prompt: string // 提示词
status: number // 状态
errorMessage: string // 错误信息
type: string // 模型下分不同的类型(清晰、真实...)
taskId: number // dr 任务id
picUrl: string // 任务地址
originalPicUrl: string // 绘制图片地址
platform: string // 平台 platform: string // 平台
model: string // 模型 model: string // 模型
style: string // 图像生成的风格
size: string // 图片尺寸
buttons: ImageMjButtonsVO[] // mj 操作按钮
createTime: string // 创建时间
updateTime: string // 更新事件
}
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)
}
export interface ImageMjActionVO {
id: string // MJ::JOB::upsample::1::85a4b4c1-8835-46c5-a15c-aea34fad1862 动作标识
customId: string // MJ::JOB::upsample::1::85a4b4c1-8835-46c5-a15c-aea34fad1862 动作标识
}
export interface ImagePageReqVO {
pageNo: number // 分页编号
pageSize: number // 分页大小
}
export interface ImageDallReqVO {
prompt: string // 提示词 prompt: string // 提示词
model: string // 模型 width: number // 图片宽度
style: string // 图像生成的风格 height: number // 图片高度
width: string // 图片宽度 status: number // 状态
height: string // 图片高度 publicStatus: boolean // 公开状态
picUrl: string // 任务地址
errorMessage: string // 错误信息
options: any // 配置 Map<string, string>
taskId: number // 任务编号
buttons: ImageMidjourneyButtonsVO[] // mj 操作按钮
createTime: Date // 创建时间
finishTime: Date // 完成时间
} }
export interface ImageDrawReqVO { export interface ImageDrawReqVO {
...@@ -65,34 +38,66 @@ export interface ImageMidjourneyImagineReqVO { ...@@ -65,34 +38,66 @@ export interface ImageMidjourneyImagineReqVO {
version: string // 版本 version: string // 版本
} }
// TODO 芋艿:review 下整体注释、方法名 export interface ImageMidjourneyActionVO {
// AI API 密钥 API id: number // 图片编号
customId: string // MJ::JOB::upsample::1::85a4b4c1-8835-46c5-a15c-aea34fad1862 动作标识
}
export interface ImageMidjourneyButtonsVO {
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
export const ImageApi = { export const ImageApi = {
// 获取 image 列表 // 获取【我的】绘图分页
getImageList: async (params: ImagePageReqVO) => { getImagePageMy: async (params: any) => {
return await request.get({ url: `/ai/image/my-page`, params }) return await request.get({ url: `/ai/image/my-page`, params })
}, },
// 获取 image 详细信息 // 获取【我的】绘图记录
getImageDetail: async (id: number) => { getImageMy: async (id: number) => {
return await request.get({ url: `/ai/image/get-my?id=${id}`}) 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)=> { drawImage: async (data: ImageDrawReqVO) => {
return await request.post({ url: `/ai/image/draw`, data }) return await request.post({ url: `/ai/image/draw`, data })
}, },
// 删除 // 删除【我的】绘画记录
deleteImage: async (id: number)=> { deleteImageMy: async (id: number) => {
return await request.delete({ url: `/ai/image/delete-my?id=${id}`}) return await request.delete({ url: `/ai/image/delete-my?id=${id}` })
}, },
// ------------ midjourney // ================ midjourney 专属 ================
// midjourney - imagine // 【Midjourney】生成图片
midjourneyImagine: async (data: ImageMidjourneyImagineReqVO)=> { midjourneyImagine: async (data: ImageMidjourneyImagineReqVO) => {
return await request.post({ url: `/ai/image/midjourney/imagine`, data }) return await request.post({ url: `/ai/image/midjourney/imagine`, data })
}, },
// midjourney - action // 【Midjourney】Action 操作(二次生成图片)
midjourneyAction: async (params: ImageMjActionVO)=> { midjourneyAction: async (data: ImageMidjourneyActionVO) => {
return await request.get({ url: `/ai/image/midjourney/action`, params }) 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', data })
},
// 删除绘画
deleteImage: async (id: number) => {
return await request.delete({ url: `/ai/image/delete?id=` + id })
}
} }
import { getAccessToken } from '@/utils/auth'
import { fetchEventSource } from '@microsoft/fetch-event-source'
import { config } from '@/config/axios/config'
import request from '@/config/axios'
// AI 思维导图 VO
export interface MindMapVO {
id: number // 编号
userId: number // 用户编号
prompt: string // 生成内容提示
generatedContent: string // 生成的思维导图内容
platform: string // 平台
model: string // 模型
errorMessage: string // 错误信息
}
// AI 思维导图生成 VO
export interface AiMindMapGenerateReqVO {
prompt: string
}
export const AiMindMapApi = {
generateMindMap: ({
data,
onClose,
onMessage,
onError,
ctrl
}: {
data: AiMindMapGenerateReqVO
onMessage?: (res: any) => void
onError?: (...args: any[]) => void
onClose?: (...args: any[]) => void
ctrl: AbortController
}) => {
const token = getAccessToken()
return fetchEventSource(`${config.base_url}/ai/mind-map/generate-stream`, {
method: 'post',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
openWhenHidden: true,
body: JSON.stringify(data),
onmessage: onMessage,
onerror: onError,
onclose: onClose,
signal: ctrl.signal
})
},
// 查询思维导图分页
getMindMapPage: async (params: any) => {
return await request.get({ url: `/ai/mind-map/page`, params })
},
// 删除思维导图
deleteMindMap: async (id: number) => {
return await request.delete({ url: `/ai/mind-map/delete?id=` + id })
}
}
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 })
}
}
import { fetchEventSource } from '@microsoft/fetch-event-source'
import { getAccessToken } from '@/utils/auth'
import { config } from '@/config/axios/config'
import { AiWriteTypeEnum } from '@/views/ai/utils/constants'
import request from '@/config/axios'
export interface WriteVO {
type: AiWriteTypeEnum.WRITING | AiWriteTypeEnum.REPLY // 1:撰写 2:回复
prompt: string // 写作内容提示 1。撰写 2回复
originalContent: string // 原文
length: number // 长度
format: number // 格式
tone: number // 语气
language: number // 语言
userId?: number // 用户编号
platform?: string // 平台
model?: string // 模型
generatedContent?: string // 生成的内容
errorMessage?: string // 错误信息
createTime?: Date // 创建时间
}
export interface AiWritePageReqVO extends PageParam {
userId?: number // 用户编号
type?: AiWriteTypeEnum // 写作类型
platform?: string // 平台
createTime?: [string, string] // 创建时间
}
export interface AiWriteRespVo {
id: number
userId: number
type: number
platform: string
model: string
prompt: string
generatedContent: string
originalContent: string
length: number
format: number
tone: number
language: number
errorMessage: string
createTime: string
}
export const WriteApi = {
writeStream: ({
data,
onClose,
onMessage,
onError,
ctrl
}: {
data: WriteVO
onMessage?: (res: any) => void
onError?: (...args: any[]) => void
onClose?: (...args: any[]) => void
ctrl: AbortController
}) => {
const token = getAccessToken()
return fetchEventSource(`${config.base_url}/ai/write/generate-stream`, {
method: 'post',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
},
openWhenHidden: true,
body: JSON.stringify(data),
onmessage: onMessage,
onerror: onError,
onclose: onClose,
signal: ctrl.signal
})
},
// 获取写作列表
getWritePage: (params: AiWritePageReqVO) => {
return request.get<PageResult<AiWriteRespVo[]>>({ url: `/ai/write/page`, params })
},
// 删除写作
deleteWrite(id: number) {
return request.delete({ url: `/ai/write/delete`, params: { id } })
}
}
import request from '@/config/axios' import request from '@/config/axios'
export const getProcessDefinition = async (id: number, key: string) => { export const getProcessDefinition = async (id?: string, key?: string) => {
return await request.get({ return await request.get({
url: '/bpm/process-definition/get', url: '/bpm/process-definition/get',
params: { id, key } params: { id, key }
......
...@@ -5,6 +5,7 @@ export type ProcessDefinitionVO = { ...@@ -5,6 +5,7 @@ export type ProcessDefinitionVO = {
version: number version: number
deploymentTIme: string deploymentTIme: string
suspensionState: number suspensionState: number
formType?: number
} }
export type ModelVO = { export type ModelVO = {
......
import request from '@/config/axios' import request from '@/config/axios'
import { ProcessDefinitionVO } from '@/api/bpm/model'
export type Task = { export type Task = {
id: string id: string
...@@ -18,17 +19,7 @@ export type ProcessInstanceVO = { ...@@ -18,17 +19,7 @@ export type ProcessInstanceVO = {
businessKey: string businessKey: string
createTime: string createTime: string
endTime: string endTime: string
} processDefinition?: ProcessDefinitionVO
export type ProcessInstanceCopyVO = {
type: number
taskName: string
taskKey: string
processInstanceName: string
processInstanceKey: string
startUserId: string
options: string[]
reason: string
} }
export const getProcessInstanceMyPage = async (params: any) => { export const getProcessInstanceMyPage = async (params: any) => {
......
...@@ -12,6 +12,7 @@ export interface JobLogVO { ...@@ -12,6 +12,7 @@ export interface JobLogVO {
duration: string duration: string
status: number status: number
createTime: string createTime: string
result: string
} }
// 任务日志列表 // 任务日志列表
......
import request from '@/config/axios'
/**
* 获得商品浏览记录分页
*
* @param params 请求参数
*/
export const getBrowseHistoryPage = (params: any) => {
return request.get({ url: '/product/browse-history/page', params })
}
import request from '@/config/axios'
export interface KeFuConversationRespVO {
id: number // 编号
userId: number // 会话所属用户
userAvatar: string // 会话所属用户头像
userNickname: string // 会话所属用户昵称
lastMessageTime: Date // 最后聊天时间
lastMessageContent: string // 最后聊天内容
lastMessageContentType: number // 最后发送的消息类型
adminPinned: boolean // 管理端置顶
userDeleted: boolean // 用户是否可见
adminDeleted: boolean // 管理员是否可见
adminUnreadMessageCount: number // 管理员未读消息数
createTime?: string // 创建时间
}
// 客服会话 API
export const KeFuConversationApi = {
// 获得客服会话列表
getConversationList: async () => {
return await request.get({ url: '/promotion/kefu-conversation/list' })
},
// 客服会话置顶
updateConversationPinned: async (data: any) => {
return await request.put({
url: '/promotion/kefu-conversation/update-conversation-pinned',
data
})
},
// 删除客服会话
deleteConversation: async (id: number) => {
return await request.get({ url: '/promotion/kefu-conversation/delete?id' + id })
}
}
import request from '@/config/axios'
export interface KeFuMessageRespVO {
id: number // 编号
conversationId: number // 会话编号
senderId: number // 发送人编号
senderAvatar: string // 发送人头像
senderType: number // 发送人类型
receiverId: number // 接收人编号
receiverType: number // 接收人类型
contentType: number // 消息类型
content: string // 消息
readStatus: boolean // 是否已读
createTime: Date // 创建时间
}
// 客服会话 API
export const KeFuMessageApi = {
// 发送客服消息
sendKeFuMessage: async (data: any) => {
return await request.post({
url: '/promotion/kefu-message/send',
data
})
},
// 更新客服消息已读状态
updateKeFuMessageReadStatus: async (conversationId: number) => {
return await request.put({
url: '/promotion/kefu-message/update-read-status?conversationId=' + conversationId
})
},
// 获得消息分页数据
getKeFuMessagePage: async (params: any) => {
return await request.get({ url: '/promotion/kefu-message/page', params })
}
}
<?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>
\ No newline at end of file
...@@ -54,7 +54,7 @@ const currentLocale = computed(() => localeStore.currentLocale) ...@@ -54,7 +54,7 @@ const currentLocale = computed(() => localeStore.currentLocale)
<ElConfigProvider <ElConfigProvider
:namespace="variables.elNamespace" :namespace="variables.elNamespace"
:locale="currentLocale.elLocale" :locale="currentLocale.elLocale"
:message="{ max: 1 }" :message="{ max: 5 }"
:size="size" :size="size"
> >
<slot></slot> <slot></slot>
......
...@@ -10,12 +10,13 @@ const prefixCls = getPrefixCls('content-wrap') ...@@ -10,12 +10,13 @@ const prefixCls = getPrefixCls('content-wrap')
defineProps({ defineProps({
title: propTypes.string.def(''), title: propTypes.string.def(''),
message: propTypes.string.def('') message: propTypes.string.def(''),
bodyStyle: propTypes.object.def({ padding: '20px' })
}) })
</script> </script>
<template> <template>
<ElCard :class="[prefixCls, 'mb-15px']" shadow="never"> <ElCard :body-style="bodyStyle" :class="[prefixCls, 'mb-15px']" shadow="never">
<template v-if="title" #header> <template v-if="title" #header>
<div class="flex items-center"> <div class="flex items-center">
<span class="text-16px font-700">{{ title }}</span> <span class="text-16px font-700">{{ title }}</span>
...@@ -30,8 +31,6 @@ defineProps({ ...@@ -30,8 +31,6 @@ defineProps({
</div> </div>
</div> </div>
</template> </template>
<div> <slot></slot>
<slot></slot>
</div>
</ElCard> </ElCard>
</template> </template>
<script lang="tsx"> <script lang="tsx">
import { defineComponent, PropType, ref } from 'vue' import { defineComponent, PropType, computed } from 'vue'
import { isHexColor } from '@/utils/color' import { isHexColor } from '@/utils/color'
import { ElTag } from 'element-plus' import { ElTag } from 'element-plus'
import { DictDataType, getDictOptions } from '@/utils/dict' import { DictDataType, getDictOptions } from '@/utils/dict'
import { isArray, isString, isNumber } from '@/utils/is'
export default defineComponent({ export default defineComponent({
name: 'DictTag', name: 'DictTag',
...@@ -12,49 +13,78 @@ export default defineComponent({ ...@@ -12,49 +13,78 @@ export default defineComponent({
required: true required: true
}, },
value: { value: {
type: [String, Number, Boolean] as PropType<string | number | boolean>, type: [String, Number, Boolean, Array],
required: true required: true
},
// 字符串分隔符 只有当 props.value 传入值为字符串时有效
separator: {
type: String as PropType<string>,
default: ','
},
// 每个 tag 之间的间隔,默认为 5px,参考的 el-row 的 gutter
gutter: {
type: String as PropType<string>,
default: '5px'
} }
}, },
setup(props) { setup(props) {
const dictData = ref<DictDataType>() const valueArr: any = computed(() => {
const getDictObj = (dictType: string, value: string) => { // 1.是Number类型的情况
const dictOptions = getDictOptions(dictType) if (isNumber(props.value)) {
dictOptions.forEach((dict: DictDataType) => { return [String(props.value)]
if (dict.value === value) { }
if (dict.colorType + '' === 'primary' || dict.colorType + '' === 'default') { // 2.是字符串(进一步判断是否有包含分隔符号 -> props.sepSymbol )
dict.colorType = '' else if (isString(props.value)) {
} return props.value.split(props.separator)
dictData.value = dict }
} // 3.数组
}) else if (isArray(props.value)) {
} return props.value.map(String)
const rederDictTag = () => { }
return []
})
const renderDictTag = () => {
if (!props.type) { if (!props.type) {
return null return null
} }
// 解决自定义字典标签值为零时标签不渲染的问题 // 解决自定义字典标签值为零时标签不渲染的问题
if (props.value === undefined || props.value === null) { if (props.value === undefined || props.value === null || props.value === '') {
return null return null
} }
getDictObj(props.type, props.value.toString()) const dictOptions = getDictOptions(props.type)
// 添加标签的文字颜色为白色,解决自定义背景颜色时标签文字看不清的问题
return ( return (
<ElTag <div
style={dictData.value?.cssClass ? 'color: #fff' : ''} class="dict-tag"
type={dictData.value?.colorType} style={{
color={ display: 'flex',
dictData.value?.cssClass && isHexColor(dictData.value?.cssClass) gap: props.gutter,
? dictData.value?.cssClass justifyContent: 'center',
: '' alignItems: 'center'
} }}
disableTransitions={true}
> >
{dictData.value?.label} {dictOptions.map((dict: DictDataType) => {
</ElTag> if (valueArr.value.includes(dict.value)) {
if (dict.colorType + '' === 'primary' || dict.colorType + '' === 'default') {
dict.colorType = ''
}
return (
// 添加标签的文字颜色为白色,解决自定义背景颜色时标签文字看不清的问题
<ElTag
style={dict?.cssClass ? 'color: #fff' : ''}
type={dict?.colorType}
color={dict?.cssClass && isHexColor(dict?.cssClass) ? dict?.cssClass : ''}
disableTransitions={true}
>
{dict?.label}
</ElTag>
)
}
})}
</div>
) )
} }
return () => rederDictTag() return () => renderDictTag()
} }
}) })
</script> </script>
...@@ -95,6 +95,7 @@ const handleCloneComponent = (component: DiyComponent<any>) => { ...@@ -95,6 +95,7 @@ const handleCloneComponent = (component: DiyComponent<any>) => {
.editor-left { .editor-left {
z-index: 1; z-index: 1;
flex-shrink: 0; flex-shrink: 0;
user-select: none;
box-shadow: 8px 0 8px -8px rgb(0 0 0 / 12%); box-shadow: 8px 0 8px -8px rgb(0 0 0 / 12%);
:deep(.el-collapse) { :deep(.el-collapse) {
......
...@@ -96,11 +96,6 @@ const editorConfig = computed((): IEditorConfig => { ...@@ -96,11 +96,6 @@ const editorConfig = computed((): IEditorConfig => {
// 选择文件时的类型限制,默认为 ['image/*'] 。如不想限制,则设置为 [] // 选择文件时的类型限制,默认为 ['image/*'] 。如不想限制,则设置为 []
allowedFileTypes: ['image/*'], allowedFileTypes: ['image/*'],
// 自定义上传参数,例如传递验证的 token 等。参数会被添加到 formData 中,一起上传到服务端。
meta: { updateSupport: 0 },
// 将 meta 拼接到 url 参数中,默认 false
metaWithUrl: true,
// 自定义增加 http header // 自定义增加 http header
headers: { headers: {
Accept: '*', Accept: '*',
...@@ -108,9 +103,6 @@ const editorConfig = computed((): IEditorConfig => { ...@@ -108,9 +103,6 @@ const editorConfig = computed((): IEditorConfig => {
'tenant-id': getTenantId() 'tenant-id': getTenantId()
}, },
// 跨域是否传递 cookie ,默认为 false
withCredentials: true,
// 超时时间,默认为 10 秒 // 超时时间,默认为 10 秒
timeout: 5 * 1000, // 5 秒 timeout: 5 * 1000, // 5 秒
...@@ -119,7 +111,7 @@ const editorConfig = computed((): IEditorConfig => { ...@@ -119,7 +111,7 @@ const editorConfig = computed((): IEditorConfig => {
// 上传之前触发 // 上传之前触发
onBeforeUpload(file: File) { onBeforeUpload(file: File) {
console.log(file) // console.log(file)
return file return file
}, },
// 上传进度的回调函数 // 上传进度的回调函数
...@@ -142,6 +134,54 @@ const editorConfig = computed((): IEditorConfig => { ...@@ -142,6 +134,54 @@ const editorConfig = computed((): IEditorConfig => {
customInsert(res: any, insertFn: InsertFnType) { customInsert(res: any, insertFn: InsertFnType) {
insertFn(res.data, 'image', res.data) insertFn(res.data, 'image', res.data)
} }
},
['uploadVideo']: {
server: import.meta.env.VITE_UPLOAD_URL,
// 单个文件的最大体积限制,默认为 10M
maxFileSize: 10 * 1024 * 1024,
// 最多可上传几个文件,默认为 100
maxNumberOfFiles: 10,
// 选择文件时的类型限制,默认为 ['video/*'] 。如不想限制,则设置为 []
allowedFileTypes: ['video/*'],
// 自定义增加 http header
headers: {
Accept: '*',
Authorization: 'Bearer ' + getAccessToken(),
'tenant-id': getTenantId()
},
// 超时时间,默认为 30 秒
timeout: 15 * 1000, // 15 秒
// form-data fieldName,后端接口参数名称,默认值wangeditor-uploaded-image
fieldName: 'file',
// 上传之前触发
onBeforeUpload(file: File) {
// console.log(file)
return file
},
// 上传进度的回调函数
onProgress(progress: number) {
// progress 是 0-100 的数字
console.log('progress', progress)
},
onSuccess(file: File, res: any) {
console.log('onSuccess', file, res)
},
onFailed(file: File, res: any) {
alert(res.message)
console.log('onFailed', file, res)
},
onError(file: File, err: any, res: any) {
alert(err.message)
console.error('onError', file, err, res)
},
// 自定义插入图片
customInsert(res: any, insertFn: InsertFnType) {
insertFn(res.data, 'mp4', res.data)
}
} }
}, },
uploadImgShowBase64: true uploadImgShowBase64: true
......
<template> <template>
<div ref="contentRef" class="markdown-view" v-html="contentHtml"></div> <div ref="contentRef" class="markdown-view" v-html="renderedMarkdown"></div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {useClipboard} from "@vueuse/core"; import { useClipboard } from '@vueuse/core'
import MarkdownIt from 'markdown-it'
import {marked} from 'marked'
import 'highlight.js/styles/vs2015.min.css' import 'highlight.js/styles/vs2015.min.css'
import hljs from 'highlight.js' 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) {
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 style="position: relative;">${copyHtml}<code class="hljs">${highlightHtml}</code></pre>`
}
}
// 配置 marked
marked.use({
renderer: renderer
})
// 渲染的html内容
const contentHtml = ref<any>()
// 定义组件属性 // 定义组件属性
const props = defineProps({ const props = defineProps({
...@@ -47,39 +16,39 @@ const props = defineProps({ ...@@ -47,39 +16,39 @@ const props = defineProps({
} }
}) })
// 将 props 变为引用类型 const message = useMessage() // 消息弹窗
const { content } = toRefs(props) const { copy } = useClipboard() // 初始化 copy 到粘贴板
const contentRef = ref()
// 监听 content 变化 const md = new MarkdownIt({
watch(content, async (newValue, oldValue) => { highlight: function (str, lang) {
await renderMarkdown(newValue); if (lang && hljs.getLanguage(lang)) {
try {
const copyHtml = `<div id="copy" data-copy='${str}' style="position: absolute; right: 10px; top: 5px; color: #fff;cursor: pointer;">复制</div>`
return `<pre style="position: relative;">${copyHtml}<code class="hljs">${hljs.highlight(lang, str, true).value}</code></pre>`
} catch (__) {}
}
return ``
}
}) })
// 渲染 markdown /** 渲染 markdown */
const renderMarkdown = async (content: string) => { const renderedMarkdown = computed(() => {
contentHtml.value = await marked(content) return md.render(props.content)
} })
// 组件挂在时 /** 初始化 **/
onMounted(async () => { onMounted(async () => {
// 解析转换 markdown
await renderMarkdown(props.content as string);
//
// 添加 copy 监听 // 添加 copy 监听
contentRef.value.addEventListener('click', (e: any) => { contentRef.value.addEventListener('click', (e: any) => {
console.log(e)
if (e.target.id === 'copy') { if (e.target.id === 'copy') {
copy(e.target?.dataset?.copy) copy(e.target?.dataset?.copy)
ElMessage({ message.success('复制成功!')
message: '复制成功!',
type: 'success'
})
} }
}) })
}) })
</script> </script>
<style lang="scss"> <style lang="scss">
.markdown-view { .markdown-view {
font-family: PingFang SC; font-family: PingFang SC;
......
...@@ -32,6 +32,7 @@ ...@@ -32,6 +32,7 @@
格式为 <b style="color: #f56c6c">{{ fileType.join('/') }}</b> 的文件 格式为 <b style="color: #f56c6c">{{ fileType.join('/') }}</b> 的文件
</div> </div>
</template> </template>
<!-- TODO @puhui999:1)表单展示的时候,位置会偏掉,已发微信;2)disable 的时候,应该把【删除】按钮也隐藏掉? -->
<template #file="row"> <template #file="row">
<div class="flex items-center"> <div class="flex items-center">
<span>{{ row.file.name }}</span> <span>{{ row.file.name }}</span>
......
...@@ -129,7 +129,7 @@ const updateFlowType = (flowType) => { ...@@ -129,7 +129,7 @@ const updateFlowType = (flowType) => {
conditionExpression: null conditionExpression: null
}) })
bpmnInstances().modeling.updateProperties(toRaw(bpmnElementSource.value), { bpmnInstances().modeling.updateProperties(toRaw(bpmnElementSource.value), {
default: bpmnElement.value default: toRaw(bpmnElement.value)
}) })
return return
} }
......
...@@ -43,9 +43,6 @@ import { CommonStatusEnum } from '@/utils/constants' ...@@ -43,9 +43,6 @@ import { CommonStatusEnum } from '@/utils/constants'
/** BPM 流程 表单 */ /** BPM 流程 表单 */
defineOptions({ name: 'ProcessListenerDialog' }) defineOptions({ name: 'ProcessListenerDialog' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示 const dialogVisible = ref(false) // 弹窗的是否展示
const loading = ref(true) // 列表的加载中 const loading = ref(true) // 列表的加载中
const list = ref<ProcessListenerVO[]>([]) // 列表的数据 const list = ref<ProcessListenerVO[]>([]) // 列表的数据
...@@ -53,17 +50,23 @@ const total = ref(0) // 列表的总页数 ...@@ -53,17 +50,23 @@ const total = ref(0) // 列表的总页数
const queryParams = reactive({ const queryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
type: undefined, type: '',
status: CommonStatusEnum.ENABLE status: CommonStatusEnum.ENABLE
}) })
/** 打开弹窗 */ /** 打开弹窗 */
const open = async (type: string) => { const open = async (type: string) => {
queryParams.pageNo = 1
queryParams.type = type
getList()
dialogVisible.value = true dialogVisible.value = true
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 查询列表 */
const getList = async () => {
loading.value = true loading.value = true
try { try {
queryParams.pageNo = 1
queryParams.type = type
const data = await ProcessListenerApi.getProcessListenerPage(queryParams) const data = await ProcessListenerApi.getProcessListenerPage(queryParams)
list.value = data.list list.value = data.list
total.value = data.total total.value = data.total
...@@ -71,7 +74,6 @@ const open = async (type: string) => { ...@@ -71,7 +74,6 @@ const open = async (type: string) => {
loading.value = false loading.value = false
} }
} }
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */ /** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
......
...@@ -28,9 +28,6 @@ import { ProcessExpressionApi, ProcessExpressionVO } from '@/api/bpm/processExpr ...@@ -28,9 +28,6 @@ import { ProcessExpressionApi, ProcessExpressionVO } from '@/api/bpm/processExpr
/** BPM 流程 表单 */ /** BPM 流程 表单 */
defineOptions({ name: 'ProcessExpressionDialog' }) defineOptions({ name: 'ProcessExpressionDialog' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示 const dialogVisible = ref(false) // 弹窗的是否展示
const loading = ref(true) // 列表的加载中 const loading = ref(true) // 列表的加载中
const list = ref<ProcessExpressionVO[]>([]) // 列表的数据 const list = ref<ProcessExpressionVO[]>([]) // 列表的数据
...@@ -38,17 +35,23 @@ const total = ref(0) // 列表的总页数 ...@@ -38,17 +35,23 @@ const total = ref(0) // 列表的总页数
const queryParams = reactive({ const queryParams = reactive({
pageNo: 1, pageNo: 1,
pageSize: 10, pageSize: 10,
type: undefined, type: '',
status: CommonStatusEnum.ENABLE status: CommonStatusEnum.ENABLE
}) })
/** 打开弹窗 */ /** 打开弹窗 */
const open = async (type: string) => { const open = (type: string) => {
queryParams.pageNo = 1
queryParams.type = type
getList()
dialogVisible.value = true dialogVisible.value = true
}
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 查询列表 */
const getList = async () => {
loading.value = true loading.value = true
try { try {
queryParams.pageNo = 1
queryParams.type = type
const data = await ProcessExpressionApi.getProcessExpressionPage(queryParams) const data = await ProcessExpressionApi.getProcessExpressionPage(queryParams)
list.value = data.list list.value = data.list
total.value = data.total total.value = data.total
...@@ -56,7 +59,6 @@ const open = async (type: string) => { ...@@ -56,7 +59,6 @@ const open = async (type: string) => {
loading.value = false loading.value = false
} }
} }
defineExpose({ open }) // 提供 open 方法,用于打开弹窗
/** 提交表单 */ /** 提交表单 */
const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调
......
...@@ -135,6 +135,7 @@ import * as PostApi from '@/api/system/post' ...@@ -135,6 +135,7 @@ import * as PostApi from '@/api/system/post'
import * as UserApi from '@/api/system/user' import * as UserApi from '@/api/system/user'
import * as UserGroupApi from '@/api/bpm/userGroup' import * as UserGroupApi from '@/api/bpm/userGroup'
import ProcessExpressionDialog from './ProcessExpressionDialog.vue' import ProcessExpressionDialog from './ProcessExpressionDialog.vue'
import { ProcessExpressionVO } from '@/api/bpm/processExpression'
defineOptions({ name: 'UserTask' }) defineOptions({ name: 'UserTask' })
const props = defineProps({ const props = defineProps({
...@@ -197,8 +198,9 @@ const processExpressionDialogRef = ref() ...@@ -197,8 +198,9 @@ const processExpressionDialogRef = ref()
const openProcessExpressionDialog = async () => { const openProcessExpressionDialog = async () => {
processExpressionDialogRef.value.open() processExpressionDialogRef.value.open()
} }
const selectProcessExpression = (expression) => { const selectProcessExpression = (expression: ProcessExpressionVO) => {
userTaskForm.value.candidateParam = [expression.expression] userTaskForm.value.candidateParam = [expression.expression]
updateElementTask()
} }
watch( watch(
......
...@@ -81,7 +81,7 @@ service.interceptors.request.use( ...@@ -81,7 +81,7 @@ service.interceptors.request.use(
(error: AxiosError) => { (error: AxiosError) => {
// Do something with request error // Do something with request error
console.log(error) // for debug console.log(error) // for debug
Promise.reject(error) return Promise.reject(error)
} }
) )
...@@ -174,6 +174,7 @@ service.interceptors.response.use( ...@@ -174,6 +174,7 @@ service.interceptors.response.use(
if (msg === '无效的刷新令牌') { if (msg === '无效的刷新令牌') {
// hard coding:忽略这个提示,直接登出 // hard coding:忽略这个提示,直接登出
console.log(msg) console.log(msg)
return handleAuthorized()
} else { } else {
ElNotification.error({ title: msg }) ElNotification.error({ title: msg })
} }
......
...@@ -90,6 +90,11 @@ export default defineComponent({ ...@@ -90,6 +90,11 @@ export default defineComponent({
backgroundColor="var(--left-menu-bg-color)" backgroundColor="var(--left-menu-bg-color)"
textColor="var(--left-menu-text-color)" textColor="var(--left-menu-text-color)"
activeTextColor="var(--left-menu-text-active-color)" activeTextColor="var(--left-menu-text-active-color)"
popperClass={
unref(menuMode) === 'vertical'
? `${prefixCls}-popper--vertical`
: `${prefixCls}-popper--horizontal`
}
onSelect={menuSelect} onSelect={menuSelect}
> >
{{ {{
......
...@@ -52,7 +52,7 @@ async function goLogin() { ...@@ -52,7 +52,7 @@ async function goLogin() {
// 登出后清理 // 登出后清理
deleteUserCache() // 清空用户缓存 deleteUserCache() // 清空用户缓存
tagsViewStore.delAllViews() tagsViewStore.delAllViews()
resetRouter() // 重置静态路由表 // resetRouter() // 重置静态路由表
lockStore.resetLockInfo() lockStore.resetLockInfo()
replace('/login') replace('/login')
} }
......
...@@ -341,7 +341,7 @@ const remainingRouter: AppRouteRecordRaw[] = [ ...@@ -341,7 +341,7 @@ const remainingRouter: AppRouteRecordRaw[] = [
component: () => import('@/views/mall/product/spu/form/index.vue'), component: () => import('@/views/mall/product/spu/form/index.vue'),
name: 'ProductSpuAdd', name: 'ProductSpuAdd',
meta: { meta: {
noCache: true, noCache: false, // 需要缓存
hidden: true, hidden: true,
canTo: true, canTo: true,
icon: 'ep:edit', icon: 'ep:edit',
...@@ -573,6 +573,26 @@ const remainingRouter: AppRouteRecordRaw[] = [ ...@@ -573,6 +573,26 @@ const remainingRouter: AppRouteRecordRaw[] = [
component: () => import('@/views/crm/product/detail/index.vue') component: () => import('@/views/crm/product/detail/index.vue')
} }
] ]
},
{
path: '/ai',
component: Layout,
name: 'Ai',
meta: {
hidden: true
},
children: [
{
path: 'image/square',
component: () => import('@/views/ai/image/square/index.vue'),
name: 'AiImageSquare',
meta: {
title: '绘图作品',
icon: 'ep:home-filled',
noCache: false
}
}
]
} }
] ]
......
...@@ -109,6 +109,14 @@ export const PayChannelEnum = { ...@@ -109,6 +109,14 @@ export const PayChannelEnum = {
code: 'wx_app', code: 'wx_app',
name: '微信 APP 支付' name: '微信 APP 支付'
}, },
WX_NATIVE: {
code: 'wx_native',
name: '微信 Native 支付'
},
WX_WAP: {
code: 'wx_wap',
name: '微信 WAP 网站支付'
},
WX_BAR: { WX_BAR: {
code: 'wx_bar', code: 'wx_bar',
name: '微信条码支付' name: '微信条码支付'
......
...@@ -125,7 +125,6 @@ export enum DICT_TYPE { ...@@ -125,7 +125,6 @@ export enum DICT_TYPE {
SYSTEM_SMS_TEMPLATE_TYPE = 'system_sms_template_type', SYSTEM_SMS_TEMPLATE_TYPE = 'system_sms_template_type',
SYSTEM_SMS_SEND_STATUS = 'system_sms_send_status', SYSTEM_SMS_SEND_STATUS = 'system_sms_send_status',
SYSTEM_SMS_RECEIVE_STATUS = 'system_sms_receive_status', SYSTEM_SMS_RECEIVE_STATUS = 'system_sms_receive_status',
SYSTEM_ERROR_CODE_TYPE = 'system_error_code_type',
SYSTEM_OAUTH2_GRANT_TYPE = 'system_oauth2_grant_type', SYSTEM_OAUTH2_GRANT_TYPE = 'system_oauth2_grant_type',
SYSTEM_MAIL_SEND_STATUS = 'system_mail_send_status', SYSTEM_MAIL_SEND_STATUS = 'system_mail_send_status',
SYSTEM_NOTIFY_TEMPLATE_TYPE = 'system_notify_template_type', SYSTEM_NOTIFY_TEMPLATE_TYPE = 'system_notify_template_type',
...@@ -219,5 +218,13 @@ export enum DICT_TYPE { ...@@ -219,5 +218,13 @@ export enum DICT_TYPE {
ERP_STOCK_RECORD_BIZ_TYPE = 'erp_stock_record_biz_type', // 库存明细的业务类型 ERP_STOCK_RECORD_BIZ_TYPE = 'erp_stock_record_biz_type', // 库存明细的业务类型
// ========== AI - 人工智能模块 ========== // ========== 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 生成模式
AI_WRITE_TYPE = 'ai_write_type', // AI 写作类型
AI_WRITE_LENGTH = 'ai_write_length', // AI 写作长度
AI_WRITE_FORMAT = 'ai_write_format', // AI 写作格式
AI_WRITE_TONE = 'ai_write_tone', // AI 写作语气
AI_WRITE_LANGUAGE = 'ai_write_language' // AI 写作语言
} }
...@@ -29,9 +29,42 @@ const download = { ...@@ -29,9 +29,42 @@ const download = {
html: (data: Blob, fileName: string) => { html: (data: Blob, fileName: string) => {
download0(data, fileName, 'text/html') download0(data, fileName, 'text/html')
}, },
// 下载 MarkdownView 方法 // 下载 Markdown 方法
markdown: (data: Blob, fileName: string) => { markdown: (data: Blob, fileName: string) => {
download0(data, fileName, 'text/markdown') download0(data, fileName, 'text/markdown')
},
// 下载图片(允许跨域)
image: ({
url,
canvasWidth,
canvasHeight,
drawWithImageSize = true
}: {
url: string
canvasWidth?: number // 指定画布宽度
canvasHeight?: number // 指定画布高度
drawWithImageSize?: boolean // 将图片绘制在画布上时带上图片的宽高值, 默认是要带上的
}) => {
const image = new Image()
// image.setAttribute('crossOrigin', 'anonymous')
image.src = url
image.onload = () => {
const canvas = document.createElement('canvas')
canvas.width = canvasWidth || image.width
canvas.height = canvasHeight || image.height
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D
ctx?.clearRect(0, 0, canvas.width, canvas.height)
if (drawWithImageSize) {
ctx.drawImage(image, 0, 0, image.width, image.height)
} else {
ctx.drawImage(image, 0, 0)
}
const url = canvas.toDataURL('image/png')
const a = document.createElement('a')
a.href = url
a.download = 'image.png'
a.click()
}
} }
} }
......
...@@ -313,7 +313,7 @@ export const fenToYuan = (price: string | number): string => { ...@@ -313,7 +313,7 @@ export const fenToYuan = (price: string | number): string => {
*/ */
export const calculateRelativeRate = (value?: number, reference?: number) => { export const calculateRelativeRate = (value?: number, reference?: number) => {
// 防止除0 // 防止除0
if (!reference) return 0 if (!reference || reference == 0) return 0
return ((100 * ((value || 0) - reference)) / reference).toFixed(0) return ((100 * ((value || 0) - reference)) / reference).toFixed(0)
} }
......
...@@ -184,9 +184,9 @@ const loginData = reactive({ ...@@ -184,9 +184,9 @@ const loginData = reactive({
captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE, captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE,
tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE, tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
loginForm: { loginForm: {
tenantName: '芋道源码', tenantName: import.meta.env.VITE_APP_DEFAULT_LOGIN_TENANT || '',
username: 'admin', username: import.meta.env.VITE_APP_DEFAULT_LOGIN_USERNAME || '',
password: 'admin123', password: import.meta.env.VITE_APP_DEFAULT_LOGIN_PASSWORD || '',
captchaVerification: '', captchaVerification: '',
rememberMe: true // 默认记录我。如果不需要,可手动修改 rememberMe: true // 默认记录我。如果不需要,可手动修改
} }
......
...@@ -8,7 +8,12 @@ ...@@ -8,7 +8,12 @@
v-loading="formLoading" v-loading="formLoading"
> >
<el-form-item label="角色设定" prop="systemMessage"> <el-form-item label="角色设定" prop="systemMessage">
<el-input type="textarea" v-model="formData.systemMessage" rows="4" placeholder="请输入角色设定" /> <el-input
type="textarea"
v-model="formData.systemMessage"
rows="4"
placeholder="请输入角色设定"
/>
</el-form-item> </el-form-item>
<el-form-item label="模型" prop="modelId"> <el-form-item label="模型" prop="modelId">
<el-select v-model="formData.modelId" placeholder="请选择模型"> <el-select v-model="formData.modelId" placeholder="请选择模型">
...@@ -57,10 +62,9 @@ import { CommonStatusEnum } from '@/utils/constants' ...@@ -57,10 +62,9 @@ import { CommonStatusEnum } from '@/utils/constants'
import { ChatModelApi, ChatModelVO } from '@/api/ai/model/chatModel' import { ChatModelApi, ChatModelVO } from '@/api/ai/model/chatModel'
import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation' import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation'
/** AI 聊天角色 表单 */ /** AI 聊天对话的更新表单 */
defineOptions({ name: 'ChatConversationUpdateForm' }) defineOptions({ name: 'ChatConversationUpdateForm' })
const { t } = useI18n() // 国际化
const message = useMessage() // 消息弹窗 const message = useMessage() // 消息弹窗
const dialogVisible = ref(false) // 弹窗的是否展示 const dialogVisible = ref(false) // 弹窗的是否展示
......
<template> <template>
<div ref="messageContainer" style="height: 100%; overflow-y: auto; position: relative"> <div ref="messageContainer" class="h-100% overflow-y-auto relative">
<div class="chat-list" v-for="(item, index) in list" :key="index"> <div class="chat-list" v-for="(item, index) in list" :key="index">
<!-- 靠左 message --> <!-- 靠左 message:system、assistant 类型 -->
<div class="left-message message-item" v-if="item.type !== 'user'"> <div class="left-message message-item" v-if="item.type !== 'user'">
<div class="avatar"> <div class="avatar">
<el-avatar :src="roleAvatar" /> <el-avatar :src="roleAvatar" />
...@@ -14,16 +14,16 @@ ...@@ -14,16 +14,16 @@
<MarkdownView class="left-text" :content="item.content" /> <MarkdownView class="left-text" :content="item.content" />
</div> </div>
<div class="left-btns"> <div class="left-btns">
<el-button class="btn-cus" link @click="noCopy(item.content)"> <el-button class="btn-cus" link @click="copyContent(item.content)">
<img class="btn-image" src="@/assets/ai/copy.svg" /> <img class="btn-image" src="@/assets/ai/copy.svg" />
</el-button> </el-button>
<el-button class="btn-cus" link @click="onDelete(item.id)"> <el-button v-if="item.id > 0" class="btn-cus" link @click="onDelete(item.id)">
<img class="btn-image" src="@/assets/ai/delete.svg" style="height: 17px" /> <img class="btn-image h-17px" src="@/assets/ai/delete.svg" />
</el-button> </el-button>
</div> </div>
</div> </div>
</div> </div>
<!-- 靠右 message --> <!-- 靠右 message:user 类型 -->
<div class="right-message message-item" v-if="item.type === 'user'"> <div class="right-message message-item" v-if="item.type === 'user'">
<div class="avatar"> <div class="avatar">
<el-avatar :src="userAvatar" /> <el-avatar :src="userAvatar" />
...@@ -36,15 +36,11 @@ ...@@ -36,15 +36,11 @@
<div class="right-text">{{ item.content }}</div> <div class="right-text">{{ item.content }}</div>
</div> </div>
<div class="right-btns"> <div class="right-btns">
<el-button class="btn-cus" link @click="noCopy(item.content)"> <el-button class="btn-cus" link @click="copyContent(item.content)">
<img class="btn-image" src="@/assets/ai/copy.svg" /> <img class="btn-image" src="@/assets/ai/copy.svg" />
</el-button> </el-button>
<el-button class="btn-cus" link @click="onDelete(item.id)"> <el-button class="btn-cus" link @click="onDelete(item.id)">
<img <img class="btn-image h-17px mr-12px" src="@/assets/ai/delete.svg" />
class="btn-image"
src="@/assets/ai/delete.svg"
style="height: 17px; margin-right: 12px"
/>
</el-button> </el-button>
<el-button class="btn-cus" link @click="onRefresh(item)"> <el-button class="btn-cus" link @click="onRefresh(item)">
<el-icon size="17"><RefreshRight /></el-icon> <el-icon size="17"><RefreshRight /></el-icon>
...@@ -63,23 +59,25 @@ ...@@ -63,23 +59,25 @@
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { PropType } from 'vue'
import { formatDate } from '@/utils/formatTime' import { formatDate } from '@/utils/formatTime'
import MarkdownView from '@/components/MarkdownView/index.vue' import MarkdownView from '@/components/MarkdownView/index.vue'
import { ChatMessageApi, ChatMessageVO } from '@/api/ai/chat/message'
import { useClipboard } from '@vueuse/core' import { useClipboard } from '@vueuse/core'
import { PropType } from 'vue'
import { ArrowDownBold, Edit, RefreshRight } from '@element-plus/icons-vue' import { ArrowDownBold, Edit, RefreshRight } from '@element-plus/icons-vue'
import { ChatMessageApi, ChatMessageVO } from '@/api/ai/chat/message'
import { ChatConversationVO } from '@/api/ai/chat/conversation' import { ChatConversationVO } from '@/api/ai/chat/conversation'
import {useUserStore} from '@/store/modules/user'; import { useUserStore } from '@/store/modules/user'
import userAvatarDefaultImg from '@/assets/imgs/avatar.gif' import userAvatarDefaultImg from '@/assets/imgs/avatar.gif'
import roleAvatarDefaultImg from '@/assets/ai/gpt.svg' import roleAvatarDefaultImg from '@/assets/ai/gpt.svg'
const message = useMessage() // 消息弹窗
const { copy } = useClipboard() // 初始化 copy 到粘贴板 const { copy } = useClipboard() // 初始化 copy 到粘贴板
// 判断 消息列表 滚动的位置(用于判断是否需要滚动到消息最下方) const userStore = useUserStore()
// 判断“消息列表”滚动的位置(用于判断是否需要滚动到消息最下方)
const messageContainer: any = ref(null) const messageContainer: any = ref(null)
const isScrolling = ref(false) //用于判断用户是否在滚动 const isScrolling = ref(false) //用于判断用户是否在滚动
const userStore = useUserStore()
const userAvatar = computed(() => userStore.user.avatar ?? userAvatarDefaultImg) const userAvatar = computed(() => userStore.user.avatar ?? userAvatarDefaultImg)
const roleAvatar = computed(() => props.conversation.roleAvatar ?? roleAvatarDefaultImg) const roleAvatar = computed(() => props.conversation.roleAvatar ?? roleAvatarDefaultImg)
...@@ -95,17 +93,20 @@ const props = defineProps({ ...@@ -95,17 +93,20 @@ const props = defineProps({
} }
}) })
const { list } = toRefs(props) // 消息列表
const emits = defineEmits(['onDeleteSuccess', 'onRefresh', 'onEdit']) // 定义 emits
// ============ 处理对话滚动 ============== // ============ 处理对话滚动 ==============
/** 滚动到底部 */
const scrollToBottom = async (isIgnore?: boolean) => { const scrollToBottom = async (isIgnore?: boolean) => {
await nextTick(() => { // 注意要使用 nextTick 以免获取不到 dom
// TODO @fan:中文写作习惯,中英文之间要有空格;另外,nextick 哈,idea 如果有绿色波兰线,可以关注下 await nextTick()
//注意要使用nexttick以免获取不到dom if (isIgnore || !isScrolling.value) {
if (isIgnore || !isScrolling.value) { messageContainer.value.scrollTop =
messageContainer.value.scrollTop = messageContainer.value.scrollHeight - messageContainer.value.offsetHeight
messageContainer.value.scrollHeight - messageContainer.value.offsetHeight }
}
})
} }
function handleScroll() { function handleScroll() {
...@@ -122,75 +123,48 @@ function handleScroll() { ...@@ -122,75 +123,48 @@ function handleScroll() {
} }
} }
/** /** 回到底部 */
* 复制 const handleGoBottom = async () => {
*/ const scrollContainer = messageContainer.value
const noCopy = async (content) => { scrollContainer.scrollTop = scrollContainer.scrollHeight
copy(content) }
ElMessage({
message: '复制成功!', /** 回到顶部 */
type: 'success' const handlerGoTop = async () => {
}) const scrollContainer = messageContainer.value
scrollContainer.scrollTop = 0
} }
/** defineExpose({ scrollToBottom, handlerGoTop }) // 提供方法给 parent 调用
* 删除
*/ // ============ 处理消息操作 ==============
/** 复制 */
const copyContent = async (content) => {
await copy(content)
message.success('复制成功!')
}
/** 删除 */
const onDelete = async (id) => { const onDelete = async (id) => {
// 删除 message // 删除 message
await ChatMessageApi.delete(id) await ChatMessageApi.deleteChatMessage(id)
ElMessage({ message.success('删除成功!')
message: '删除成功!',
type: 'success'
})
// 回调 // 回调
emits('onDeleteSuccess') emits('onDeleteSuccess')
} }
/** /** 刷新 */
* 刷新
*/
const onRefresh = async (message: ChatMessageVO) => { const onRefresh = async (message: ChatMessageVO) => {
emits('onRefresh', message) emits('onRefresh', message)
} }
/** /** 编辑 */
* 编辑
*/
const onEdit = async (message: ChatMessageVO) => { const onEdit = async (message: ChatMessageVO) => {
emits('onEdit', message) 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 () => { onMounted(async () => {
messageContainer.value.addEventListener('scroll', handleScroll) messageContainer.value.addEventListener('scroll', handleScroll)
}) })
...@@ -199,15 +173,7 @@ onMounted(async () => { ...@@ -199,15 +173,7 @@ onMounted(async () => {
<style scoped lang="scss"> <style scoped lang="scss">
.message-container { .message-container {
position: relative; position: relative;
//top: 0;
//bottom: 0;
//left: 0;
//right: 0;
//width: 100%;
//height: 100%;
overflow-y: scroll; overflow-y: scroll;
//padding: 0 15px;
//z-index: -1;
} }
// 中间 // 中间
...@@ -231,11 +197,6 @@ onMounted(async () => { ...@@ -231,11 +197,6 @@ onMounted(async () => {
justify-content: flex-start; justify-content: flex-start;
} }
.avatar {
//height: 170px;
//width: 170px;
}
.message { .message {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
...@@ -272,7 +233,6 @@ onMounted(async () => { ...@@ -272,7 +233,6 @@ onMounted(async () => {
color: #fff; color: #fff;
display: inline; display: inline;
background-color: #267fff; background-color: #267fff;
color: #fff;
box-shadow: 0 0 0 1px #267fff; box-shadow: 0 0 0 1px #267fff;
border-radius: 10px; border-radius: 10px;
padding: 10px; padding: 10px;
......
<!-- 消息列表为空时,展示 prompt 列表 -->
<template> <template>
<div class="chat-empty"> <div class="chat-empty">
<!-- title -->
<!-- title -->
<div class="center-container"> <div class="center-container">
<div class="title"> AI</div> <div class="title"> AI</div>
<div class="role-list"> <div class="role-list">
<div class="role-item" v-for="prompt in promptList" :key="prompt.prompt" @click="handlerPromptClick(prompt)"> <div
{{prompt.prompt}} class="role-item"
v-for="prompt in promptList"
:key="prompt.prompt"
@click="handlerPromptClick(prompt)"
>
{{ prompt.prompt }}
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const promptList = [
const promptList = ref<any[]>() // 角色列表
promptList.value = [
{ {
"prompt": "今天气怎么样?", prompt: '今天气怎么样?'
}, },
{ {
"prompt": "写一首好听的诗歌?", prompt: '写一首好听的诗歌?'
} }
] ] // prompt 列表
const emits = defineEmits(['onPrompt']) const emits = defineEmits(['onPrompt'])
/** 选中 prompt 点击 */
const handlerPromptClick = async ({ prompt }) => { const handlerPromptClick = async ({ prompt }) => {
emits('onPrompt', prompt) emits('onPrompt', prompt)
} }
......
<!-- message 新增对话 --> <!-- 无聊天对话时,在 message 区域,可以新增对话 -->
<template> <template>
<div class="new-chat" > <div class="new-chat">
<div class="box-center"> <div class="box-center">
<div class="tip">点击下方按钮,开始你的对话吧</div> <div class="tip">点击下方按钮,开始你的对话吧</div>
<div class="btns"><el-button type="primary" round @click="handlerNewChat">新建对话</el-button></div> <div class="btns">
<el-button type="primary" round @click="handlerNewChat">新建对话</el-button>
</div>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
const emits = defineEmits(['onNewConversation'])
// 定义钩子 /** 新建 conversation 聊天对话 */
const emits = defineEmits(['onNewChat']) const handlerNewChat = () => {
emits('onNewConversation')
/**
* 新建 chat
*/
const handlerNewChat = async () => {
await emits('onNewChat')
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.new-chat { .new-chat {
......
<template> <template>
<div class="category-list"> <div class="category-list">
<div class="category" v-for="(category) in categoryList" :key="category"> <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
<el-button plain round size="small" v-else type="primary" @click="handleCategoryClick(category)">{{ category }}</el-button> plain
round
size="small"
:type="category === active ? 'primary' : ''"
@click="handleCategoryClick(category)"
>
{{ category }}
</el-button>
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {PropType} from "vue"; import { PropType } from 'vue'
// 定义属性 // 定义属性
defineProps({ defineProps({
...@@ -25,11 +32,10 @@ defineProps({ ...@@ -25,11 +32,10 @@ defineProps({
// 定义回调 // 定义回调
const emits = defineEmits(['onCategoryClick']) const emits = defineEmits(['onCategoryClick'])
// 处理分类点击事件 /** 处理分类点击事件 */
const handleCategoryClick = async (category) => { const handleCategoryClick = async (category: string) => {
emits('onCategoryClick', category) emits('onCategoryClick', category)
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.category-list { .category-list {
......
<template> <template>
<div class="card-list" ref="tabsRef" @scroll="handleTabsScroll"> <div class="card-list" ref="tabsRef" @scroll="handleTabsScroll">
<div class="card-item" v-for="role in roleList" :key="role.id"> <div class="card-item" v-for="role in roleList" :key="role.id">
<el-card class="card" body-class="card-body"> <el-card class="card" body-class="card-body">
<!-- 更多 --> <!-- 更多操作 -->
<div class="more-container" v-if="showMore"> <div class="more-container" v-if="showMore">
<el-dropdown @command="handleMoreClick"> <el-dropdown @command="handleMoreClick">
<span class="el-dropdown-link"> <span class="el-dropdown-link">
<el-button type="text" > <el-button type="text">
<el-icon><More /></el-icon> <el-icon><More /></el-icon>
</el-button> </el-button>
</span> </span>
<!-- TODO @fan:下面两个 icon,可以使用类似 <Icon icon="ep:question-filled" /> 替代哈 -->
<template #dropdown> <template #dropdown>
<el-dropdown-menu> <el-dropdown-menu>
<el-dropdown-item :command="['edit', role]" > <el-dropdown-item :command="['edit', role]">
<el-icon><EditPen /></el-icon>编辑 <Icon icon="ep:edit" color="#787878" />编辑
</el-dropdown-item> </el-dropdown-item>
<el-dropdown-item :command="['delete', role]" style="color: red;" > <el-dropdown-item :command="['delete', role]" style="color: red">
<el-icon><Delete /></el-icon> <Icon icon="ep:delete" color="red" />删除
<span>删除</span>
</el-dropdown-item> </el-dropdown-item>
</el-dropdown-menu> </el-dropdown-menu>
</template> </template>
</el-dropdown> </el-dropdown>
</div> </div>
<!-- 头像 --> <!-- 角色信息 -->
<div> <div>
<img class="avatar" :src="role.avatar"/> <img class="avatar" :src="role.avatar" />
</div> </div>
<div class="right-container"> <div class="right-container">
<div class="content-container"> <div class="content-container">
...@@ -44,8 +42,8 @@ ...@@ -44,8 +42,8 @@
<script setup lang="ts"> <script setup lang="ts">
import {ChatRoleVO} from '@/api/ai/model/chatRole' import {ChatRoleVO} from '@/api/ai/model/chatRole'
import {PropType, ref} from "vue"; import {PropType, ref} from 'vue'
import {Delete, EditPen, More} from "@element-plus/icons-vue"; import {More} from '@element-plus/icons-vue'
const tabsRef = ref<any>() // tabs ref const tabsRef = ref<any>() // tabs ref
...@@ -65,10 +63,11 @@ const props = defineProps({ ...@@ -65,10 +63,11 @@ const props = defineProps({
default: false default: false
} }
}) })
// 定义钩子 // 定义钩子
const emits = defineEmits(['onDelete', 'onEdit', 'onUse', 'onPage']) const emits = defineEmits(['onDelete', 'onEdit', 'onUse', 'onPage'])
// more 点击 /** 操作:编辑、删除 */
const handleMoreClick = async (data) => { const handleMoreClick = async (data) => {
const type = data[0] const type = data[0]
const role = data[1] const role = data[1]
...@@ -79,28 +78,20 @@ const handleMoreClick = async (data) => { ...@@ -79,28 +78,20 @@ const handleMoreClick = async (data) => {
} }
} }
// 使用 /** 选中 */
const handleUseClick = (role) => { const handleUseClick = (role) => {
emits('onUse', role) emits('onUse', role)
} }
/** 滚动 */
const handleTabsScroll = async () => { const handleTabsScroll = async () => {
if (tabsRef.value) { if (tabsRef.value) {
const { scrollTop, scrollHeight, clientHeight } = tabsRef.value; const { scrollTop, scrollHeight, clientHeight } = tabsRef.value
console.log('scrollTop', scrollTop)
if (scrollTop + clientHeight >= scrollHeight - 20 && !props.loading) { if (scrollTop + clientHeight >= scrollHeight - 20 && !props.loading) {
console.log('分页')
// page.value++;
// fetchData(page.value);
await emits('onPage') await emits('onPage')
} }
} }
} }
onMounted(() => {
console.log('props', props.roleList)
})
</script> </script>
<style lang="scss"> <style lang="scss">
...@@ -114,11 +105,9 @@ onMounted(() => { ...@@ -114,11 +105,9 @@ onMounted(() => {
flex-direction: row; flex-direction: row;
justify-content: flex-start; justify-content: flex-start;
position: relative; position: relative;
} }
</style> </style>
<style scoped lang="scss"> <style scoped lang="scss">
// 卡片列表 // 卡片列表
.card-list { .card-list {
display: flex; display: flex;
...@@ -180,9 +169,6 @@ onMounted(() => { ...@@ -180,9 +169,6 @@ onMounted(() => {
margin-top: 2px; margin-top: 2px;
} }
} }
} }
} }
</style> </style>
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
<template> <template>
<el-container class="role-container"> <el-container class="role-container">
<ChatRoleForm ref="formRef" @success="handlerAddRoleSuccess" /> <ChatRoleForm ref="formRef" @success="handlerAddRoleSuccess" />
<!-- header --> <!-- header -->
<Header title="角色仓库" style="position: relative" /> <RoleHeader title="角色仓库" class="relative" />
<!-- main --> <!-- main -->
<el-main class="role-main"> <el-main class="role-main">
<div class="search-container"> <div class="search-container">
...@@ -18,20 +18,17 @@ ...@@ -18,20 +18,17 @@
@change="getActiveTabsRole" @change="getActiveTabsRole"
/> />
<el-button <el-button
v-if="activeRole == 'my-role'" v-if="activeTab == 'my-role'"
type="primary" type="primary"
@click="handlerAddRole" @click="handlerAddRole"
style="margin-left: 20px" class="ml-20px"
> >
<!-- TODO @fan:下面两个 icon,可以使用类似 <Icon icon="ep:question-filled" /> 替代哈 --> <Icon icon="ep:user" style="margin-right: 5px;" />
<el-icon>
<User />
</el-icon>
添加角色 添加角色
</el-button> </el-button>
</div> </div>
<!-- tabs --> <!-- tabs -->
<el-tabs v-model="activeRole" class="tabs" @tab-click="handleTabsClick"> <el-tabs v-model="activeTab" class="tabs" @tab-click="handleTabsClick">
<el-tab-pane class="role-pane" label="我的角色" name="my-role"> <el-tab-pane class="role-pane" label="我的角色" name="my-role">
<RoleList <RoleList
:loading="loading" :loading="loading"
...@@ -41,7 +38,7 @@ ...@@ -41,7 +38,7 @@
@on-edit="handlerCardEdit" @on-edit="handlerCardEdit"
@on-use="handlerCardUse" @on-use="handlerCardUse"
@on-page="handlerCardPage('my')" @on-page="handlerCardPage('my')"
style="margin-top: 20px" class="mt-20px"
/> />
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="公共角色" name="public-role"> <el-tab-pane label="公共角色" name="public-role">
...@@ -57,7 +54,7 @@ ...@@ -57,7 +54,7 @@
@on-edit="handlerCardEdit" @on-edit="handlerCardEdit"
@on-use="handlerCardUse" @on-use="handlerCardUse"
@on-page="handlerCardPage('public')" @on-page="handlerCardPage('public')"
style="margin-top: 20px" class="mt-20px"
loading loading
/> />
</el-tab-pane> </el-tab-pane>
...@@ -67,28 +64,31 @@ ...@@ -67,28 +64,31 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import {ref} from 'vue'
import Header from '@/views/ai/chat/components/Header.vue' import RoleHeader from './RoleHeader.vue'
import RoleList from './RoleList.vue' import RoleList from './RoleList.vue'
import ChatRoleForm from '@/views/ai/model/chatRole/ChatRoleForm.vue' import ChatRoleForm from '@/views/ai/model/chatRole/ChatRoleForm.vue'
import RoleCategoryList from './RoleCategoryList.vue' import RoleCategoryList from './RoleCategoryList.vue'
import { ChatRoleApi, ChatRolePageReqVO, ChatRoleVO } from '@/api/ai/model/chatRole' import {ChatRoleApi, ChatRolePageReqVO, ChatRoleVO} from '@/api/ai/model/chatRole'
import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation' import {ChatConversationApi, ChatConversationVO} from '@/api/ai/chat/conversation'
import { TabsPaneContext } from 'element-plus' import {Search} from '@element-plus/icons-vue'
import { Search, User } from '@element-plus/icons-vue' import {TabsPaneContext} from 'element-plus'
const router = useRouter() // 路由对象 const router = useRouter() // 路由对象
// 属性定义 // 属性定义
const loading = ref<boolean>(false) // 加载中 const loading = ref<boolean>(false) // 加载中
const activeRole = ref<string>('my-role') // 选中的角色 TODO @fan:是不是叫 activeTab 会更明确一点哈。选中的角色,会以为是某个角色 const activeTab = ref<string>('my-role') // 选中的角色 Tab
const search = ref<string>('') // 加载中 const search = ref<string>('') // 加载中
// TODO @fan:要不 myPage、pubPage,搞成类似 const queryParams = reactive({ ,分别搞成两个大的参数哈? const myRoleParams = reactive({
const myPageNo = ref<number>(1) // my 分页下标 pageNo: 1,
const myPageSize = ref<number>(50) // my 分页大小 pageSize: 50
})
const myRoleList = ref<ChatRoleVO[]>([]) // my 分页大小 const myRoleList = ref<ChatRoleVO[]>([]) // my 分页大小
const publicPageNo = ref<number>(1) // public 分页下标 const publicRoleParams = reactive({
const publicPageSize = ref<number>(50) // public 分页大小 pageNo: 1,
pageSize: 50
})
const publicRoleList = ref<ChatRoleVO[]>([]) // public 分页大小 const publicRoleList = ref<ChatRoleVO[]>([]) // public 分页大小
const activeCategory = ref<string>('全部') // 选择中的分类 const activeCategory = ref<string>('全部') // 选择中的分类
const categoryList = ref<string[]>([]) // 角色分类类别 const categoryList = ref<string[]>([]) // 角色分类类别
...@@ -96,7 +96,7 @@ const categoryList = ref<string[]>([]) // 角色分类类别 ...@@ -96,7 +96,7 @@ const categoryList = ref<string[]>([]) // 角色分类类别
/** tabs 点击 */ /** tabs 点击 */
const handleTabsClick = async (tab: TabsPaneContext) => { const handleTabsClick = async (tab: TabsPaneContext) => {
// 设置切换状态 // 设置切换状态
activeRole.value = tab.paneName + '' activeTab.value = tab.paneName + ''
// 切换的时候重新加载数据 // 切换的时候重新加载数据
await getActiveTabsRole() await getActiveTabsRole()
} }
...@@ -104,12 +104,11 @@ const handleTabsClick = async (tab: TabsPaneContext) => { ...@@ -104,12 +104,11 @@ const handleTabsClick = async (tab: TabsPaneContext) => {
/** 获取 my role 我的角色 */ /** 获取 my role 我的角色 */
const getMyRole = async (append?: boolean) => { const getMyRole = async (append?: boolean) => {
const params: ChatRolePageReqVO = { const params: ChatRolePageReqVO = {
pageNo: myPageNo.value, ...myRoleParams,
pageSize: myPageSize.value,
name: search.value, name: search.value,
publicStatus: false publicStatus: false
} }
const { total, list } = await ChatRoleApi.getMyPage(params) const { list } = await ChatRoleApi.getMyPage(params)
if (append) { if (append) {
myRoleList.value.push.apply(myRoleList.value, list) myRoleList.value.push.apply(myRoleList.value, list)
} else { } else {
...@@ -120,8 +119,7 @@ const getMyRole = async (append?: boolean) => { ...@@ -120,8 +119,7 @@ const getMyRole = async (append?: boolean) => {
/** 获取 public role 公共角色 */ /** 获取 public role 公共角色 */
const getPublicRole = async (append?: boolean) => { const getPublicRole = async (append?: boolean) => {
const params: ChatRolePageReqVO = { const params: ChatRolePageReqVO = {
pageNo: publicPageNo.value, ...publicRoleParams,
pageSize: publicPageSize.value,
category: activeCategory.value === '全部' ? '' : activeCategory.value, category: activeCategory.value === '全部' ? '' : activeCategory.value,
name: search.value, name: search.value,
publicStatus: true publicStatus: true
...@@ -136,20 +134,18 @@ const getPublicRole = async (append?: boolean) => { ...@@ -136,20 +134,18 @@ const getPublicRole = async (append?: boolean) => {
/** 获取选中的 tabs 角色 */ /** 获取选中的 tabs 角色 */
const getActiveTabsRole = async () => { const getActiveTabsRole = async () => {
if (activeRole.value === 'my-role') { if (activeTab.value === 'my-role') {
myPageNo.value = 1 myRoleParams.pageNo = 1
await getMyRole() await getMyRole()
} else { } else {
publicPageNo.value = 1 publicRoleParams.pageNo = 1
await getPublicRole() await getPublicRole()
} }
} }
/** 获取角色分类列表 */ /** 获取角色分类列表 */
const getRoleCategoryList = async () => { const getRoleCategoryList = async () => {
const res = await ChatRoleApi.getCategoryList() categoryList.value = ['全部', ...(await ChatRoleApi.getCategoryList())]
const defaultRole = ['全部']
categoryList.value = [...defaultRole, ...res]
} }
/** 处理分类点击 */ /** 处理分类点击 */
...@@ -165,6 +161,10 @@ const formRef = ref() ...@@ -165,6 +161,10 @@ const formRef = ref()
const handlerAddRole = async () => { const handlerAddRole = async () => {
formRef.value.open('my-create', null, '添加角色') formRef.value.open('my-create', null, '添加角色')
} }
/** 编辑角色 */
const handlerCardEdit = async (role) => {
formRef.value.open('my-update', role.id, '编辑角色')
}
/** 添加角色成功 */ /** 添加角色成功 */
const handlerAddRoleSuccess = async (e) => { const handlerAddRoleSuccess = async (e) => {
...@@ -172,28 +172,22 @@ const handlerAddRoleSuccess = async (e) => { ...@@ -172,28 +172,22 @@ const handlerAddRoleSuccess = async (e) => {
await getActiveTabsRole() await getActiveTabsRole()
} }
// card 删除 /** 删除角色 */
const handlerCardDelete = async (role) => { const handlerCardDelete = async (role) => {
await ChatRoleApi.deleteMy(role.id) await ChatRoleApi.deleteMy(role.id)
// 刷新数据 // 刷新数据
await getActiveTabsRole() await getActiveTabsRole()
} }
// card 编辑 /** 角色分页:获取下一页 */
const handlerCardEdit = async (role) => {
formRef.value.open('my-update', role.id, '编辑角色')
}
/** card 分页:获取下一页 */
const handlerCardPage = async (type) => { const handlerCardPage = async (type) => {
console.log('handlerCardPage', type)
try { try {
loading.value = true loading.value = true
if (type === 'public') { if (type === 'public') {
publicPageNo.value++ publicRoleParams.pageNo++
await getPublicRole(true) await getPublicRole(true)
} else { } else {
myPageNo.value++ myRoleParams.pageNo++
await getMyRole(true) await getMyRole(true)
} }
} finally { } finally {
...@@ -208,10 +202,10 @@ const handlerCardUse = async (role) => { ...@@ -208,10 +202,10 @@ const handlerCardUse = async (role) => {
roleId: role.id roleId: role.id
} as unknown as ChatConversationVO } as unknown as ChatConversationVO
const conversationId = await ChatConversationApi.createChatConversationMy(data) const conversationId = await ChatConversationApi.createChatConversationMy(data)
// 2. 跳转页面 // 2. 跳转页面
// TODO @fan:最好用 name,后续可能会改~~~
await router.push({ await router.push({
path: `/ai/chat`, name: 'AiChat',
query: { query: {
conversationId: conversationId conversationId: conversationId
} }
...@@ -225,15 +219,14 @@ onMounted(async () => { ...@@ -225,15 +219,14 @@ onMounted(async () => {
// 获取 role 数据 // 获取 role 数据
await getActiveTabsRole() await getActiveTabsRole()
}) })
// TODO @fan:css 是不是可以融合到 scss 里面呀?
</script> </script>
<style lang="css"> <!-- 覆盖 element ui css -->
<style lang="scss">
.el-tabs__content { .el-tabs__content {
position: relative; position: relative;
height: 100%; height: 100%;
overflow: hidden; overflow: hidden;
} }
.el-tabs__nav-scroll { .el-tabs__nav-scroll {
margin: 10px 20px; margin: 10px 20px;
} }
......
...@@ -16,5 +16,5 @@ import ChatConversationList from './ChatConversationList.vue' ...@@ -16,5 +16,5 @@ import ChatConversationList from './ChatConversationList.vue'
import ChatMessageList from './ChatMessageList.vue' import ChatMessageList from './ChatMessageList.vue'
/** AI 聊天对话 列表 */ /** AI 聊天对话 列表 */
defineOptions({ name: 'ChatConversation' }) defineOptions({ name: 'AiChatManager' })
</script> </script>
<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, ImageDetailVO} from '@/api/ai/image';
import ImageTaskCard from './ImageTaskCard.vue';
const showDrawer = ref<boolean>(false) // 是否显示
const imageDetail = ref<ImageDetailVO>({} as ImageDetailVO) // 图片详细信息
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.getImageDetail(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>
<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, ImageDetailVO, 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<ImageDetailVO[]>([]) // 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.getImageList({pageNo: pageNo.value, pageSize: pageSize.value})
if (apply) {
imageList.value = [...imageList.value, ...list]
} else {
imageList.value = list
}
pageTotal.value = total
} finally {
if (imageTaskLoadingInstance.value) {
imageTaskLoadingInstance.value.close();
imageTaskLoadingInstance.value = null;
}
}
}
/** 图片 - btn click */
const handlerImageBtnClick = async (type, imageDetail: ImageDetailVO) => {
// 获取 image detail id
showImageDetailId.value = imageDetail.id
console.log('type', imageDetail.id)
// 处理不用 btn
if (type === 'more') {
await handlerDrawerOpen()
} else if (type === 'delete') {
await message.confirm(`是否删除照片?`)
await ImageApi.deleteImage(imageDetail.id)
await getImageList()
await message.success("删除成功!")
} else if (type === 'download') {
await downloadImage(imageDetail.picUrl)
}
}
/** 图片 - mj btn click */
const handlerImageMjBtnClick = async (button: ImageMjButtonsVO, imageDetail: ImageDetailVO) => {
// 1、构建 params 参数
const params = {
id: imageDetail.id,
customId: button.customId,
} as ImageMjActionVO
// 2、发送 action
await ImageApi.midjourneyAction(params)
// 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 getImageList(false)
}, 1000 * 20)
})
/** 组件取消挂在的时候 */
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>
<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 {ImageDetailVO, 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<ImageDetailVO>,
require: true
}
})
/** 按钮 - 点击事件 */
const handlerBtnClick = async (type, imageDetail: ImageDetailVO) => {
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>
<template>
<el-card body-class="" class="image-card">
<div class="image-operation">
<div>
<el-button type="primary" text bg v-if="detail?.status === AiImageStatusEnum.IN_PROGRESS">
生成中
</el-button>
<el-button text bg v-else-if="detail?.status === AiImageStatusEnum.SUCCESS">
已完成
</el-button>
<el-button type="danger" text bg v-else-if="detail?.status === AiImageStatusEnum.FAIL">
异常
</el-button>
</div>
<!-- 操作区 -->
<div>
<el-button
class="btn"
text
:icon="Download"
@click="handleButtonClick('download', detail)"
/>
<el-button
class="btn"
text
:icon="RefreshRight"
@click="handleButtonClick('regeneration', detail)"
/>
<el-button class="btn" text :icon="Delete" @click="handleButtonClick('delete', detail)" />
<el-button class="btn" text :icon="More" @click="handleButtonClick('more', detail)" />
</div>
</div>
<div class="image-wrapper" ref="cardImageRef">
<el-image
class="image"
:src="detail?.picUrl"
:preview-src-list="[detail.picUrl]"
preview-teleported
/>
<div v-if="detail?.status === AiImageStatusEnum.FAIL">
{{ detail?.errorMessage }}
</div>
</div>
<!-- Midjourney 专属操作 -->
<div class="image-mj-btns">
<el-button
size="small"
v-for="button in detail?.buttons"
:key="button"
class="min-w-40px ml-0 mr-10px mt-5px"
@click="handleMidjourneyBtnClick(button)"
>
{{ button.label }}{{ button.emoji }}
</el-button>
</div>
</el-card>
</template>
<script setup lang="ts">
import { Delete, Download, More, RefreshRight } from '@element-plus/icons-vue'
import { ImageVO, ImageMidjourneyButtonsVO } from '@/api/ai/image'
import { PropType } from 'vue'
import { ElLoading, LoadingOptionsResolved } from 'element-plus'
import { AiImageStatusEnum } from '@/views/ai/utils/constants'
const message = useMessage() // 消息
const props = defineProps({
detail: {
type: Object as PropType<ImageVO>,
require: true
}
})
const cardImageRef = ref<any>() // 卡片 image ref
const cardImageLoadingInstance = ref<any>() // 卡片 image ref
/** 处理点击事件 */
const handleButtonClick = async (type, detail: ImageVO) => {
emits('onBtnClick', type, detail)
}
/** 处理 Midjourney 按钮点击事件 */
const handleMidjourneyBtnClick = async (button: ImageMidjourneyButtonsVO) => {
// 确认窗体
await message.confirm(`确认操作 "${button.label} ${button.emoji}" ?`)
emits('onMjBtnClick', button, props.detail)
}
const emits = defineEmits(['onBtnClick', 'onMjBtnClick']) // emits
/** 监听详情 */
const { detail } = toRefs(props)
watch(detail, async (newVal, oldVal) => {
await handleLoading(newVal.status as string)
})
/** 处理加载状态 */
const handleLoading = async (status: number) => {
// 情况一:如果是生成中,则设置加载中的 loading
if (status === AiImageStatusEnum.IN_PROGRESS) {
cardImageLoadingInstance.value = ElLoading.service({
target: cardImageRef.value,
text: '生成中...'
} as LoadingOptionsResolved)
// 情况二:如果已经生成结束,则移除 loading
} else {
if (cardImageLoadingInstance.value) {
cardImageLoadingInstance.value.close()
cardImageLoadingInstance.value = null
}
}
}
/** 初始化 */
onMounted(async () => {
await handleLoading(props.detail.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>
<template>
<el-drawer
v-model="showDrawer"
title="图片详细"
@close="handleDrawerClose"
custom-class="drawer-class"
>
<!-- 图片 -->
<div class="item">
<div class="body">
<el-image
class="image"
:src="detail?.picUrl"
:preview-src-list="[detail.picUrl]"
preview-teleported
/>
</div>
</div>
<!-- 时间 -->
<div class="item">
<div class="tip">时间</div>
<div class="body">
<div>提交时间:{{ formatTime(detail.createTime, 'yyyy-MM-dd HH:mm:ss') }}</div>
<div>生成时间:{{ formatTime(detail.finishTime, 'yyyy-MM-dd HH:mm:ss') }}</div>
</div>
</div>
<!-- 模型 -->
<div class="item">
<div class="tip">模型</div>
<div class="body"> {{ detail.model }}({{ detail.height }}x{{ detail.width }}) </div>
</div>
<!-- 提示词 -->
<div class="item">
<div class="tip">提示词</div>
<div class="body">
{{ detail.prompt }}
</div>
</div>
<!-- 地址 -->
<div class="item">
<div class="tip">图片地址</div>
<div class="body">
{{ detail.picUrl }}
</div>
</div>
<!-- StableDiffusion 专属区域 -->
<div
class="item"
v-if="detail.platform === AiPlatformEnum.STABLE_DIFFUSION && detail?.options?.sampler"
>
<div class="tip">采样方法</div>
<div class="body">
{{
StableDiffusionSamplers.find(
(item: ImageModelVO) => item.key === detail?.options?.sampler
)?.name
}}
</div>
</div>
<div
class="item"
v-if="
detail.platform === AiPlatformEnum.STABLE_DIFFUSION && detail?.options?.clipGuidancePreset
"
>
<div class="tip">CLIP</div>
<div class="body">
{{
StableDiffusionClipGuidancePresets.find(
(item: ImageModelVO) => item.key === detail?.options?.clipGuidancePreset
)?.name
}}
</div>
</div>
<div
class="item"
v-if="detail.platform === AiPlatformEnum.STABLE_DIFFUSION && detail?.options?.stylePreset"
>
<div class="tip">风格</div>
<div class="body">
{{
StableDiffusionStylePresets.find(
(item: ImageModelVO) => item.key === detail?.options?.stylePreset
)?.name
}}
</div>
</div>
<div
class="item"
v-if="detail.platform === AiPlatformEnum.STABLE_DIFFUSION && detail?.options?.steps"
>
<div class="tip">迭代步数</div>
<div class="body">
{{ detail?.options?.steps }}
</div>
</div>
<div
class="item"
v-if="detail.platform === AiPlatformEnum.STABLE_DIFFUSION && detail?.options?.scale"
>
<div class="tip">引导系数</div>
<div class="body">
{{ detail?.options?.scale }}
</div>
</div>
<div
class="item"
v-if="detail.platform === AiPlatformEnum.STABLE_DIFFUSION && detail?.options?.seed"
>
<div class="tip">随机因子</div>
<div class="body">
{{ detail?.options?.seed }}
</div>
</div>
<!-- Dall3 专属区域 -->
<div class="item" v-if="detail.platform === AiPlatformEnum.OPENAI && detail?.options?.style">
<div class="tip">风格选择</div>
<div class="body">
{{ Dall3StyleList.find((item: ImageModelVO) => item.key === detail?.options?.style)?.name }}
</div>
</div>
<!-- Midjourney 专属区域 -->
<div
class="item"
v-if="detail.platform === AiPlatformEnum.MIDJOURNEY && detail?.options?.version"
>
<div class="tip">模型版本</div>
<div class="body">
{{ detail?.options?.version }}
</div>
</div>
<div
class="item"
v-if="detail.platform === AiPlatformEnum.MIDJOURNEY && detail?.options?.referImageUrl"
>
<div class="tip">参考图</div>
<div class="body">
<el-image :src="detail.options.referImageUrl" />
</div>
</div>
</el-drawer>
</template>
<script setup lang="ts">
import { ImageApi, ImageVO } from '@/api/ai/image'
import {
AiPlatformEnum,
Dall3StyleList,
ImageModelVO,
StableDiffusionClipGuidancePresets,
StableDiffusionSamplers,
StableDiffusionStylePresets
} from '@/views/ai/utils/constants'
import { formatTime } from '@/utils'
const showDrawer = ref<boolean>(false) // 是否显示
const detail = ref<ImageVO>({} as ImageVO) // 图片详细信息
const props = defineProps({
show: {
type: Boolean,
require: true,
default: false
},
id: {
type: Number,
required: true
}
})
/** 关闭抽屉 */
const handleDrawerClose = async () => {
emits('handleDrawerClose')
}
/** 监听 drawer 是否打开 */
const { show } = toRefs(props)
watch(show, async (newValue, oldValue) => {
showDrawer.value = newValue as boolean
})
/** 获取图片详情 */
const getImageDetail = async (id: number) => {
detail.value = await ImageApi.getImageMy(id)
}
/** 监听 id 变化,加载最新图片详情 */
const { id } = toRefs(props)
watch(id, async (newVal, oldVal) => {
if (newVal) {
await getImageDetail(newVal)
}
})
const emits = defineEmits(['handleDrawerClose'])
</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>
<template>
<el-card class="dr-task" body-class="task-card" shadow="never">
<template #header>
绘画任务
<!-- TODO @fan:看看,怎么优化下这个样子哈。 -->
<el-button @click="handleViewPublic">绘画作品</el-button>
</template>
<!-- 图片列表 -->
<div class="task-image-list" ref="imageListRef">
<ImageCard
v-for="image in imageList"
:key="image.id"
:detail="image"
@on-btn-click="handleImageButtonClick"
@on-mj-btn-click="handleImageMidjourneyButtonClick"
/>
</div>
<div class="task-image-pagination">
<Pagination
:total="pageTotal"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getImageList"
/>
</div>
</el-card>
<!-- 图片详情 -->
<ImageDetail
:show="isShowImageDetail"
:id="showImageDetailId"
@handle-drawer-close="handleDetailClose"
/>
</template>
<script setup lang="ts">
import {
ImageApi,
ImageVO,
ImageMidjourneyActionVO,
ImageMidjourneyButtonsVO
} from '@/api/ai/image'
import ImageDetail from './ImageDetail.vue'
import ImageCard from './ImageCard.vue'
import { ElLoading, LoadingOptionsResolved } from 'element-plus'
import { AiImageStatusEnum } from '@/views/ai/utils/constants'
import download from '@/utils/download'
const message = useMessage() // 消息弹窗
const router = useRouter() // 路由
// 图片分页相关的参数
const queryParams = reactive({
pageNo: 1,
pageSize: 10
})
const pageTotal = ref<number>(0) // page size
const imageList = ref<ImageVO[]>([]) // image 列表
const imageListLoadingInstance = ref<any>() // image 列表是否正在加载中
const imageListRef = ref<any>() // ref
// 图片轮询相关的参数(正在生成中的)
const inProgressImageMap = ref<{}>({}) // 监听的 image 映射,一般是生成中(需要轮询),key 为 image 编号,value 为 image
const inProgressTimer = ref<any>() // 生成中的 image 定时器,轮询生成进展
// 图片详情相关的参数
const isShowImageDetail = ref<boolean>(false) // 图片详情是否展示
const showImageDetailId = ref<number>(0) // 图片详情的图片编号
/** 处理查看绘图作品 */
const handleViewPublic = () => {
router.push({
name: 'AiImageSquare'
})
}
/** 查看图片的详情 */
const handleDetailOpen = async () => {
isShowImageDetail.value = true
}
/** 关闭图片的详情 */
const handleDetailClose = async () => {
isShowImageDetail.value = false
}
/** 获得 image 图片列表 */
const getImageList = async () => {
try {
// 1. 加载图片列表
imageListLoadingInstance.value = ElLoading.service({
target: imageListRef.value,
text: '加载中...'
} as LoadingOptionsResolved)
const { list, total } = await ImageApi.getImagePageMy(queryParams)
imageList.value = list
pageTotal.value = total
// 2. 计算需要轮询的图片
const newWatImages = {}
imageList.value.forEach((item) => {
if (item.status === AiImageStatusEnum.IN_PROGRESS) {
newWatImages[item.id] = item
}
})
inProgressImageMap.value = newWatImages
} finally {
// 关闭正在“加载中”的 Loading
if (imageListLoadingInstance.value) {
imageListLoadingInstance.value.close()
imageListLoadingInstance.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 === AiImageStatusEnum.IN_PROGRESS) {
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
}
/** 图片的点击事件 */
const handleImageButtonClick = async (type: string, imageDetail: ImageVO) => {
// 详情
if (type === 'more') {
showImageDetailId.value = imageDetail.id
await handleDetailOpen()
return
}
// 删除
if (type === 'delete') {
await message.confirm(`是否删除照片?`)
await ImageApi.deleteImageMy(imageDetail.id)
await getImageList()
message.success('删除成功!')
return
}
// 下载
if (type === 'download') {
await download.image({ url: imageDetail.picUrl })
return
}
// 重新生成
if (type === 'regeneration') {
await emits('onRegeneration', imageDetail)
return
}
}
/** 处理 Midjourney 按钮点击事件 */
const handleImageMidjourneyButtonClick = async (
button: ImageMidjourneyButtonsVO,
imageDetail: ImageVO
) => {
// 1. 构建 params 参数
const data = {
id: imageDetail.id,
customId: button.customId
} as ImageMidjourneyActionVO
// 2. 发送 action
await ImageApi.midjourneyAction(data)
// 3. 刷新列表
await getImageList()
}
defineExpose({ getImageList }) // 暴露组件方法
const emits = defineEmits(['onRegeneration'])
/** 组件挂在的时候 */
onMounted(async () => {
// 获取 image 列表
await getImageList()
// 自动刷新 image 列表
inProgressTimer.value = setInterval(async () => {
await refreshWatchImages()
}, 1000 * 3)
})
/** 组件取消挂在的时候 */
onUnmounted(async () => {
if (inProgressTimer.value) {
clearInterval(inProgressTimer.value)
}
})
</script>
<style lang="scss">
.dr-task {
width: 100%;
height: 100%;
}
.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 20px 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>
<!-- 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"
class="w-100% mt-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 ImageHotWords"
: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="otherPlatform"
placeholder="Select"
size="large"
class="!w-350px"
@change="handlerPlatformChange"
>
<el-option
v-for="item in OtherPlatformEnum"
: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="model" placeholder="Select" size="large" class="!w-350px">
<el-option v-for="item in models" :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="width" type="number" class="w-170px" placeholder="图片宽度" />
<el-input v-model="height" type="number" class="w-170px" placeholder="图片高度" />
</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, ImageVO } from '@/api/ai/image'
import {
AiPlatformEnum,
ChatGlmModels,
ImageHotWords,
ImageModelVO,
OtherPlatformEnum,
QianFanModels,
TongYiWanXiangModels
} from '@/views/ai/utils/constants'
const message = useMessage() // 消息弹窗
// 定义属性
const drawIn = ref<boolean>(false) // 生成中
const selectHotWord = ref<string>('') // 选中的热词
// 表单
const prompt = ref<string>('') // 提示词
const width = ref<number>(512) // 图片宽度
const height = ref<number>(512) // 图片高度
const otherPlatform = ref<string>(AiPlatformEnum.TONG_YI) // 平台
const models = ref<ImageModelVO[]>(TongYiWanXiangModels) // 模型 TongYiWanXiangModels、QianFanModels
const model = ref<string>(models.value[0].key) // 模型
const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // 定义 emits
/** 选择热词 */
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', AiPlatformEnum.STABLE_DIFFUSION)
// 发送请求
const form = {
platform: otherPlatform.value,
model: model.value, // 模型
prompt: prompt.value, // 提示词
width: width.value, // 图片宽度
height: height.value, // 图片高度
options: {}
} as unknown as ImageDrawReqVO
await ImageApi.drawImage(form)
} finally {
// 回调
emits('onDrawComplete', AiPlatformEnum.STABLE_DIFFUSION)
// 加载结束
drawIn.value = false
}
}
/** 填充值 */
const settingValues = async (detail: ImageVO) => {
prompt.value = detail.prompt
width.value = detail.width
height.value = detail.height
}
/** 平台切换 */
const handlerPlatformChange = async (platform: string) => {
// 切换平台,切换模型、风格
if (AiPlatformEnum.TONG_YI === platform) {
models.value = TongYiWanXiangModels
} else if (AiPlatformEnum.YI_YAN === platform) {
models.value = QianFanModels
} else if (AiPlatformEnum.ZHI_PU === platform) {
models.value = ChatGlmModels
} else {
models.value = []
}
// 切换平台,默认选择一个风格
if (models.value.length > 0) {
model.value = models.value[0].key
} else {
model.value = ''
}
}
/** 暴露组件方法 */
defineExpose({ settingValues })
</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>
...@@ -3,59 +3,96 @@ ...@@ -3,59 +3,96 @@
<div class="ai-image"> <div class="ai-image">
<div class="left"> <div class="left">
<div class="segmented"> <div class="segmented">
<el-segmented v-model="selectModel" :options="modelOptions" /> <el-segmented v-model="selectPlatform" :options="platformOptions" />
</div> </div>
<div class="modal-switch-container"> <div class="modal-switch-container">
<!-- TODO @fan:1)建议 Dall3 改成 OpenAI 绘图。因为 dall3 其实本质是模型;2)涉及到中英文的地方,中文和英文之间,有个空格哈 --> <Dall3
<Dall3 v-if="selectModel === 'DALL3绘画'" v-if="selectPlatform === AiPlatformEnum.OPENAI"
@on-draw-start="handlerDrawStart" ref="dall3Ref"
@on-draw-complete="handlerDrawComplete" /> @on-draw-start="handleDrawStart"
<Midjourney v-if="selectModel === 'MJ绘画'" /> @on-draw-complete="handleDrawComplete"
<StableDiffusion v-if="selectModel === 'Stable Diffusion'" /> />
<Midjourney v-if="selectPlatform === AiPlatformEnum.MIDJOURNEY" ref="midjourneyRef" />
<StableDiffusion
v-if="selectPlatform === AiPlatformEnum.STABLE_DIFFUSION"
ref="stableDiffusionRef"
@on-draw-complete="handleDrawComplete"
/>
<Other
v-if="selectPlatform === 'other'"
ref="otherRef"
@on-draw-complete="handleDrawComplete"
/>
</div> </div>
</div> </div>
<div class="main"> <div class="main">
<ImageTask ref="imageTaskRef" /> <ImageList ref="imageListRef" @on-regeneration="handleRegeneration" />
</div> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
// TODO @fan:在整个挪到 /views/ai/image/index 目录。因为我想在 /views/ai/image/manager 做管理的功能,进行下区分! import ImageList from './components/ImageList.vue'
import Dall3 from './dall3/index.vue' import { AiPlatformEnum } from '@/views/ai/utils/constants'
import Midjourney from './midjourney/index.vue' import { ImageVO } from '@/api/ai/image'
import StableDiffusion from './stable-diffusion/index.vue' import Dall3 from './components/dall3/index.vue'
import ImageTask from './ImageTask.vue' import Midjourney from './components/midjourney/index.vue'
import StableDiffusion from './components/stableDiffusion/index.vue'
// ref import Other from './components/other/index.vue'
const imageTaskRef = ref<any>() // image task ref
const imageListRef = ref<any>() // image 列表 ref
const dall3Ref = ref<any>() // dall3(openai) ref
const midjourneyRef = ref<any>() // midjourney ref
const stableDiffusionRef = ref<any>() // stable diffusion ref
const otherRef = ref<any>() // stable diffusion ref
// 定义属性 // 定义属性
const selectModel = ref('Stable Diffusion') const selectPlatform = ref(AiPlatformEnum.MIDJOURNEY)
const modelOptions = ['DALL3绘画', 'MJ绘画', 'Stable Diffusion'] const platformOptions = [
const drawIn = ref<boolean>(false) // 生成中 {
label: 'DALL3 绘画',
value: AiPlatformEnum.OPENAI
},
{
label: 'MJ 绘画',
value: AiPlatformEnum.MIDJOURNEY
},
{
label: 'Stable Diffusion',
value: AiPlatformEnum.STABLE_DIFFUSION
},
{
label: '其它',
value: 'other'
}
]
/** 绘画 - start */ /** 绘画 start */
const handlerDrawStart = async (type) => { const handleDrawStart = async (platform: string) => {}
// todo
drawIn.value = true
}
/** 绘画 - complete */ /** 绘画 complete */
const handlerDrawComplete = async (type) => { const handleDrawComplete = async (platform: string) => {
drawIn.value = false await imageListRef.value.getImageList()
// todo
await imageTaskRef.value.getImageList()
} }
// /** 重新生成:将画图详情填充到对应平台 */
onMounted( async () => { const handleRegeneration = async (image: ImageVO) => {
}) // 切换平台
selectPlatform.value = image.platform
// 根据不同平台填充 image
await nextTick()
if (image.platform === AiPlatformEnum.MIDJOURNEY) {
midjourneyRef.value.settingValues(image)
} else if (image.platform === AiPlatformEnum.OPENAI) {
dall3Ref.value.settingValues(image)
} else if (image.platform === AiPlatformEnum.STABLE_DIFFUSION) {
stableDiffusionRef.value.settingValues(image)
}
// TODO @fan:貌似 other 重新设置不行?
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.ai-image { .ai-image {
position: absolute; position: absolute;
left: 0; left: 0;
...@@ -101,5 +138,4 @@ onMounted( async () => { ...@@ -101,5 +138,4 @@ onMounted( async () => {
background-color: #f7f8fa; background-color: #f7f8fa;
} }
} }
</style> </style>
<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 !== AiImageStatusEnum.SUCCESS"
/>
</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'
import { AiImageStatusEnum } from '@/views/ai/utils/constants'
/** 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>
<template>
<div class="square-container">
<!-- TODO @fan:style 建议换成 unocss -->
<!-- TODO @fan:Search 可以换成 Icon 组件么? -->
<el-input
v-model="queryParams.prompt"
style="width: 100%; margin-bottom: 20px"
size="large"
placeholder="请输入要搜索的内容"
:suffix-icon="Search"
@keyup.enter="handleQuery"
/>
<div class="gallery">
<!-- TODO @fan:这个图片的风格,要不和 ImageCard.vue 界面一致?(只有卡片,没有操作);因为看着更有相框的感觉~~~ -->
<div v-for="item in list" :key="item.id" class="gallery-item">
<img :src="item.picUrl" class="img" />
</div>
</div>
<!-- TODO @fan:缺少翻页 -->
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.pageNo"
v-model:limit="queryParams.pageSize"
@pagination="getList"
/>
</div>
</template>
<script setup lang="ts">
import { ImageApi, ImageVO } from '@/api/ai/image'
import { Search } from '@element-plus/icons-vue'
// TODO @fan:加个 loading 加载中的状态
const loading = ref(true) // 列表的加载中
const list = ref<ImageVO[]>([]) // 列表的数据
const total = ref(0) // 列表的总页数
const queryParams = reactive({
pageNo: 1,
pageSize: 10,
publicStatus: true,
prompt: undefined
})
/** 查询列表 */
const getList = async () => {
loading.value = true
try {
const data = await ImageApi.getImagePageMy(queryParams)
list.value = data.list
total.value = data.total
} finally {
loading.value = false
}
}
/** 搜索按钮操作 */
const handleQuery = () => {
queryParams.pageNo = 1
getList()
}
/** 初始化 */
onMounted(async () => {
await getList()
})
</script>
<style scoped lang="scss">
.square-container {
background-color: #fff;
padding: 20px;
.gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 10px;
//max-width: 1000px;
background-color: #fff;
box-shadow: 0 0 10px rgba(0, 0, 0, 0.1);
}
.gallery-item {
position: relative;
overflow: hidden;
background: #f0f0f0;
cursor: pointer;
transition: transform 0.3s;
}
.gallery-item img {
width: 100%;
height: auto;
display: block;
transition: transform 0.3s;
}
.gallery-item:hover img {
transform: scale(1.1);
}
.gallery-item:hover {
transform: scale(1.05);
}
}
</style>
<template>
<div class="w-[350px] p-5 flex flex-col bg-[#f5f7f9]">
<h3 class="w-full h-full h-7 text-5 text-center leading-[28px] title">思维导图创作中心</h3>
<!--下面表单部分-->
<div class="flex-grow overflow-y-auto">
<div class="mt-[30ppx]">
<el-text tag="b">您的需求?</el-text>
<el-input
v-model="formData.prompt"
maxlength="1024"
rows="5"
class="w-100% mt-15px"
input-style="border-radius: 7px;"
placeholder="请输入提示词,让AI帮你完善"
show-word-limit
type="textarea"
/>
<el-button
class="!w-full mt-[15px]"
type="primary"
:loading="isGenerating"
@click="emits('submit', formData)"
>
智能生成思维导图
</el-button>
</div>
<div class="mt-[30px]">
<el-text tag="b">使用已有内容生成?</el-text>
<el-input
v-model="generatedContent"
maxlength="1024"
rows="5"
class="w-100% mt-15px"
input-style="border-radius: 7px;"
placeholder="例如:童话里的小屋应该是什么样子?"
show-word-limit
type="textarea"
/>
<el-button
class="!w-full mt-[15px]"
type="primary"
@click="emits('directGenerate', generatedContent)"
:disabled="isGenerating"
>
直接生成
</el-button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { MindMapContentExample } from '@/views/ai/utils/constants'
const emits = defineEmits(['submit', 'directGenerate'])
defineProps<{
isGenerating: boolean
}>()
// 提交的提示词字段
const formData = reactive({
prompt: ''
})
const generatedContent = ref(MindMapContentExample) // 已有的内容
defineExpose({
setGeneratedContent(newContent: string) {
// 设置已有的内容,在生成结束的时候将结果赋值给该值
generatedContent.value = newContent
}
})
</script>
<style lang="scss" scoped>
.title {
color: var(--el-color-primary);
}
</style>
<template>
<el-card class="my-card h-full flex-grow">
<template #header>
<h3 class="m-0 px-7 shrink-0 flex items-center justify-between">
<span>思维导图预览</span>
<!-- 展示在右上角 -->
<el-button type="primary" v-show="isEnd" @click="downloadImage" size="small">
<template #icon>
<Icon icon="ph:copy-bold" />
</template>
下载图片
</el-button>
</h3>
</template>
<div ref="contentRef" class="hide-scroll-bar h-full box-border">
<!--展示 markdown 的容器,最终生成的是 html 字符串,直接用 v-html 嵌入-->
<div v-if="isGenerating" ref="mdContainerRef" class="wh-full overflow-y-auto">
<div class="flex flex-col items-center justify-center" v-html="html"></div>
</div>
<div ref="mindMapRef" class="wh-full">
<svg ref="svgRef" class="w-full" :style="{ height: `${contentAreaHeight}px` }" />
<div ref="toolBarRef" class="absolute bottom-[10px] right-5"></div>
</div>
</div>
</el-card>
</template>
<script setup lang="ts">
import { Markmap } from 'markmap-view'
import { Transformer } from 'markmap-lib'
import { Toolbar } from 'markmap-toolbar'
import markdownit from 'markdown-it'
import download from '@/utils/download'
const md = markdownit()
const message = useMessage() // 消息弹窗
const props = defineProps<{
generatedContent: string // 生成结果
isEnd: boolean // 是否结束
isGenerating: boolean // 是否正在生成
isStart: boolean // 开始状态,开始时需要清除 html
}>()
const contentRef = ref<HTMLDivElement>() // 右侧出来 header 以下的区域
const mdContainerRef = ref<HTMLDivElement>() // markdown 的容器,用来滚动到底下的
const mindMapRef = ref<HTMLDivElement>() // 思维导图的容器
const svgRef = ref<SVGElement>() // 思维导图的渲染 svg
const toolBarRef = ref<HTMLDivElement>() // 思维导图右下角的工具栏,缩放等
const html = ref('') // 生成过程中的文本
const contentAreaHeight = ref(0) // 生成区域的高度,出去 header 部分
let markMap: Markmap | null = null
const transformer = new Transformer()
onMounted(() => {
contentAreaHeight.value = contentRef.value?.clientHeight || 0 // 获取区域高度
/** 初始化思维导图 **/
try {
markMap = Markmap.create(svgRef.value!)
const { el } = Toolbar.create(markMap)
toolBarRef.value?.append(el)
nextTick(update)
} catch (e) {
message.error('思维导图初始化失败')
}
})
watch(props, ({ generatedContent, isGenerating, isEnd, isStart }) => {
// 开始生成的时候清空一下 markdown 的内容
if (isStart) {
html.value = ''
}
// 生成内容的时候使用 markdown 来渲染
if (isGenerating) {
html.value = md.render(generatedContent)
}
// 生成结束时更新思维导图
if (isEnd) {
update()
}
})
/** 更新思维导图的展示 */
const update = () => {
try {
const { root } = transformer.transform(processContent(props.generatedContent))
markMap?.setData(root)
markMap?.fit()
} catch (e) {
console.error(e)
}
}
/** 处理内容 */
const processContent = (text: string) => {
const arr: string[] = []
const lines = text.split('\n')
for (let line of lines) {
if (line.indexOf('```') !== -1) {
continue
}
line = line.replace(/([*_~`>])|(\d+\.)\s/g, '')
arr.push(line)
}
return arr.join('\n')
}
/** 下载图片:download SVG to png file */
const downloadImage = () => {
const svgElement = mindMapRef.value
// 将 SVG 渲染到图片对象
const serializer = new XMLSerializer()
const source = `<?xml version="1.0" standalone="no"?>\r\n${serializer.serializeToString(svgRef.value!)}`
const base64Url = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(source)}`
download.image({
url: base64Url,
canvasWidth: svgElement?.offsetWidth,
canvasHeight: svgElement?.offsetHeight,
drawWithImageSize: false
})
}
defineExpose({
scrollBottom() {
mdContainerRef.value?.scrollTo(0, mdContainerRef.value?.scrollHeight)
}
})
</script>
<style lang="scss" scoped>
.hide-scroll-bar {
-ms-overflow-style: none;
scrollbar-width: none;
&::-webkit-scrollbar {
width: 0;
height: 0;
}
}
.my-card {
display: flex;
flex-direction: column;
:deep(.el-card__body) {
box-sizing: border-box;
flex-grow: 1;
overflow-y: auto;
padding: 0;
@extend .hide-scroll-bar;
}
}
// markmaptool样式覆盖
:deep(.markmap) {
width: 100%;
}
:deep(.mm-toolbar-brand) {
display: none;
}
:deep(.mm-toolbar) {
display: flex;
flex-direction: row;
}
</style>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or sign in to comment