From abfde09eac076a73110d59d270c4edc7b7adef12 Mon Sep 17 00:00:00 2001 From: houzhongjian <houzhongyi@126.com> Date: 星期一, 26 八月 2024 14:50:57 +0800 Subject: [PATCH] v1.0.1-sanpshot --- /dev/null | 256 --------------------------------------------------- package.json | 6 2 files changed, 3 insertions(+), 259 deletions(-) diff --git a/package.json b/package.json index bf3c3e1..625d410 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "iailab-plat-ui-vue3", - "version": "1.0.0-snapshot", + "version": "1.0.1-sanpshot", "description": "基于vue3、vite4、element-plus、typesScript", "author": "iailab", "private": false, @@ -119,7 +119,7 @@ "stylelint-order": "^6.0.4", "terser": "^5.28.1", "typescript": "5.3.3", - "unocss": "^0.58.5", + "unocss": "^0.58.9", "unplugin-auto-import": "^0.16.7", "unplugin-element-plus": "^0.8.0", "unplugin-vue-components": "^0.25.2", @@ -134,7 +134,7 @@ "vue-eslint-parser": "^9.3.2", "vue-tsc": "^1.8.27" }, - "license": "IAILAB", + "license": "MIT", "repository": { "type": "git", "url": "git+https://" diff --git a/src/api/ai/chat/conversation/index.ts b/src/api/ai/chat/conversation/index.ts deleted file mode 100644 index 6ce4482..0000000 --- a/src/api/ai/chat/conversation/index.ts +++ /dev/null @@ -1,65 +0,0 @@ -import request from '@/config/axios' - -// AI 聊天对话 VO -export interface ChatConversationVO { - id: number // ID 编号 - userId: number // 用户编号 - title: string // 对话标题 - pinned: boolean // 是否置顶 - roleId: number // 角色编号 - modelId: number // 模型编号 - model: string // 模型标志 - temperature: number // 温度参数 - maxTokens: number // 单条回复的最大 Token 数量 - maxContexts: number // 上下文的最大 Message 数量 - createTime?: Date // 创建时间 - // 额外字段 - systemMessage?: string // 角色设定 - modelName?: string // 模型名字 - roleAvatar?: string // 角色头像 - modelMaxTokens?: string // 模型的单条回复的最大 Token 数量 - modelMaxContexts?: string // 模型的上下文的最大 Message 数量 -} - -// AI 聊天对话 API -export const ChatConversationApi = { - // 获得【我的】聊天对话 - getChatConversationMy: async (id: number) => { - return await request.get({ url: `/ai/chat/conversation/get-my?id=${id}` }) - }, - - // 新增【我的】聊天对话 - createChatConversationMy: async (data?: ChatConversationVO) => { - return await request.post({ url: `/ai/chat/conversation/create-my`, data }) - }, - - // 更新【我的】聊天对话 - updateChatConversationMy: async (data: ChatConversationVO) => { - return await request.put({ url: `/ai/chat/conversation/update-my`, data }) - }, - - // 删除【我的】聊天对话 - deleteChatConversationMy: async (id: string) => { - return await request.delete({ url: `/ai/chat/conversation/delete-my?id=${id}` }) - }, - - // 删除【我的】所有对话,置顶除外 - deleteChatConversationMyByUnpinned: async () => { - return await request.delete({ url: `/ai/chat/conversation/delete-by-unpinned` }) - }, - - // 获得【我的】聊天对话列表 - getChatConversationMyList: async () => { - return await request.get({ url: `/ai/chat/conversation/my-list` }) - }, - - // 获得对话分页 - getChatConversationPage: async (params: any) => { - return await request.get({ url: `/ai/chat/conversation/page`, params }) - }, - - // 管理员删除消息 - deleteChatConversationByAdmin: async (id: number) => { - return await request.delete({ url: `/ai/chat/conversation/delete-by-admin?id=${id}` }) - } -} diff --git a/src/api/ai/chat/message/index.ts b/src/api/ai/chat/message/index.ts deleted file mode 100644 index ef1196a..0000000 --- a/src/api/ai/chat/message/index.ts +++ /dev/null @@ -1,83 +0,0 @@ -import request from '@/config/axios' -import { fetchEventSource } from '@microsoft/fetch-event-source' -import { getAccessToken } from '@/utils/auth' -import { config } from '@/config/axios/config' - -// 聊天VO -export interface ChatMessageVO { - id: number // 编号 - conversationId: number // 对话编号 - type: string // 消息类型 - userId: string // 用户编号 - roleId: string // 角色编号 - model: number // 模型标志 - modelId: number // 模型编号 - content: string // 聊天内容 - tokens: number // 消耗 Token 数量 - createTime: Date // 创建时间 - roleAvatar: string // 角色头像 - userAvatar: string // 创建时间 -} - -// AI chat 聊天 -export const ChatMessageApi = { - // 消息列表 - getChatMessageListByConversationId: async (conversationId: number | null) => { - return await request.get({ - url: `/ai/chat/message/list-by-conversation-id?conversationId=${conversationId}` - }) - }, - - // 发送 Stream 消息 - // 为什么不用 axios 呢?因为它不支持 SSE 调用 - sendChatMessageStream: async ( - conversationId: number, - content: string, - ctrl, - enableContext: boolean, - onMessage, - onError, - onClose - ) => { - const token = getAccessToken() - return fetchEventSource(`${config.base_url}/ai/chat/message/send-stream`, { - method: 'post', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}` - }, - openWhenHidden: true, - body: JSON.stringify({ - conversationId, - content, - useContext: enableContext - }), - onmessage: onMessage, - onerror: onError, - onclose: onClose, - signal: ctrl.signal - }) - }, - - // 删除消息 - deleteChatMessage: async (id: string) => { - return await request.delete({ url: `/ai/chat/message/delete?id=${id}` }) - }, - - // 删除指定对话的消息 - deleteByConversationId: async (conversationId: number) => { - return await request.delete({ - url: `/ai/chat/message/delete-by-conversation-id?conversationId=${conversationId}` - }) - }, - - // 获得消息分页 - getChatMessagePage: async (params: any) => { - return await request.get({ url: '/ai/chat/message/page', params }) - }, - - // 管理员删除消息 - deleteChatMessageByAdmin: async (id: number) => { - return await request.delete({ url: `/ai/chat/message/delete-by-admin?id=${id}` }) - } -} diff --git a/src/api/ai/image/index.ts b/src/api/ai/image/index.ts deleted file mode 100644 index 2f276c7..0000000 --- a/src/api/ai/image/index.ts +++ /dev/null @@ -1,103 +0,0 @@ -import request from '@/config/axios' - -// AI 绘图 VO -export interface ImageVO { - id: number // 编号 - platform: string // 平台 - model: string // 模型 - prompt: string // 提示词 - width: number // 图片宽度 - height: number // 图片高度 - status: number // 状态 - publicStatus: boolean // 公开状态 - picUrl: string // 任务地址 - errorMessage: string // 错误信息 - options: any // 配置 Map<string, string> - taskId: number // 任务编号 - buttons: ImageMidjourneyButtonsVO[] // mj 操作按钮 - createTime: Date // 创建时间 - finishTime: Date // 完成时间 -} - -export interface ImageDrawReqVO { - platform: string // 平台 - prompt: string // 提示词 - model: string // 模型 - style: string // 图像生成的风格 - width: string // 图片宽度 - height: string // 图片高度 - options: object // 绘制参数,Map<String, String> -} - -export interface ImageMidjourneyImagineReqVO { - prompt: string // 提示词 - model: string // 模型 mj nijj - base64Array: string[] // size不能为空 - width: string // 图片宽度 - height: string // 图片高度 - version: string // 版本 -} - -export interface ImageMidjourneyActionVO { - 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 = { - // 获取【我的】绘图分页 - getImagePageMy: async (params: any) => { - return await request.get({ url: `/ai/image/my-page`, params }) - }, - // 获取【我的】绘图记录 - getImageMy: async (id: number) => { - return await request.get({ url: `/ai/image/get-my?id=${id}` }) - }, - // 获取【我的】绘图记录列表 - getImageListMyByIds: async (ids: number[]) => { - return await request.get({ url: `/ai/image/my-list-by-ids`, params: { ids: ids.join(',') } }) - }, - // 生成图片 - drawImage: async (data: ImageDrawReqVO) => { - return await request.post({ url: `/ai/image/draw`, data }) - }, - // 删除【我的】绘画记录 - deleteImageMy: async (id: number) => { - return await request.delete({ url: `/ai/image/delete-my?id=${id}` }) - }, - - // ================ midjourney 专属 ================ - - // 【Midjourney】生成图片 - midjourneyImagine: async (data: ImageMidjourneyImagineReqVO) => { - return await request.post({ url: `/ai/image/midjourney/imagine`, data }) - }, - // 【Midjourney】Action 操作(二次生成图片) - midjourneyAction: async (data: ImageMidjourneyActionVO) => { - 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 }) - } -} diff --git a/src/api/ai/mindmap/index.ts b/src/api/ai/mindmap/index.ts deleted file mode 100644 index def113b..0000000 --- a/src/api/ai/mindmap/index.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { getAccessToken } from '@/utils/auth' -import { fetchEventSource } from '@microsoft/fetch-event-source' -import { config } from '@/config/axios/config' - -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 - }) - } -} diff --git a/src/api/ai/model/apiKey/index.ts b/src/api/ai/model/apiKey/index.ts deleted file mode 100644 index ed94836..0000000 --- a/src/api/ai/model/apiKey/index.ts +++ /dev/null @@ -1,44 +0,0 @@ -import request from '@/config/axios' - -// AI API 密钥 VO -export interface ApiKeyVO { - id: number // 编号 - name: string // 名称 - apiKey: string // 密钥 - platform: string // 平台 - url: string // 自定义 API 地址 - status: number // 状态 -} - -// AI API 密钥 API -export const ApiKeyApi = { - // 查询 API 密钥分页 - getApiKeyPage: async (params: any) => { - return await request.get({ url: `/ai/api-key/page`, params }) - }, - - // 获得 API 密钥列表 - getApiKeySimpleList: async () => { - return await request.get({ url: `/ai/api-key/simple-list` }) - }, - - // 查询 API 密钥详情 - getApiKey: async (id: number) => { - return await request.get({ url: `/ai/api-key/get?id=` + id }) - }, - - // 新增 API 密钥 - createApiKey: async (data: ApiKeyVO) => { - return await request.post({ url: `/ai/api-key/create`, data }) - }, - - // 修改 API 密钥 - updateApiKey: async (data: ApiKeyVO) => { - return await request.put({ url: `/ai/api-key/update`, data }) - }, - - // 删除 API 密钥 - deleteApiKey: async (id: number) => { - return await request.delete({ url: `/ai/api-key/delete?id=` + id }) - } -} diff --git a/src/api/ai/model/chatModel/index.ts b/src/api/ai/model/chatModel/index.ts deleted file mode 100644 index c2ef4c8..0000000 --- a/src/api/ai/model/chatModel/index.ts +++ /dev/null @@ -1,53 +0,0 @@ -import request from '@/config/axios' - -// AI 聊天模型 VO -export interface ChatModelVO { - id: number // 编号 - keyId: number // API 秘钥编号 - name: string // 模型名字 - model: string // 模型标识 - platform: string // 模型平台 - sort: number // 排序 - status: number // 状态 - temperature: number // 温度参数 - maxTokens: number // 单条回复的最大 Token 数量 - maxContexts: number // 上下文的最大 Message 数量 -} - -// AI 聊天模型 API -export const ChatModelApi = { - // 查询聊天模型分页 - getChatModelPage: async (params: any) => { - return await request.get({ url: `/ai/chat-model/page`, params }) - }, - - // 获得聊天模型列表 - getChatModelSimpleList: async (status?: number) => { - return await request.get({ - url: `/ai/chat-model/simple-list`, - params: { - status - } - }) - }, - - // 查询聊天模型详情 - getChatModel: async (id: number) => { - return await request.get({ url: `/ai/chat-model/get?id=` + id }) - }, - - // 新增聊天模型 - createChatModel: async (data: ChatModelVO) => { - return await request.post({ url: `/ai/chat-model/create`, data }) - }, - - // 修改聊天模型 - updateChatModel: async (data: ChatModelVO) => { - return await request.put({ url: `/ai/chat-model/update`, data }) - }, - - // 删除聊天模型 - deleteChatModel: async (id: number) => { - return await request.delete({ url: `/ai/chat-model/delete?id=` + id }) - } -} diff --git a/src/api/ai/model/chatRole/index.ts b/src/api/ai/model/chatRole/index.ts deleted file mode 100644 index a9fce13..0000000 --- a/src/api/ai/model/chatRole/index.ts +++ /dev/null @@ -1,80 +0,0 @@ -import request from '@/config/axios' - -// AI 聊天角色 VO -export interface ChatRoleVO { - id: number // 角色编号 - modelId: number // 模型编号 - name: string // 角色名称 - avatar: string // 角色头像 - category: string // 角色类别 - sort: number // 角色排序 - description: string // 角色描述 - systemMessage: string // 角色设定 - welcomeMessage: string // 角色设定 - publicStatus: boolean // 是否公开 - status: number // 状态 -} - -// AI 聊天角色 分页请求 vo -export interface ChatRolePageReqVO { - name?: string // 角色名称 - category?: string // 角色类别 - publicStatus: boolean // 是否公开 - pageNo: number // 是否公开 - pageSize: number // 是否公开 -} - -// AI 聊天角色 API -export const ChatRoleApi = { - // 查询聊天角色分页 - getChatRolePage: async (params: any) => { - return await request.get({ url: `/ai/chat-role/page`, params }) - }, - - // 查询聊天角色详情 - getChatRole: async (id: number) => { - return await request.get({ url: `/ai/chat-role/get?id=` + id }) - }, - - // 新增聊天角色 - createChatRole: async (data: ChatRoleVO) => { - return await request.post({ url: `/ai/chat-role/create`, data }) - }, - - // 修改聊天角色 - updateChatRole: async (data: ChatRoleVO) => { - return await request.put({ url: `/ai/chat-role/update`, data }) - }, - - // 删除聊天角色 - deleteChatRole: async (id: number) => { - return await request.delete({ url: `/ai/chat-role/delete?id=` + id }) - }, - - // ======= chat 聊天 - - // 获取 my role - getMyPage: async (params: ChatRolePageReqVO) => { - return await request.get({ url: `/ai/chat-role/my-page`, params}) - }, - - // 获取角色分类 - getCategoryList: async () => { - return await request.get({ url: `/ai/chat-role/category-list`}) - }, - - // 创建角色 - createMy: async (data: ChatRoleVO) => { - return await request.post({ url: `/ai/chat-role/create-my`, data}) - }, - - // 更新角色 - updateMy: async (data: ChatRoleVO) => { - return await request.put({ url: `/ai/chat-role/update-my`, data}) - }, - - // 删除角色 my - deleteMy: async (id: number) => { - return await request.delete({ url: `/ai/chat-role/delete-my?id=` + id }) - }, -} diff --git a/src/api/ai/music/index.ts b/src/api/ai/music/index.ts deleted file mode 100644 index 74b8526..0000000 --- a/src/api/ai/music/index.ts +++ /dev/null @@ -1,41 +0,0 @@ -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 }) - } -} diff --git a/src/api/ai/write/index.ts b/src/api/ai/write/index.ts deleted file mode 100644 index 013f998..0000000 --- a/src/api/ai/write/index.ts +++ /dev/null @@ -1,85 +0,0 @@ -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 } }) - } -} diff --git a/src/views/ai/chat/index/components/conversation/ConversationList.vue b/src/views/ai/chat/index/components/conversation/ConversationList.vue deleted file mode 100644 index 54940f8..0000000 --- a/src/views/ai/chat/index/components/conversation/ConversationList.vue +++ /dev/null @@ -1,472 +0,0 @@ -<!-- AI 对话 --> -<template> - <el-aside width="260px" class="conversation-container h-100%"> - <!-- 左顶部:对话 --> - <div class="h-100%"> - <el-button class="w-1/1 btn-new-conversation" type="primary" @click="createConversation"> - <Icon icon="ep:plus" class="mr-5px" /> - 新建对话 - </el-button> - - <!-- 左顶部:搜索对话 --> - <el-input - v-model="searchName" - size="large" - class="mt-10px search-input" - placeholder="搜索历史记录" - @keyup="searchConversation" - > - <template #prefix> - <Icon icon="ep:search" /> - </template> - </el-input> - - <!-- 左中间:对话列表 --> - <div class="conversation-list"> - <!-- 情况一:加载中 --> - <el-empty v-if="loading" description="." :v-loading="loading" /> - <!-- 情况二:按照 group 分组,展示聊天会话 list 列表 --> - <div v-for="conversationKey in Object.keys(conversationMap)" :key="conversationKey"> - <div - class="conversation-item classify-title" - v-if="conversationMap[conversationKey].length" - > - <el-text class="mx-1" size="small" tag="b">{{ conversationKey }}</el-text> - </div> - <div - class="conversation-item" - v-for="conversation in conversationMap[conversationKey]" - :key="conversation.id" - @click="handleConversationClick(conversation.id)" - @mouseover="hoverConversationId = conversation.id" - @mouseout="hoverConversationId = ''" - > - <div - :class=" - conversation.id === activeConversationId ? 'conversation active' : 'conversation' - " - > - <div class="title-wrapper"> - <img class="avatar" :src="conversation.roleAvatar || roleAvatarDefaultImg" /> - <span class="title">{{ conversation.title }}</span> - </div> - <div class="button-wrapper" v-show="hoverConversationId === conversation.id"> - <el-button class="btn" link @click.stop="handleTop(conversation)"> - <el-icon title="置顶" v-if="!conversation.pinned"><Top /></el-icon> - <el-icon title="置顶" v-if="conversation.pinned"><Bottom /></el-icon> - </el-button> - <el-button class="btn" link @click.stop="updateConversationTitle(conversation)"> - <el-icon title="编辑"> - <Icon icon="ep:edit" /> - </el-icon> - </el-button> - <el-button class="btn" link @click.stop="deleteChatConversation(conversation)"> - <el-icon title="删除对话"> - <Icon icon="ep:delete" /> - </el-icon> - </el-button> - </div> - </div> - </div> - </div> - <!-- 底部占位 --> - <div class="h-160px w-100%"></div> - </div> - </div> - - <!-- 左底部:工具栏 --> - <div class="tool-box"> - <div @click="handleRoleRepository"> - <Icon icon="ep:user" /> - <el-text size="small">角色仓库</el-text> - </div> - <div @click="handleClearConversation"> - <Icon icon="ep:delete" /> - <el-text size="small">清空未置顶对话</el-text> - </div> - </div> - - <!-- 角色仓库抽屉 --> - <el-drawer v-model="roleRepositoryOpen" title="角色仓库" size="754px"> - <RoleRepository /> - </el-drawer> - </el-aside> -</template> - -<script setup lang="ts"> -import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation' -import RoleRepository from '../role/RoleRepository.vue' -import { Bottom, Top } from '@element-plus/icons-vue' -import roleAvatarDefaultImg from '@/assets/ai/gpt.svg' - -const message = useMessage() // 消息弹窗 - -// 定义属性 -const searchName = ref<string>('') // 对话搜索 -const activeConversationId = ref<number | null>(null) // 选中的对话,默认为 null -const hoverConversationId = ref<number | null>(null) // 悬浮上去的对话 -const conversationList = ref([] as ChatConversationVO[]) // 对话列表 -const conversationMap = ref<any>({}) // 对话分组 (置顶、今天、三天前、一星期前、一个月前) -const loading = ref<boolean>(false) // 加载中 -const loadingTime = ref<any>() // 加载中定时器 - -// 定义组件 props -const props = defineProps({ - activeId: { - type: String || null, - required: true - } -}) - -// 定义钩子 -const emits = defineEmits([ - 'onConversationCreate', - 'onConversationClick', - 'onConversationClear', - 'onConversationDelete' -]) - -/** 搜索对话 */ -const searchConversation = async (e) => { - // 恢复数据 - if (!searchName.value.trim().length) { - conversationMap.value = await getConversationGroupByCreateTime(conversationList.value) - } else { - // 过滤 - const filterValues = conversationList.value.filter((item) => { - return item.title.includes(searchName.value.trim()) - }) - conversationMap.value = await getConversationGroupByCreateTime(filterValues) - } -} - -/** 点击对话 */ -const handleConversationClick = async (id: number) => { - // 过滤出选中的对话 - const filterConversation = conversationList.value.filter((item) => { - return item.id === id - }) - // 回调 onConversationClick - // noinspection JSVoidFunctionReturnValueUsed - const success = emits('onConversationClick', filterConversation[0]) - // 切换对话 - if (success) { - activeConversationId.value = id - } -} - -/** 获取对话列表 */ -const getChatConversationList = async () => { - try { - // 加载中 - loadingTime.value = setTimeout(() => { - loading.value = true - }, 50) - - // 1.1 获取 对话数据 - conversationList.value = await ChatConversationApi.getChatConversationMyList() - // 1.2 排序 - conversationList.value.sort((a, b) => { - return b.createTime - a.createTime - }) - // 1.3 没有任何对话情况 - if (conversationList.value.length === 0) { - activeConversationId.value = null - conversationMap.value = {} - return - } - - // 2. 对话根据时间分组(置顶、今天、一天前、三天前、七天前、30 天前) - conversationMap.value = await getConversationGroupByCreateTime(conversationList.value) - } finally { - // 清理定时器 - if (loadingTime.value) { - clearTimeout(loadingTime.value) - } - // 加载完成 - loading.value = false - } -} - -/** 按照 creteTime 创建时间,进行分组 */ -const getConversationGroupByCreateTime = async (list: ChatConversationVO[]) => { - // 排序、指定、时间分组(今天、一天前、三天前、七天前、30天前) - // noinspection NonAsciiCharacters - const groupMap = { - 置顶: [], - 今天: [], - 一天前: [], - 三天前: [], - 七天前: [], - 三十天前: [] - } - // 当前时间的时间戳 - const now = Date.now() - // 定义时间间隔常量(单位:毫秒) - const oneDay = 24 * 60 * 60 * 1000 - const threeDays = 3 * oneDay - const sevenDays = 7 * oneDay - const thirtyDays = 30 * oneDay - for (const conversation of list) { - // 置顶 - if (conversation.pinned) { - groupMap['置顶'].push(conversation) - continue - } - // 计算时间差(单位:毫秒) - const diff = now - conversation.createTime - // 根据时间间隔判断 - if (diff < oneDay) { - groupMap['今天'].push(conversation) - } else if (diff < threeDays) { - groupMap['一天前'].push(conversation) - } else if (diff < sevenDays) { - groupMap['三天前'].push(conversation) - } else if (diff < thirtyDays) { - groupMap['七天前'].push(conversation) - } else { - groupMap['三十天前'].push(conversation) - } - } - return groupMap -} - -/** 新建对话 */ -const createConversation = async () => { - // 1. 新建对话 - const conversationId = await ChatConversationApi.createChatConversationMy( - {} as unknown as ChatConversationVO - ) - // 2. 获取对话内容 - await getChatConversationList() - // 3. 选中对话 - await handleConversationClick(conversationId) - // 4. 回调 - emits('onConversationCreate') -} - -/** 修改对话的标题 */ -const updateConversationTitle = async (conversation: ChatConversationVO) => { - // 1. 二次确认 - const { value } = await ElMessageBox.prompt('修改标题', { - inputPattern: /^[\s\S]*.*\S[\s\S]*$/, // 判断非空,且非空格 - inputErrorMessage: '标题不能为空', - inputValue: conversation.title - }) - // 2. 发起修改 - await ChatConversationApi.updateChatConversationMy({ - id: conversation.id, - title: value - } as ChatConversationVO) - message.success('重命名成功') - // 3. 刷新列表 - await getChatConversationList() - // 4. 过滤当前切换的 - const filterConversationList = conversationList.value.filter((item) => { - return item.id === conversation.id - }) - if (filterConversationList.length > 0) { - // tip:避免切换对话 - if (activeConversationId.value === filterConversationList[0].id) { - emits('onConversationClick', filterConversationList[0]) - } - } -} - -/** 删除聊天对话 */ -const deleteChatConversation = async (conversation: ChatConversationVO) => { - try { - // 删除的二次确认 - await message.delConfirm(`是否确认删除对话 - ${conversation.title}?`) - // 发起删除 - await ChatConversationApi.deleteChatConversationMy(conversation.id) - message.success('对话已删除') - // 刷新列表 - await getChatConversationList() - // 回调 - emits('onConversationDelete', conversation) - } catch {} -} - -/** 清空对话 */ -const handleClearConversation = async () => { - try { - await message.confirm('确认后对话会全部清空,置顶的对话除外。') - await ChatConversationApi.deleteChatConversationMyByUnpinned() - ElMessage({ - message: '操作成功!', - type: 'success' - }) - // 清空 对话 和 对话内容 - activeConversationId.value = null - // 获取 对话列表 - await getChatConversationList() - // 回调 方法 - emits('onConversationClear') - } catch {} -} - -/** 对话置顶 */ -const handleTop = async (conversation: ChatConversationVO) => { - // 更新对话置顶 - conversation.pinned = !conversation.pinned - await ChatConversationApi.updateChatConversationMy(conversation) - // 刷新对话 - await getChatConversationList() -} - -// ============ 角色仓库 ============ - -/** 角色仓库抽屉 */ -const roleRepositoryOpen = ref<boolean>(false) // 角色仓库是否打开 -const handleRoleRepository = async () => { - roleRepositoryOpen.value = !roleRepositoryOpen.value -} - -/** 监听选中的对话 */ -const { activeId } = toRefs(props) -watch(activeId, async (newValue, oldValue) => { - activeConversationId.value = newValue as string -}) - -// 定义 public 方法 -defineExpose({ createConversation }) - -/** 初始化 */ -onMounted(async () => { - // 获取 对话列表 - await getChatConversationList() - // 默认选中 - if (props.activeId) { - activeConversationId.value = props.activeId - } else { - // 首次默认选中第一个 - if (conversationList.value.length) { - activeConversationId.value = conversationList.value[0].id - // 回调 onConversationClick - await emits('onConversationClick', conversationList.value[0]) - } - } -}) -</script> - -<style scoped lang="scss"> -.conversation-container { - position: relative; - display: flex; - flex-direction: column; - justify-content: space-between; - padding: 10px 10px 0; - overflow: hidden; - - .btn-new-conversation { - padding: 18px 0; - } - - .search-input { - margin-top: 20px; - } - - .conversation-list { - overflow: auto; - height: 100%; - - .classify-title { - padding-top: 10px; - } - - .conversation-item { - margin-top: 5px; - } - - .conversation { - display: flex; - flex-direction: row; - justify-content: space-between; - flex: 1; - padding: 0 5px; - cursor: pointer; - border-radius: 5px; - align-items: center; - line-height: 30px; - - &.active { - background-color: #e6e6e6; - - .button { - display: inline-block; - } - } - - .title-wrapper { - display: flex; - flex-direction: row; - align-items: center; - } - - .title { - padding: 2px 10px; - max-width: 220px; - font-size: 14px; - font-weight: 400; - color: rgba(0, 0, 0, 0.77); - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - - .avatar { - width: 25px; - height: 25px; - border-radius: 5px; - display: flex; - flex-direction: row; - justify-items: center; - } - - // 对话编辑、删除 - .button-wrapper { - right: 2px; - display: flex; - flex-direction: row; - justify-items: center; - color: #606266; - - .btn { - margin: 0; - } - } - } - } - - // 角色仓库、清空未设置对话 - .tool-box { - position: absolute; - bottom: 0; - left: 0; - right: 0; - //width: 100%; - padding: 0 20px; - background-color: #f4f4f4; - box-shadow: 0 0 1px 1px rgba(228, 228, 228, 0.8); - line-height: 35px; - display: flex; - justify-content: space-between; - align-items: center; - color: var(--el-text-color); - - > div { - display: flex; - align-items: center; - color: #606266; - padding: 0; - margin: 0; - cursor: pointer; - - > span { - margin-left: 5px; - } - } - } -} -</style> diff --git a/src/views/ai/chat/index/components/conversation/ConversationUpdateForm.vue b/src/views/ai/chat/index/components/conversation/ConversationUpdateForm.vue deleted file mode 100644 index bff094f..0000000 --- a/src/views/ai/chat/index/components/conversation/ConversationUpdateForm.vue +++ /dev/null @@ -1,145 +0,0 @@ -<template> - <Dialog title="设定" v-model="dialogVisible"> - <el-form - ref="formRef" - :model="formData" - :rules="formRules" - label-width="130px" - v-loading="formLoading" - > - <el-form-item label="角色设定" prop="systemMessage"> - <el-input - type="textarea" - v-model="formData.systemMessage" - rows="4" - placeholder="请输入角色设定" - /> - </el-form-item> - <el-form-item label="模型" prop="modelId"> - <el-select v-model="formData.modelId" placeholder="请选择模型"> - <el-option - v-for="chatModel in chatModelList" - :key="chatModel.id" - :label="chatModel.name" - :value="chatModel.id" - /> - </el-select> - </el-form-item> - <el-form-item label="温度参数" prop="temperature"> - <el-input-number - v-model="formData.temperature" - placeholder="请输入温度参数" - :min="0" - :max="2" - :precision="2" - /> - </el-form-item> - <el-form-item label="回复数 Token 数" prop="maxTokens"> - <el-input-number - v-model="formData.maxTokens" - placeholder="请输入回复数 Token 数" - :min="0" - :max="4096" - /> - </el-form-item> - <el-form-item label="上下文数量" prop="maxContexts"> - <el-input-number - v-model="formData.maxContexts" - placeholder="请输入上下文数量" - :min="0" - :max="20" - /> - </el-form-item> - </el-form> - <template #footer> - <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> - <el-button @click="dialogVisible = false">取 消</el-button> - </template> - </Dialog> -</template> -<script setup lang="ts"> -import { CommonStatusEnum } from '@/utils/constants' -import { ChatModelApi, ChatModelVO } from '@/api/ai/model/chatModel' -import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation' - -/** AI 聊天对话的更新表单 */ -defineOptions({ name: 'ChatConversationUpdateForm' }) - -const message = useMessage() // 消息弹窗 - -const dialogVisible = ref(false) // 弹窗的是否展示 -const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 -const formData = ref({ - id: undefined, - systemMessage: undefined, - modelId: undefined, - temperature: undefined, - maxTokens: undefined, - maxContexts: undefined -}) -const formRules = reactive({ - modelId: [{ required: true, message: '模型不能为空', trigger: 'blur' }], - status: [{ required: true, message: '状态不能为空', trigger: 'blur' }], - temperature: [{ required: true, message: '温度参数不能为空', trigger: 'blur' }], - maxTokens: [{ required: true, message: '回复数 Token 数不能为空', trigger: 'blur' }], - maxContexts: [{ required: true, message: '上下文数量不能为空', trigger: 'blur' }] -}) -const formRef = ref() // 表单 Ref -const chatModelList = ref([] as ChatModelVO[]) // 聊天模型列表 - -/** 打开弹窗 */ -const open = async (id: number) => { - dialogVisible.value = true - resetForm() - // 修改时,设置数据 - if (id) { - formLoading.value = true - try { - const data = await ChatConversationApi.getChatConversationMy(id) - formData.value = Object.keys(formData.value).reduce((obj, key) => { - if (data.hasOwnProperty(key)) { - obj[key] = data[key] - } - return obj - }, {}) - } finally { - formLoading.value = false - } - } - // 获得下拉数据 - chatModelList.value = await ChatModelApi.getChatModelSimpleList(CommonStatusEnum.ENABLE) -} -defineExpose({ open }) // 提供 open 方法,用于打开弹窗 - -/** 提交表单 */ -const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 -const submitForm = async () => { - // 校验表单 - await formRef.value.validate() - // 提交请求 - formLoading.value = true - try { - const data = formData.value as unknown as ChatConversationVO - await ChatConversationApi.updateChatConversationMy(data) - message.success('对话配置已更新') - dialogVisible.value = false - // 发送操作成功的事件 - emit('success') - } finally { - formLoading.value = false - } -} - -/** 重置表单 */ -const resetForm = () => { - formData.value = { - id: undefined, - systemMessage: undefined, - modelId: undefined, - temperature: undefined, - maxTokens: undefined, - maxContexts: undefined - } - formRef.value?.resetFields() -} -</script> diff --git a/src/views/ai/chat/index/components/message/MessageList.vue b/src/views/ai/chat/index/components/message/MessageList.vue deleted file mode 100644 index 2cc8407..0000000 --- a/src/views/ai/chat/index/components/message/MessageList.vue +++ /dev/null @@ -1,282 +0,0 @@ -<template> - <div ref="messageContainer" class="h-100% overflow-y-auto relative"> - <div class="chat-list" v-for="(item, index) in list" :key="index"> - <!-- 靠左 message:system、assistant 类型 --> - <div class="left-message message-item" v-if="item.type !== 'user'"> - <div class="avatar"> - <el-avatar :src="roleAvatar" /> - </div> - <div class="message"> - <div> - <el-text class="time">{{ formatDate(item.createTime) }}</el-text> - </div> - <div class="left-text-container" ref="markdownViewRef"> - <MarkdownView class="left-text" :content="item.content" /> - </div> - <div class="left-btns"> - <el-button class="btn-cus" link @click="copyContent(item.content)"> - <img class="btn-image" src="@/assets/ai/copy.svg" /> - </el-button> - <el-button v-if="item.id > 0" class="btn-cus" link @click="onDelete(item.id)"> - <img class="btn-image h-17px" src="@/assets/ai/delete.svg" /> - </el-button> - </div> - </div> - </div> - <!-- 靠右 message:user 类型 --> - <div class="right-message message-item" v-if="item.type === 'user'"> - <div class="avatar"> - <el-avatar :src="userAvatar" /> - </div> - <div class="message"> - <div> - <el-text class="time">{{ formatDate(item.createTime) }}</el-text> - </div> - <div class="right-text-container"> - <div class="right-text">{{ item.content }}</div> - </div> - <div class="right-btns"> - <el-button class="btn-cus" link @click="copyContent(item.content)"> - <img class="btn-image" src="@/assets/ai/copy.svg" /> - </el-button> - <el-button class="btn-cus" link @click="onDelete(item.id)"> - <img class="btn-image h-17px mr-12px" src="@/assets/ai/delete.svg" /> - </el-button> - <el-button class="btn-cus" link @click="onRefresh(item)"> - <el-icon size="17"><RefreshRight /></el-icon> - </el-button> - <el-button class="btn-cus" link @click="onEdit(item)"> - <el-icon size="17"><Edit /></el-icon> - </el-button> - </div> - </div> - </div> - </div> - </div> - <!-- 回到底部 --> - <div v-if="isScrolling" class="to-bottom" @click="handleGoBottom"> - <el-button :icon="ArrowDownBold" circle /> - </div> -</template> -<script setup lang="ts"> -import { PropType } from 'vue' -import { formatDate } from '@/utils/formatTime' -import MarkdownView from '@/components/MarkdownView/index.vue' -import { useClipboard } from '@vueuse/core' -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 { useUserStore } from '@/store/modules/user' -import userAvatarDefaultImg from '@/assets/imgs/avatar.gif' -import roleAvatarDefaultImg from '@/assets/ai/gpt.svg' - -const message = useMessage() // 消息弹窗 -const { copy } = useClipboard() // 初始化 copy 到粘贴板 -const userStore = useUserStore() - -// 判断“消息列表”滚动的位置(用于判断是否需要滚动到消息最下方) -const messageContainer: any = ref(null) -const isScrolling = ref(false) //用于判断用户是否在滚动 - -const userAvatar = computed(() => userStore.user.avatar ?? userAvatarDefaultImg) -const roleAvatar = computed(() => props.conversation.roleAvatar ?? roleAvatarDefaultImg) - -// 定义 props -const props = defineProps({ - conversation: { - type: Object as PropType<ChatConversationVO>, - required: true - }, - list: { - type: Array as PropType<ChatMessageVO[]>, - required: true - } -}) - -const { list } = toRefs(props) // 消息列表 - -const emits = defineEmits(['onDeleteSuccess', 'onRefresh', 'onEdit']) // 定义 emits - -// ============ 处理对话滚动 ============== - -/** 滚动到底部 */ -const scrollToBottom = async (isIgnore?: boolean) => { - // 注意要使用 nextTick 以免获取不到 dom - await nextTick() - if (isIgnore || !isScrolling.value) { - messageContainer.value.scrollTop = - messageContainer.value.scrollHeight - messageContainer.value.offsetHeight - } -} - -function handleScroll() { - const scrollContainer = messageContainer.value - const scrollTop = scrollContainer.scrollTop - const scrollHeight = scrollContainer.scrollHeight - const offsetHeight = scrollContainer.offsetHeight - if (scrollTop + offsetHeight < scrollHeight - 100) { - // 用户开始滚动并在最底部之上,取消保持在最底部的效果 - isScrolling.value = true - } else { - // 用户停止滚动并滚动到最底部,开启保持到最底部的效果 - isScrolling.value = false - } -} - -/** 回到底部 */ -const handleGoBottom = async () => { - const scrollContainer = messageContainer.value - scrollContainer.scrollTop = scrollContainer.scrollHeight -} - -/** 回到顶部 */ -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) => { - // 删除 message - await ChatMessageApi.deleteChatMessage(id) - message.success('删除成功!') - // 回调 - emits('onDeleteSuccess') -} - -/** 刷新 */ -const onRefresh = async (message: ChatMessageVO) => { - emits('onRefresh', message) -} - -/** 编辑 */ -const onEdit = async (message: ChatMessageVO) => { - emits('onEdit', message) -} - -/** 初始化 */ -onMounted(async () => { - messageContainer.value.addEventListener('scroll', handleScroll) -}) -</script> - -<style scoped lang="scss"> -.message-container { - position: relative; - overflow-y: scroll; -} - -// 中间 -.chat-list { - display: flex; - flex-direction: column; - overflow-y: hidden; - padding: 0 20px; - .message-item { - margin-top: 50px; - } - - .left-message { - display: flex; - flex-direction: row; - } - - .right-message { - display: flex; - flex-direction: row-reverse; - justify-content: flex-start; - } - - .message { - display: flex; - flex-direction: column; - text-align: left; - margin: 0 15px; - - .time { - text-align: left; - line-height: 30px; - } - - .left-text-container { - position: relative; - display: flex; - flex-direction: column; - overflow-wrap: break-word; - background-color: rgba(228, 228, 228, 0.8); - box-shadow: 0 0 0 1px rgba(228, 228, 228, 0.8); - border-radius: 10px; - padding: 10px 10px 5px 10px; - - .left-text { - color: #393939; - font-size: 0.95rem; - } - } - - .right-text-container { - display: flex; - flex-direction: row-reverse; - - .right-text { - font-size: 0.95rem; - color: #fff; - display: inline; - background-color: #267fff; - box-shadow: 0 0 0 1px #267fff; - border-radius: 10px; - padding: 10px; - width: auto; - overflow-wrap: break-word; - white-space: pre-wrap; - } - } - - .left-btns { - display: flex; - flex-direction: row; - margin-top: 8px; - } - - .right-btns { - display: flex; - flex-direction: row-reverse; - margin-top: 8px; - } - } - - // 复制、删除按钮 - .btn-cus { - display: flex; - background-color: transparent; - align-items: center; - - .btn-image { - height: 20px; - } - } - - .btn-cus:hover { - cursor: pointer; - background-color: #f6f6f6; - } -} - -// 回到底部 -.to-bottom { - position: absolute; - z-index: 1000; - bottom: 0; - right: 50%; -} -</style> diff --git a/src/views/ai/chat/index/components/message/MessageListEmpty.vue b/src/views/ai/chat/index/components/message/MessageListEmpty.vue deleted file mode 100644 index e4d4539..0000000 --- a/src/views/ai/chat/index/components/message/MessageListEmpty.vue +++ /dev/null @@ -1,83 +0,0 @@ -<!-- 消息列表为空时,展示 prompt 列表 --> -<template> - <div class="chat-empty"> - <!-- title --> - <div class="center-container"> - <div class="title">工业互联网平台 AI</div> - <div class="role-list"> - <div - class="role-item" - v-for="prompt in promptList" - :key="prompt.prompt" - @click="handlerPromptClick(prompt)" - > - {{ prompt.prompt }} - </div> - </div> - </div> - </div> -</template> -<script setup lang="ts"> -const promptList = [ - { - prompt: '今天气怎么样?' - }, - { - prompt: '写一首好听的诗歌?' - } -] // prompt 列表 - -const emits = defineEmits(['onPrompt']) - -/** 选中 prompt 点击 */ -const handlerPromptClick = async ({ prompt }) => { - emits('onPrompt', prompt) -} -</script> -<style scoped lang="scss"> -.chat-empty { - position: relative; - display: flex; - flex-direction: row; - justify-content: center; - width: 100%; - height: 100%; - - .center-container { - display: flex; - flex-direction: column; - justify-content: center; - - .title { - font-size: 28px; - font-weight: bold; - text-align: center; - } - - .role-list { - display: flex; - flex-direction: row; - flex-wrap: wrap; - align-items: center; - justify-content: center; - width: 460px; - margin-top: 20px; - - .role-item { - display: flex; - justify-content: center; - width: 180px; - line-height: 50px; - border: 1px solid #e4e4e4; - border-radius: 10px; - margin: 10px; - cursor: pointer; - } - - .role-item:hover { - background-color: rgba(243, 243, 243, 0.73); - } - } - } -} -</style> diff --git a/src/views/ai/chat/index/components/message/MessageLoading.vue b/src/views/ai/chat/index/components/message/MessageLoading.vue deleted file mode 100644 index f3198cb..0000000 --- a/src/views/ai/chat/index/components/message/MessageLoading.vue +++ /dev/null @@ -1,15 +0,0 @@ -<!-- message 加载页面 --> -<template> - <div class="message-loading" > - <el-skeleton animated /> - </div> -</template> - -<script setup lang="ts"> - -</script> -<style scoped lang="scss"> -.message-loading { - padding: 30px 30px; -} -</style> diff --git a/src/views/ai/chat/index/components/message/MessageNewConversation.vue b/src/views/ai/chat/index/components/message/MessageNewConversation.vue deleted file mode 100644 index 40c3107..0000000 --- a/src/views/ai/chat/index/components/message/MessageNewConversation.vue +++ /dev/null @@ -1,46 +0,0 @@ -<!-- 无聊天对话时,在 message 区域,可以新增对话 --> -<template> - <div class="new-chat"> - <div class="box-center"> - <div class="tip">点击下方按钮,开始你的对话吧</div> - <div class="btns"> - <el-button type="primary" round @click="handlerNewChat">新建对话</el-button> - </div> - </div> - </div> -</template> -<script setup lang="ts"> -const emits = defineEmits(['onNewConversation']) - -/** 新建 conversation 聊天对话 */ -const handlerNewChat = () => { - emits('onNewConversation') -} -</script> -<style scoped lang="scss"> -.new-chat { - display: flex; - flex-direction: row; - justify-content: center; - width: 100%; - height: 100%; - - .box-center { - display: flex; - flex-direction: column; - justify-content: center; - - .tip { - font-size: 14px; - color: #858585; - } - - .btns { - display: flex; - flex-direction: row; - justify-content: center; - margin-top: 20px; - } - } -} -</style> diff --git a/src/views/ai/chat/index/components/role/RoleCategoryList.vue b/src/views/ai/chat/index/components/role/RoleCategoryList.vue deleted file mode 100644 index c02126d..0000000 --- a/src/views/ai/chat/index/components/role/RoleCategoryList.vue +++ /dev/null @@ -1,53 +0,0 @@ -<template> - <div class="category-list"> - <div class="category" v-for="category in categoryList" :key="category"> - <el-button - plain - round - size="small" - :type="category === active ? 'primary' : ''" - @click="handleCategoryClick(category)" - > - {{ category }} - </el-button> - </div> - </div> -</template> -<script setup lang="ts"> -import { PropType } from 'vue' - -// 定义属性 -defineProps({ - categoryList: { - type: Array as PropType<string[]>, - required: true - }, - active: { - type: String, - required: false, - default: '全部' - } -}) - -// 定义回调 -const emits = defineEmits(['onCategoryClick']) - -/** 处理分类点击事件 */ -const handleCategoryClick = async (category: string) => { - emits('onCategoryClick', category) -} -</script> -<style scoped lang="scss"> -.category-list { - display: flex; - flex-direction: row; - flex-wrap: wrap; - align-items: center; - - .category { - display: flex; - flex-direction: row; - margin-right: 10px; - } -} -</style> diff --git a/src/views/ai/chat/index/components/role/RoleHeader.vue b/src/views/ai/chat/index/components/role/RoleHeader.vue deleted file mode 100644 index 17b1693..0000000 --- a/src/views/ai/chat/index/components/role/RoleHeader.vue +++ /dev/null @@ -1,48 +0,0 @@ -<!-- header --> -<template> - <el-header class="chat-header"> - <div class="title"> - {{ title }} - </div> - <div class="title-right"> - <slot></slot> - </div> - </el-header> -</template> - -<script setup lang="ts"> -// 设置组件属性 -defineProps({ - title: { - type: String, - required: true - } -}) -</script> - -<style scoped lang="scss"> -.chat-header { - display: flex; - flex-direction: row; - justify-content: space-between; - align-items: center; - padding: 0 10px; - white-space: nowrap; - text-overflow: ellipsis; - background-color: #ececec; - width: 100%; - - .title { - font-size: 20px; - font-weight: bold; - overflow: hidden; - color: #3e3e3e; - max-width: 220px; - } - - .title-right { - display: flex; - flex-direction: row; - } -} -</style> diff --git a/src/views/ai/chat/index/components/role/RoleList.vue b/src/views/ai/chat/index/components/role/RoleList.vue deleted file mode 100644 index b148b22..0000000 --- a/src/views/ai/chat/index/components/role/RoleList.vue +++ /dev/null @@ -1,174 +0,0 @@ -<template> - <div class="card-list" ref="tabsRef" @scroll="handleTabsScroll"> - <div class="card-item" v-for="role in roleList" :key="role.id"> - <el-card class="card" body-class="card-body"> - <!-- 更多操作 --> - <div class="more-container" v-if="showMore"> - <el-dropdown @command="handleMoreClick"> - <span class="el-dropdown-link"> - <el-button type="text"> - <el-icon><More /></el-icon> - </el-button> - </span> - <template #dropdown> - <el-dropdown-menu> - <el-dropdown-item :command="['edit', role]"> - <Icon icon="ep:edit" color="#787878" />编辑 - </el-dropdown-item> - <el-dropdown-item :command="['delete', role]" style="color: red"> - <Icon icon="ep:delete" color="red" />删除 - </el-dropdown-item> - </el-dropdown-menu> - </template> - </el-dropdown> - </div> - <!-- 角色信息 --> - <div> - <img class="avatar" :src="role.avatar" /> - </div> - <div class="right-container"> - <div class="content-container"> - <div class="title">{{ role.name }}</div> - <div class="description">{{ role.description }}</div> - </div> - <div class="btn-container"> - <el-button type="primary" size="small" @click="handleUseClick(role)">使用</el-button> - </div> - </div> - </el-card> - </div> - </div> -</template> - -<script setup lang="ts"> -import {ChatRoleVO} from '@/api/ai/model/chatRole' -import {PropType, ref} from 'vue' -import {More} from '@element-plus/icons-vue' - -const tabsRef = ref<any>() // tabs ref - -// 定义属性 -const props = defineProps({ - loading: { - type: Boolean, - required: true - }, - roleList: { - type: Array as PropType<ChatRoleVO[]>, - required: true - }, - showMore: { - type: Boolean, - required: false, - default: false - } -}) - -// 定义钩子 -const emits = defineEmits(['onDelete', 'onEdit', 'onUse', 'onPage']) - -/** 操作:编辑、删除 */ -const handleMoreClick = async (data) => { - const type = data[0] - const role = data[1] - if (type === 'delete') { - emits('onDelete', role) - } else { - emits('onEdit', role) - } -} - -/** 选中 */ -const handleUseClick = (role) => { - emits('onUse', role) -} - -/** 滚动 */ -const handleTabsScroll = async () => { - if (tabsRef.value) { - const { scrollTop, scrollHeight, clientHeight } = tabsRef.value - if (scrollTop + clientHeight >= scrollHeight - 20 && !props.loading) { - await emits('onPage') - } - } -} -</script> - -<style lang="scss"> -// 重写 card 组件 body 样式 -.card-body { - max-width: 240px; - width: 240px; - padding: 15px 15px 10px 15px; - - display: flex; - flex-direction: row; - justify-content: flex-start; - position: relative; -} -</style> -<style scoped lang="scss"> -// 卡片列表 -.card-list { - display: flex; - flex-direction: row; - flex-wrap: wrap; - position: relative; - height: 100%; - overflow: auto; - padding: 0px 25px; - padding-bottom: 140px; - align-items: start; - align-content: flex-start; - justify-content: start; - - .card { - display: inline-block; - margin-right: 20px; - border-radius: 10px; - margin-bottom: 20px; - position: relative; - - .more-container { - position: absolute; - top: 0; - right: 12px; - } - - .avatar { - width: 40px; - height: 40px; - border-radius: 10px; - overflow: hidden; - } - - .right-container { - margin-left: 10px; - width: 100%; - //height: 100px; - - .content-container { - height: 85px; - - .title { - font-size: 18px; - font-weight: bold; - color: #3e3e3e; - } - - .description { - margin-top: 10px; - font-size: 14px; - color: #6a6a6a; - } - } - - .btn-container { - display: flex; - flex-direction: row-reverse; - margin-top: 2px; - } - } - } -} -</style> diff --git a/src/views/ai/chat/index/components/role/RoleRepository.vue b/src/views/ai/chat/index/components/role/RoleRepository.vue deleted file mode 100644 index 246dcb4..0000000 --- a/src/views/ai/chat/index/components/role/RoleRepository.vue +++ /dev/null @@ -1,289 +0,0 @@ -<!-- chat 角色仓库 --> -<template> - <el-container class="role-container"> - <ChatRoleForm ref="formRef" @success="handlerAddRoleSuccess" /> - <!-- header --> - <RoleHeader title="角色仓库" class="relative" /> - <!-- main --> - <el-main class="role-main"> - <div class="search-container"> - <!-- 搜索按钮 --> - <el-input - :loading="loading" - v-model="search" - class="search-input" - size="default" - placeholder="请输入搜索的内容" - :suffix-icon="Search" - @change="getActiveTabsRole" - /> - <el-button - v-if="activeTab == 'my-role'" - type="primary" - @click="handlerAddRole" - class="ml-20px" - > - <Icon icon="ep:user" style="margin-right: 5px;" /> - 添加角色 - </el-button> - </div> - <!-- tabs --> - <el-tabs v-model="activeTab" class="tabs" @tab-click="handleTabsClick"> - <el-tab-pane class="role-pane" label="我的角色" name="my-role"> - <RoleList - :loading="loading" - :role-list="myRoleList" - :show-more="true" - @on-delete="handlerCardDelete" - @on-edit="handlerCardEdit" - @on-use="handlerCardUse" - @on-page="handlerCardPage('my')" - class="mt-20px" - /> - </el-tab-pane> - <el-tab-pane label="公共角色" name="public-role"> - <RoleCategoryList - class="role-category-list" - :category-list="categoryList" - :active="activeCategory" - @on-category-click="handlerCategoryClick" - /> - <RoleList - :role-list="publicRoleList" - @on-delete="handlerCardDelete" - @on-edit="handlerCardEdit" - @on-use="handlerCardUse" - @on-page="handlerCardPage('public')" - class="mt-20px" - loading - /> - </el-tab-pane> - </el-tabs> - </el-main> - </el-container> -</template> - -<script setup lang="ts"> -import {ref} from 'vue' -import RoleHeader from './RoleHeader.vue' -import RoleList from './RoleList.vue' -import ChatRoleForm from '@/views/ai/model/chatRole/ChatRoleForm.vue' -import RoleCategoryList from './RoleCategoryList.vue' -import {ChatRoleApi, ChatRolePageReqVO, ChatRoleVO} from '@/api/ai/model/chatRole' -import {ChatConversationApi, ChatConversationVO} from '@/api/ai/chat/conversation' -import {Search} from '@element-plus/icons-vue' -import {TabsPaneContext} from 'element-plus' - -const router = useRouter() // 路由对象 - -// 属性定义 -const loading = ref<boolean>(false) // 加载中 -const activeTab = ref<string>('my-role') // 选中的角色 Tab -const search = ref<string>('') // 加载中 -const myRoleParams = reactive({ - pageNo: 1, - pageSize: 50 -}) -const myRoleList = ref<ChatRoleVO[]>([]) // my 分页大小 -const publicRoleParams = reactive({ - pageNo: 1, - pageSize: 50 -}) -const publicRoleList = ref<ChatRoleVO[]>([]) // public 分页大小 -const activeCategory = ref<string>('全部') // 选择中的分类 -const categoryList = ref<string[]>([]) // 角色分类类别 - -/** tabs 点击 */ -const handleTabsClick = async (tab: TabsPaneContext) => { - // 设置切换状态 - activeTab.value = tab.paneName + '' - // 切换的时候重新加载数据 - await getActiveTabsRole() -} - -/** 获取 my role 我的角色 */ -const getMyRole = async (append?: boolean) => { - const params: ChatRolePageReqVO = { - ...myRoleParams, - name: search.value, - publicStatus: false - } - const { list } = await ChatRoleApi.getMyPage(params) - if (append) { - myRoleList.value.push.apply(myRoleList.value, list) - } else { - myRoleList.value = list - } -} - -/** 获取 public role 公共角色 */ -const getPublicRole = async (append?: boolean) => { - const params: ChatRolePageReqVO = { - ...publicRoleParams, - category: activeCategory.value === '全部' ? '' : activeCategory.value, - name: search.value, - publicStatus: true - } - const { total, list } = await ChatRoleApi.getMyPage(params) - if (append) { - publicRoleList.value.push.apply(publicRoleList.value, list) - } else { - publicRoleList.value = list - } -} - -/** 获取选中的 tabs 角色 */ -const getActiveTabsRole = async () => { - if (activeTab.value === 'my-role') { - myRoleParams.pageNo = 1 - await getMyRole() - } else { - publicRoleParams.pageNo = 1 - await getPublicRole() - } -} - -/** 获取角色分类列表 */ -const getRoleCategoryList = async () => { - categoryList.value = ['全部', ...(await ChatRoleApi.getCategoryList())] -} - -/** 处理分类点击 */ -const handlerCategoryClick = async (category: string) => { - // 切换选择的分类 - activeCategory.value = category - // 筛选 - await getActiveTabsRole() -} - -/** 添加/修改操作 */ -const formRef = ref() -const handlerAddRole = async () => { - formRef.value.open('my-create', null, '添加角色') -} -/** 编辑角色 */ -const handlerCardEdit = async (role) => { - formRef.value.open('my-update', role.id, '编辑角色') -} - -/** 添加角色成功 */ -const handlerAddRoleSuccess = async (e) => { - // 刷新数据 - await getActiveTabsRole() -} - -/** 删除角色 */ -const handlerCardDelete = async (role) => { - await ChatRoleApi.deleteMy(role.id) - // 刷新数据 - await getActiveTabsRole() -} - -/** 角色分页:获取下一页 */ -const handlerCardPage = async (type) => { - try { - loading.value = true - if (type === 'public') { - publicRoleParams.pageNo++ - await getPublicRole(true) - } else { - myRoleParams.pageNo++ - await getMyRole(true) - } - } finally { - loading.value = false - } -} - -/** 选择 card 角色:新建聊天对话 */ -const handlerCardUse = async (role) => { - // 1. 创建对话 - const data: ChatConversationVO = { - roleId: role.id - } as unknown as ChatConversationVO - const conversationId = await ChatConversationApi.createChatConversationMy(data) - - // 2. 跳转页面 - await router.push({ - name: 'AiChat', - query: { - conversationId: conversationId - } - }) -} - -/** 初始化 **/ -onMounted(async () => { - // 获取分类 - await getRoleCategoryList() - // 获取 role 数据 - await getActiveTabsRole() -}) -</script> -<!-- 覆盖 element ui css --> -<style lang="scss"> -.el-tabs__content { - position: relative; - height: 100%; - overflow: hidden; -} -.el-tabs__nav-scroll { - margin: 10px 20px; -} -</style> -<!-- 样式 --> -<style scoped lang="scss"> -// 跟容器 -.role-container { - position: absolute; - width: 100%; - height: 100%; - margin: 0; - padding: 0; - left: 0; - right: 0; - top: 0; - bottom: 0; - background-color: #ffffff; - overflow: hidden; - display: flex; - flex-direction: column; - - .role-main { - flex: 1; - overflow: hidden; - margin: 0; - padding: 0; - position: relative; - - .search-container { - margin: 20px 20px 0px 20px; - position: absolute; - right: 0; - top: -5px; - z-index: 100; - } - - .search-input { - width: 240px; - } - - .tabs { - position: relative; - height: 100%; - - .role-category-list { - margin: 0 27px; - } - } - - .role-pane { - display: flex; - flex-direction: column; - height: 100%; - overflow-y: auto; - position: relative; - } - } -} -</style> diff --git a/src/views/ai/chat/index/index.vue b/src/views/ai/chat/index/index.vue deleted file mode 100644 index 28f1d65..0000000 --- a/src/views/ai/chat/index/index.vue +++ /dev/null @@ -1,772 +0,0 @@ -<template> - <el-container class="ai-layout"> - <!-- 左侧:对话列表 --> - <ConversationList - :active-id="activeConversationId" - ref="conversationListRef" - @on-conversation-create="handleConversationCreateSuccess" - @on-conversation-click="handleConversationClick" - @on-conversation-clear="handleConversationClear" - @on-conversation-delete="handlerConversationDelete" - /> - <!-- 右侧:对话详情 --> - <el-container class="detail-container"> - <el-header class="header"> - <div class="title"> - {{ activeConversation?.title ? activeConversation?.title : '对话' }} - <span v-if="activeMessageList.length">({{ activeMessageList.length }})</span> - </div> - <div class="btns" v-if="activeConversation"> - <el-button type="primary" bg plain size="small" @click="openChatConversationUpdateForm"> - <span v-html="activeConversation?.modelName"></span> - <Icon icon="ep:setting" class="ml-10px" /> - </el-button> - <el-button size="small" class="btn" @click="handlerMessageClear"> - <Icon icon="heroicons-outline:archive-box-x-mark" color="#787878" /> - </el-button> - <el-button size="small" class="btn"> - <Icon icon="ep:download" color="#787878" /> - </el-button> - <el-button size="small" class="btn" @click="handleGoTopMessage"> - <Icon icon="ep:top" color="#787878" /> - </el-button> - </div> - </el-header> - - <!-- main:消息列表 --> - <el-main class="main-container"> - <div> - <div class="message-container"> - <!-- 情况一:消息加载中 --> - <MessageLoading v-if="activeMessageListLoading" /> - <!-- 情况二:无聊天对话时 --> - <MessageNewConversation - v-if="!activeConversation" - @on-new-conversation="handleConversationCreate" - /> - <!-- 情况三:消息列表为空 --> - <MessageListEmpty - v-if="!activeMessageListLoading && messageList.length === 0 && activeConversation" - @on-prompt="doSendMessage" - /> - <!-- 情况四:消息列表不为空 --> - <MessageList - v-if="!activeMessageListLoading && messageList.length > 0" - ref="messageRef" - :conversation="activeConversation" - :list="messageList" - @on-delete-success="handleMessageDelete" - @on-edit="handleMessageEdit" - @on-refresh="handleMessageRefresh" - /> - </div> - </div> - </el-main> - - <!-- 底部 --> - <el-footer class="footer-container"> - <form class="prompt-from"> - <textarea - class="prompt-input" - v-model="prompt" - @keydown="handleSendByKeydown" - @input="handlePromptInput" - @compositionstart="onCompositionstart" - @compositionend="onCompositionend" - placeholder="问我任何问题...(Shift+Enter 换行,按下 Enter 发送)" - ></textarea> - <div class="prompt-btns"> - <div> - <el-switch v-model="enableContext" /> - <span class="ml-5px text-14px text-#8f8f8f">上下文</span> - </div> - <el-button - type="primary" - size="default" - @click="handleSendByButton" - :loading="conversationInProgress" - v-if="conversationInProgress == false" - > - {{ conversationInProgress ? '进行中' : '发送' }} - </el-button> - <el-button - type="danger" - size="default" - @click="stopStream()" - v-if="conversationInProgress == true" - > - 停止 - </el-button> - </div> - </form> - </el-footer> - </el-container> - - <!-- 更新对话 Form --> - <ConversationUpdateForm - ref="conversationUpdateFormRef" - @success="handleConversationUpdateSuccess" - /> - </el-container> -</template> - -<script setup lang="ts"> -import { ChatMessageApi, ChatMessageVO } from '@/api/ai/chat/message' -import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation' -import ConversationList from './components/conversation/ConversationList.vue' -import ConversationUpdateForm from './components/conversation/ConversationUpdateForm.vue' -import MessageList from './components/message/MessageList.vue' -import MessageListEmpty from './components/message/MessageListEmpty.vue' -import MessageLoading from './components/message/MessageLoading.vue' -import MessageNewConversation from './components/message/MessageNewConversation.vue' - -/** AI 聊天对话 列表 */ -defineOptions({ name: 'AiChat' }) - -const route = useRoute() // 路由 -const message = useMessage() // 消息弹窗 - -// 聊天对话 -const conversationListRef = ref() -const activeConversationId = ref<number | null>(null) // 选中的对话编号 -const activeConversation = ref<ChatConversationVO | null>(null) // 选中的 Conversation -const conversationInProgress = ref(false) // 对话是否正在进行中。目前只有【发送】消息时,会更新为 true,避免切换对话、删除对话等操作 - -// 消息列表 -const messageRef = ref() -const activeMessageList = ref<ChatMessageVO[]>([]) // 选中对话的消息列表 -const activeMessageListLoading = ref<boolean>(false) // activeMessageList 是否正在加载中 -const activeMessageListLoadingTimer = ref<any>() // activeMessageListLoading Timer 定时器。如果加载速度很快,就不进入加载中 -// 消息滚动 -const textSpeed = ref<number>(50) // Typing speed in milliseconds -const textRoleRunning = ref<boolean>(false) // Typing speed in milliseconds - -// 发送消息输入框 -const isComposing = ref(false) // 判断用户是否在输入 -const conversationInAbortController = ref<any>() // 对话进行中 abort 控制器(控制 stream 对话) -const inputTimeout = ref<any>() // 处理输入中回车的定时器 -const prompt = ref<string>() // prompt -const enableContext = ref<boolean>(true) // 是否开启上下文 -// 接收 Stream 消息 -const receiveMessageFullText = ref('') -const receiveMessageDisplayedText = ref('') - -// =========== 【聊天对话】相关 =========== - -/** 获取对话信息 */ -const getConversation = async (id: number | null) => { - if (!id) { - return - } - const conversation: ChatConversationVO = await ChatConversationApi.getChatConversationMy(id) - if (!conversation) { - return - } - activeConversation.value = conversation - activeConversationId.value = conversation.id -} - -/** - * 点击某个对话 - * - * @param conversation 选中的对话 - * @return 是否切换成功 - */ -const handleConversationClick = async (conversation: ChatConversationVO) => { - // 对话进行中,不允许切换 - if (conversationInProgress.value) { - message.alert('对话中,不允许切换!') - return false - } - - // 更新选中的对话 id - activeConversationId.value = conversation.id - activeConversation.value = conversation - // 刷新 message 列表 - await getMessageList() - // 滚动底部 - scrollToBottom(true) - // 清空输入框 - prompt.value = '' - return true -} - -/** 删除某个对话*/ -const handlerConversationDelete = async (delConversation: ChatConversationVO) => { - // 删除的对话如果是当前选中的,那么就重置 - if (activeConversationId.value === delConversation.id) { - await handleConversationClear() - } -} -/** 清空选中的对话 */ -const handleConversationClear = async () => { - // 对话进行中,不允许切换 - if (conversationInProgress.value) { - message.alert('对话中,不允许切换!') - return false - } - activeConversationId.value = null - activeConversation.value = null - activeMessageList.value = [] -} - -/** 修改聊天对话 */ -const conversationUpdateFormRef = ref() -const openChatConversationUpdateForm = async () => { - conversationUpdateFormRef.value.open(activeConversationId.value) -} -const handleConversationUpdateSuccess = async () => { - // 对话更新成功,刷新最新信息 - await getConversation(activeConversationId.value) -} - -/** 处理聊天对话的创建成功 */ -const handleConversationCreate = async () => { - // 创建对话 - await conversationListRef.value.createConversation() -} -/** 处理聊天对话的创建成功 */ -const handleConversationCreateSuccess = async () => { - // 创建新的对话,清空输入框 - prompt.value = '' -} - -// =========== 【消息列表】相关 =========== - -/** 获取消息 message 列表 */ -const getMessageList = async () => { - try { - if (activeConversationId.value === null) { - return - } - // Timer 定时器,如果加载速度很快,就不进入加载中 - activeMessageListLoadingTimer.value = setTimeout(() => { - activeMessageListLoading.value = true - }, 60) - - // 获取消息列表 - activeMessageList.value = await ChatMessageApi.getChatMessageListByConversationId( - activeConversationId.value - ) - - // 滚动到最下面 - await nextTick() - await scrollToBottom() - } finally { - // time 定时器,如果加载速度很快,就不进入加载中 - if (activeMessageListLoadingTimer.value) { - clearTimeout(activeMessageListLoadingTimer.value) - } - // 加载结束 - activeMessageListLoading.value = false - } -} - -/** - * 消息列表 - * - * 和 {@link #getMessageList()} 的差异是,把 systemMessage 考虑进去 - */ -const messageList = computed(() => { - if (activeMessageList.value.length > 0) { - return activeMessageList.value - } - // 没有消息时,如果有 systemMessage 则展示它 - if (activeConversation.value?.systemMessage) { - return [ - { - id: 0, - type: 'system', - content: activeConversation.value.systemMessage - } - ] - } - return [] -}) - -/** 处理删除 message 消息 */ -const handleMessageDelete = () => { - if (conversationInProgress.value) { - message.alert('回答中,不能删除!') - return - } - // 刷新 message 列表 - getMessageList() -} - -/** 处理 message 清空 */ -const handlerMessageClear = async () => { - if (!activeConversationId.value) { - return - } - try { - // 确认提示 - await message.delConfirm('确认清空对话消息?') - // 清空对话 - await ChatMessageApi.deleteByConversationId(activeConversationId.value) - // 刷新 message 列表 - activeMessageList.value = [] - } catch {} -} - -/** 回到 message 列表的顶部 */ -const handleGoTopMessage = () => { - messageRef.value.handlerGoTop() -} - -// =========== 【发送消息】相关 =========== - -/** 处理来自 keydown 的发送消息 */ -const handleSendByKeydown = async (event) => { - // 判断用户是否在输入 - if (isComposing.value) { - return - } - // 进行中不允许发送 - if (conversationInProgress.value) { - return - } - const content = prompt.value?.trim() as string - if (event.key === 'Enter') { - if (event.shiftKey) { - // 插入换行 - prompt.value += '\r\n' - event.preventDefault() // 防止默认的换行行为 - } else { - // 发送消息 - await doSendMessage(content) - event.preventDefault() // 防止默认的提交行为 - } - } -} - -/** 处理来自【发送】按钮的发送消息 */ -const handleSendByButton = () => { - doSendMessage(prompt.value?.trim() as string) -} - -/** 处理 prompt 输入变化 */ -const handlePromptInput = (event) => { - // 非输入法 输入设置为 true - if (!isComposing.value) { - // 回车 event data 是 null - if (event.data == null) { - return - } - isComposing.value = true - } - // 清理定时器 - if (inputTimeout.value) { - clearTimeout(inputTimeout.value) - } - // 重置定时器 - inputTimeout.value = setTimeout(() => { - isComposing.value = false - }, 400) -} -// TODO @芋艿:是不是可以通过 @keydown.enter、@keydown.shift.enter 来实现,回车发送、shift+回车换行;主要看看,是不是可以简化 isComposing 相关的逻辑 -const onCompositionstart = () => { - isComposing.value = true -} -const onCompositionend = () => { - // console.log('输入结束...') - setTimeout(() => { - isComposing.value = false - }, 200) -} - -/** 真正执行【发送】消息操作 */ -const doSendMessage = async (content: string) => { - // 校验 - if (content.length < 1) { - message.error('发送失败,原因:内容为空!') - return - } - if (activeConversationId.value == null) { - message.error('还没创建对话,不能发送!') - return - } - // 清空输入框 - prompt.value = '' - // 执行发送 - await doSendMessageStream({ - conversationId: activeConversationId.value, - content: content - } as ChatMessageVO) -} - -/** 真正执行【发送】消息操作 */ -const doSendMessageStream = async (userMessage: ChatMessageVO) => { - // 创建 AbortController 实例,以便中止请求 - conversationInAbortController.value = new AbortController() - // 标记对话进行中 - conversationInProgress.value = true - // 设置为空 - receiveMessageFullText.value = '' - - try { - // 1.1 先添加两个假数据,等 stream 返回再替换 - activeMessageList.value.push({ - id: -1, - conversationId: activeConversationId.value, - type: 'user', - content: userMessage.content, - createTime: new Date() - } as ChatMessageVO) - activeMessageList.value.push({ - id: -2, - conversationId: activeConversationId.value, - type: 'assistant', - content: '思考中...', - createTime: new Date() - } as ChatMessageVO) - // 1.2 滚动到最下面 - await nextTick() - await scrollToBottom() // 底部 - // 1.3 开始滚动 - textRoll() - - // 2. 发送 event stream - let isFirstChunk = true // 是否是第一个 chunk 消息段 - await ChatMessageApi.sendChatMessageStream( - userMessage.conversationId, - userMessage.content, - conversationInAbortController.value, - enableContext.value, - async (res) => { - const { code, data, msg } = JSON.parse(res.data) - if (code !== 0) { - message.alert(`对话异常! ${msg}`) - return - } - - // 如果内容为空,就不处理。 - if (data.receive.content === '') { - return - } - // 首次返回需要添加一个 message 到页面,后面的都是更新 - if (isFirstChunk) { - isFirstChunk = false - // 弹出两个假数据 - activeMessageList.value.pop() - activeMessageList.value.pop() - // 更新返回的数据 - activeMessageList.value.push(data.send) - activeMessageList.value.push(data.receive) - } - // debugger - receiveMessageFullText.value = receiveMessageFullText.value + data.receive.content - // 滚动到最下面 - await scrollToBottom() - }, - (error) => { - message.alert(`对话异常! ${error}`) - stopStream() - }, - () => { - stopStream() - } - ) - } catch {} -} - -/** 停止 stream 流式调用 */ -const stopStream = async () => { - // tip:如果 stream 进行中的 message,就需要调用 controller 结束 - if (conversationInAbortController.value) { - conversationInAbortController.value.abort() - } - // 设置为 false - conversationInProgress.value = false -} - -/** 编辑 message:设置为 prompt,可以再次编辑 */ -const handleMessageEdit = (message: ChatMessageVO) => { - prompt.value = message.content -} - -/** 刷新 message:基于指定消息,再次发起对话 */ -const handleMessageRefresh = (message: ChatMessageVO) => { - doSendMessage(message.content) -} - -// ============== 【消息滚动】相关 ============= - -/** 滚动到 message 底部 */ -const scrollToBottom = async (isIgnore?: boolean) => { - await nextTick() - if (messageRef.value) { - messageRef.value.scrollToBottom(isIgnore) - } -} - -/** 自提滚动效果 */ -const textRoll = async () => { - let index = 0 - try { - // 只能执行一次 - if (textRoleRunning.value) { - return - } - // 设置状态 - textRoleRunning.value = true - receiveMessageDisplayedText.value = '' - const task = async () => { - // 调整速度 - const diff = - (receiveMessageFullText.value.length - receiveMessageDisplayedText.value.length) / 10 - if (diff > 5) { - textSpeed.value = 10 - } else if (diff > 2) { - textSpeed.value = 30 - } else if (diff > 1.5) { - textSpeed.value = 50 - } else { - textSpeed.value = 100 - } - // 对话结束,就按 30 的速度 - if (!conversationInProgress.value) { - textSpeed.value = 10 - } - - if (index < receiveMessageFullText.value.length) { - receiveMessageDisplayedText.value += receiveMessageFullText.value[index] - index++ - - // 更新 message - const lastMessage = activeMessageList.value[activeMessageList.value.length - 1] - lastMessage.content = receiveMessageDisplayedText.value - // 滚动到住下面 - await scrollToBottom() - // 重新设置任务 - timer = setTimeout(task, textSpeed.value) - } else { - // 不是对话中可以结束 - if (!conversationInProgress.value) { - textRoleRunning.value = false - clearTimeout(timer) - } else { - // 重新设置任务 - timer = setTimeout(task, textSpeed.value) - } - } - } - let timer = setTimeout(task, textSpeed.value) - } catch {} -} - -/** 初始化 **/ -onMounted(async () => { - // 如果有 conversationId 参数,则默认选中 - if (route.query.conversationId) { - const id = route.query.conversationId as unknown as number - activeConversationId.value = id - await getConversation(id) - } - - // 获取列表数据 - activeMessageListLoading.value = true - await getMessageList() -}) -</script> - -<style lang="scss" scoped> -.ai-layout { - position: absolute; - flex: 1; - top: 0; - left: 0; - height: 100%; - width: 100%; -} - -.conversation-container { - position: relative; - display: flex; - flex-direction: column; - justify-content: space-between; - padding: 10px 10px 0; - - .btn-new-conversation { - padding: 18px 0; - } - - .search-input { - margin-top: 20px; - } - - .conversation-list { - margin-top: 20px; - - .conversation { - display: flex; - flex-direction: row; - justify-content: space-between; - flex: 1; - padding: 0 5px; - margin-top: 10px; - cursor: pointer; - border-radius: 5px; - align-items: center; - line-height: 30px; - - &.active { - background-color: #e6e6e6; - - .button { - display: inline-block; - } - } - - .title-wrapper { - display: flex; - flex-direction: row; - align-items: center; - } - - .title { - padding: 5px 10px; - max-width: 220px; - font-size: 14px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - } - - .avatar { - width: 28px; - height: 28px; - display: flex; - flex-direction: row; - justify-items: center; - } - - // 对话编辑、删除 - .button-wrapper { - right: 2px; - display: flex; - flex-direction: row; - justify-items: center; - color: #606266; - - .el-icon { - margin-right: 5px; - } - } - } - } - - // 角色仓库、清空未设置对话 - .tool-box { - line-height: 35px; - display: flex; - justify-content: space-between; - align-items: center; - color: var(--el-text-color); - - > div { - display: flex; - align-items: center; - color: #606266; - padding: 0; - margin: 0; - cursor: pointer; - - > span { - margin-left: 5px; - } - } - } -} - -// 头部 -.detail-container { - background: #ffffff; - - .header { - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - background: #fbfbfb; - box-shadow: 0 0 0 0 #dcdfe6; - - .title { - font-size: 18px; - font-weight: bold; - } - - .btns { - display: flex; - width: 300px; - flex-direction: row; - justify-content: flex-end; - //justify-content: space-between; - - .btn { - padding: 10px; - } - } - } -} - -// main 容器 -.main-container { - margin: 0; - padding: 0; - position: relative; - height: 100%; - width: 100%; - - .message-container { - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - overflow-y: hidden; - padding: 0; - margin: 0; - } -} - -// 底部 -.footer-container { - display: flex; - flex-direction: column; - height: auto; - margin: 0; - padding: 0; - - .prompt-from { - display: flex; - flex-direction: column; - height: auto; - border: 1px solid #e3e3e3; - border-radius: 10px; - margin: 10px 20px 20px 20px; - padding: 9px 10px; - } - - .prompt-input { - height: 80px; - //box-shadow: none; - border: none; - box-sizing: border-box; - resize: none; - padding: 0 2px; - overflow: auto; - } - - .prompt-input:focus { - outline: none; - } - - .prompt-btns { - display: flex; - justify-content: space-between; - padding-bottom: 0; - padding-top: 5px; - } -} -</style> diff --git a/src/views/ai/chat/manager/ChatConversationList.vue b/src/views/ai/chat/manager/ChatConversationList.vue deleted file mode 100644 index 23933f0..0000000 --- a/src/views/ai/chat/manager/ChatConversationList.vue +++ /dev/null @@ -1,163 +0,0 @@ -<template> - <ContentWrap> - <!-- 搜索工作栏 --> - <el-form - class="-mb-15px" - :model="queryParams" - ref="queryFormRef" - :inline="true" - label-width="68px" - > - <el-form-item label="用户编号" prop="userId"> - <el-select - v-model="queryParams.userId" - clearable - placeholder="请输入用户编号" - class="!w-240px" - > - <el-option - v-for="item in userList" - :key="item.id" - :label="item.nickname" - :value="item.id" - /> - </el-select> - </el-form-item> - <el-form-item label="聊天编号" prop="title"> - <el-input - v-model="queryParams.title" - placeholder="请输入聊天编号" - clearable - @keyup.enter="handleQuery" - class="!w-240px" - /> - </el-form-item> - <el-form-item label="创建时间" prop="createTime"> - <el-date-picker - v-model="queryParams.createTime" - value-format="YYYY-MM-DD HH:mm:ss" - type="daterange" - start-placeholder="开始日期" - end-placeholder="结束日期" - :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" - class="!w-240px" - /> - </el-form-item> - <el-form-item> - <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> - <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> - </el-form-item> - </el-form> - </ContentWrap> - - <!-- 列表 --> - <ContentWrap> - <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"> - <el-table-column label="对话编号" align="center" prop="id" width="180" fixed="left" /> - <el-table-column label="对话标题" align="center" prop="title" width="180" fixed="left" /> - <el-table-column label="用户" align="center" prop="userId" width="180"> - <template #default="scope"> - <span>{{ userList.find((item) => item.id === scope.row.userId)?.nickname }}</span> - </template> - </el-table-column> - <el-table-column label="角色" align="center" prop="roleName" width="180" /> - <el-table-column label="模型标识" align="center" prop="model" width="180" /> - <el-table-column label="消息数" align="center" prop="messageCount" /> - <el-table-column - label="创建时间" - align="center" - prop="createTime" - :formatter="dateFormatter" - width="180px" - /> - <el-table-column label="温度参数" align="center" prop="temperature" /> - <el-table-column label="回复 Token 数" align="center" prop="maxTokens" width="120" /> - <el-table-column label="上下文数量" align="center" prop="maxContexts" width="120" /> - <el-table-column label="操作" align="center" width="180" fixed="right"> - <template #default="scope"> - <el-button - link - type="danger" - @click="handleDelete(scope.row.id)" - v-hasPermi="['ai:chat-conversation:delete']" - > - 删除 - </el-button> - </template> - </el-table-column> - </el-table> - <!-- 分页 --> - <Pagination - :total="total" - v-model:page="queryParams.pageNo" - v-model:limit="queryParams.pageSize" - @pagination="getList" - /> - </ContentWrap> -</template> - -<script setup lang="ts"> -import { dateFormatter } from '@/utils/formatTime' -import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation' -import * as UserApi from '@/api/system/user' - -const message = useMessage() // 消息弹窗 -const { t } = useI18n() // 国际化 - -const loading = ref(true) // 列表的加载中 -const list = ref<ChatConversationVO[]>([]) // 列表的数据 -const total = ref(0) // 列表的总页数 -const queryParams = reactive({ - pageNo: 1, - pageSize: 10, - userId: undefined, - title: undefined, - createTime: [] -}) -const queryFormRef = ref() // 搜索的表单 -const userList = ref<UserApi.UserVO[]>([]) // 用户列表 - -/** 查询列表 */ -const getList = async () => { - loading.value = true - try { - const data = await ChatConversationApi.getChatConversationPage(queryParams) - list.value = data.list - total.value = data.total - } finally { - loading.value = false - } -} - -/** 搜索按钮操作 */ -const handleQuery = () => { - queryParams.pageNo = 1 - getList() -} - -/** 重置按钮操作 */ -const resetQuery = () => { - queryFormRef.value.resetFields() - handleQuery() -} - -/** 删除按钮操作 */ -const handleDelete = async (id: number) => { - try { - // 删除的二次确认 - await message.delConfirm() - // 发起删除 - await ChatConversationApi.deleteChatConversationByAdmin(id) - message.success(t('common.delSuccess')) - // 刷新列表 - await getList() - } catch {} -} - -/** 初始化 **/ -onMounted(async () => { - getList() - // 获得用户列表 - userList.value = await UserApi.getSimpleUserList() -}) -</script> diff --git a/src/views/ai/chat/manager/ChatMessageList.vue b/src/views/ai/chat/manager/ChatMessageList.vue deleted file mode 100644 index 0d84184..0000000 --- a/src/views/ai/chat/manager/ChatMessageList.vue +++ /dev/null @@ -1,175 +0,0 @@ -<template> - <ContentWrap> - <!-- 搜索工作栏 --> - <el-form - class="-mb-15px" - :model="queryParams" - ref="queryFormRef" - :inline="true" - label-width="68px" - > - <el-form-item label="对话编号" prop="conversationId"> - <el-input - v-model="queryParams.conversationId" - placeholder="请输入对话编号" - clearable - @keyup.enter="handleQuery" - class="!w-240px" - /> - </el-form-item> - <el-form-item label="用户编号" prop="userId"> - <el-select - v-model="queryParams.userId" - clearable - placeholder="请输入用户编号" - class="!w-240px" - > - <el-option - v-for="item in userList" - :key="item.id" - :label="item.nickname" - :value="item.id" - /> - </el-select> - </el-form-item> - <el-form-item label="创建时间" prop="createTime"> - <el-date-picker - v-model="queryParams.createTime" - value-format="YYYY-MM-DD HH:mm:ss" - type="daterange" - start-placeholder="开始日期" - end-placeholder="结束日期" - :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" - class="!w-240px" - /> - </el-form-item> - <el-form-item> - <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> - <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> - </el-form-item> - </el-form> - </ContentWrap> - - <!-- 列表 --> - <ContentWrap> - <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"> - <el-table-column label="消息编号" align="center" prop="id" width="180" fixed="left" /> - <el-table-column - label="对话编号" - align="center" - prop="conversationId" - width="180" - fixed="left" - /> - <el-table-column label="用户" align="center" prop="userId" width="180"> - <template #default="scope"> - <span>{{ userList.find((item) => item.id === scope.row.userId)?.nickname }}</span> - </template> - </el-table-column> - <el-table-column label="角色" align="center" prop="roleName" width="180" /> - <el-table-column label="消息类型" align="center" prop="type" width="100" /> - <el-table-column label="模型标识" align="center" prop="model" width="180" /> - <el-table-column label="消息内容" align="center" prop="content" width="300" /> - <el-table-column - label="创建时间" - align="center" - prop="createTime" - :formatter="dateFormatter" - width="180px" - /> - <el-table-column label="回复消息编号" align="center" prop="replyId" width="180" /> - <el-table-column label="携带上下文" align="center" prop="useContext" width="100"> - <template #default="scope"> - <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.useContext" /> - </template> - </el-table-column> - <el-table-column label="操作" align="center" fixed="right"> - <template #default="scope"> - <el-button - link - type="danger" - @click="handleDelete(scope.row.id)" - v-hasPermi="['ai:chat-message:delete']" - > - 删除 - </el-button> - </template> - </el-table-column> - </el-table> - <!-- 分页 --> - <Pagination - :total="total" - v-model:page="queryParams.pageNo" - v-model:limit="queryParams.pageSize" - @pagination="getList" - /> - </ContentWrap> -</template> - -<script setup lang="ts"> -import { dateFormatter } from '@/utils/formatTime' -import { ChatMessageApi, ChatMessageVO } from '@/api/ai/chat/message' -import * as UserApi from '@/api/system/user' -import { DICT_TYPE } from '@/utils/dict' - -const message = useMessage() // 消息弹窗 -const { t } = useI18n() // 国际化 - -const loading = ref(true) // 列表的加载中 -const list = ref<ChatMessageVO[]>([]) // 列表的数据 -const total = ref(0) // 列表的总页数 -const queryParams = reactive({ - pageNo: 1, - pageSize: 10, - conversationId: undefined, - userId: undefined, - content: undefined, - createTime: [] -}) -const queryFormRef = ref() // 搜索的表单 -const userList = ref<UserApi.UserVO[]>([]) // 用户列表 - -/** 查询列表 */ -const getList = async () => { - loading.value = true - try { - const data = await ChatMessageApi.getChatMessagePage(queryParams) - list.value = data.list - total.value = data.total - } finally { - loading.value = false - } -} - -/** 搜索按钮操作 */ -const handleQuery = () => { - queryParams.pageNo = 1 - getList() -} - -/** 重置按钮操作 */ -const resetQuery = () => { - queryFormRef.value.resetFields() - handleQuery() -} - -/** 删除按钮操作 */ -const handleDelete = async (id: number) => { - try { - // 删除的二次确认 - await message.delConfirm() - // 发起删除 - await ChatMessageApi.deleteChatMessageByAdmin(id) - message.success(t('common.delSuccess')) - // 刷新列表 - await getList() - } catch {} -} - -/** 初始化 **/ -onMounted(async () => { - getList() - // 获得用户列表 - userList.value = await UserApi.getSimpleUserList() -}) -</script> diff --git a/src/views/ai/chat/manager/index.vue b/src/views/ai/chat/manager/index.vue deleted file mode 100644 index ca2d092..0000000 --- a/src/views/ai/chat/manager/index.vue +++ /dev/null @@ -1,20 +0,0 @@ -<template> - <ContentWrap> - <el-tabs> - <el-tab-pane label="对话列表"> - <ChatConversationList /> - </el-tab-pane> - <el-tab-pane label="消息列表"> - <ChatMessageList /> - </el-tab-pane> - </el-tabs> - </ContentWrap> -</template> - -<script setup lang="ts"> -import ChatConversationList from './ChatConversationList.vue' -import ChatMessageList from './ChatMessageList.vue' - -/** AI 聊天对话 列表 */ -defineOptions({ name: 'AiChatManager' }) -</script> diff --git a/src/views/ai/image/index/components/ImageCard.vue b/src/views/ai/image/index/components/ImageCard.vue deleted file mode 100644 index 4ba78ca..0000000 --- a/src/views/ai/image/index/components/ImageCard.vue +++ /dev/null @@ -1,162 +0,0 @@ -<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> diff --git a/src/views/ai/image/index/components/ImageDetail.vue b/src/views/ai/image/index/components/ImageDetail.vue deleted file mode 100644 index ad15aa8..0000000 --- a/src/views/ai/image/index/components/ImageDetail.vue +++ /dev/null @@ -1,224 +0,0 @@ -<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> diff --git a/src/views/ai/image/index/components/ImageList.vue b/src/views/ai/image/index/components/ImageList.vue deleted file mode 100644 index 9ffde77..0000000 --- a/src/views/ai/image/index/components/ImageList.vue +++ /dev/null @@ -1,245 +0,0 @@ -<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(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> diff --git a/src/views/ai/image/index/components/dall3/index.vue b/src/views/ai/image/index/components/dall3/index.vue deleted file mode 100644 index 5c891ab..0000000 --- a/src/views/ai/image/index/components/dall3/index.vue +++ /dev/null @@ -1,320 +0,0 @@ -<!-- 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="model"> - <div> - <el-text tag="b">模型选择</el-text> - </div> - <el-space wrap class="model-list"> - <div - :class="selectModel === model.key ? 'modal-item selectModel' : 'modal-item'" - v-for="model in Dall3Models" - :key="model.key" - > - <el-image :src="model.image" fit="contain" @click="handleModelClick(model)" /> - <div class="model-font">{{ model.name }}</div> - </div> - </el-space> - </div> - <div class="image-style"> - <div> - <el-text tag="b">风格选择</el-text> - </div> - <el-space wrap class="image-style-list"> - <div - :class="style === imageStyle.key ? 'image-style-item selectImageStyle' : 'image-style-item'" - v-for="imageStyle in Dall3StyleList" - :key="imageStyle.key" - > - <el-image :src="imageStyle.image" fit="contain" @click="handleStyleClick(imageStyle)" /> - <div class="style-font">{{ imageStyle.name }}</div> - </div> - </el-space> - </div> - <div class="image-size"> - <div> - <el-text tag="b">画面比例</el-text> - </div> - <el-space wrap class="size-list"> - <div - class="size-item" - v-for="imageSize in Dall3SizeList" - :key="imageSize.key" - @click="handleSizeClick(imageSize)" - > - <div - :class="selectSize === imageSize.key ? 'size-wrapper selectImageSize' : 'size-wrapper'" - > - <div :style="imageSize.style"></div> - </div> - <div class="size-font">{{ imageSize.name }}</div> - </div> - </el-space> - </div> - <div class="btns"> - <el-button type="primary" size="large" round :loading="drawIn" @click="handleGenerateImage"> - {{ drawIn ? '生成中' : '生成内容' }} - </el-button> - </div> -</template> -<script setup lang="ts"> -import { ImageApi, ImageDrawReqVO, ImageVO } from '@/api/ai/image' -import { - Dall3Models, - Dall3StyleList, - ImageHotWords, - Dall3SizeList, - ImageModelVO, - AiPlatformEnum -} from '@/views/ai/utils/constants' - -const message = useMessage() // 消息弹窗 - -// 定义属性 -const prompt = ref<string>('') // 提示词 -const drawIn = ref<boolean>(false) // 生成中 -const selectHotWord = ref<string>('') // 选中的热词 -const selectModel = ref<string>('dall-e-3') // 模型 -const selectSize = ref<string>('1024x1024') // 选中 size -const style = ref<string>('vivid') // style 样式 - -const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // 定义 emits - -/** 选择热词 */ -const handleHotWordClick = async (hotWord: string) => { - // 情况一:取消选中 - if (selectHotWord.value == hotWord) { - selectHotWord.value = '' - return - } - - // 情况二:选中 - selectHotWord.value = hotWord - prompt.value = hotWord -} - -/** 选择 model 模型 */ -const handleModelClick = async (model: ImageModelVO) => { - selectModel.value = model.key -} - -/** 选择 style 样式 */ -const handleStyleClick = async (imageStyle: ImageModelVO) => { - style.value = imageStyle.key -} - -/** 选择 size 大小 */ -const handleSizeClick = async (imageSize: ImageSizeVO) => { - selectSize.value = imageSize.key -} - -/** 图片生产 */ -const handleGenerateImage = async () => { - // 二次确认 - await message.confirm(`确认生成内容?`) - try { - // 加载中 - drawIn.value = true - // 回调 - emits('onDrawStart', AiPlatformEnum.OPENAI) - const imageSize = Dall3SizeList.find((item) => item.key === selectSize.value) as ImageSizeVO - const form = { - platform: AiPlatformEnum.OPENAI, - prompt: prompt.value, // 提示词 - model: selectModel.value, // 模型 - width: imageSize.width, // size 不能为空 - height: imageSize.height, // size 不能为空 - options: { - style: style.value // 图像生成的风格 - } - } as ImageDrawReqVO - // 发送请求 - await ImageApi.drawImage(form) - } finally { - // 回调 - emits('onDrawComplete', AiPlatformEnum.OPENAI) - // 加载结束 - drawIn.value = false - } -} - -/** 填充值 */ -const settingValues = async (detail: ImageVO) => { - prompt.value = detail.prompt - selectModel.value = detail.model - style.value = detail.options?.style - const imageSize = Dall3SizeList.find( - (item) => item.key === `${detail.width}x${detail.height}` - ) as ImageSizeVO - await handleSizeClick(imageSize) -} - -/** 暴露组件方法 */ -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; - } - } -} - -// 模型 -.model { - margin-top: 30px; - - .model-list { - margin-top: 15px; - - .modal-item { - width: 110px; - //outline: 1px solid blue; - overflow: hidden; - display: flex; - flex-direction: column; - align-items: center; - border: 3px solid transparent; - cursor: pointer; - - .model-font { - font-size: 14px; - color: #3e3e3e; - font-weight: bold; - } - } - - .selectModel { - border: 3px solid #1293ff; - border-radius: 5px; - } - } -} - -// 样式 style -.image-style { - margin-top: 30px; - - .image-style-list { - margin-top: 15px; - - .image-style-item { - width: 110px; - //outline: 1px solid blue; - overflow: hidden; - display: flex; - flex-direction: column; - align-items: center; - border: 3px solid transparent; - cursor: pointer; - - .style-font { - font-size: 14px; - color: #3e3e3e; - font-weight: bold; - } - } - - .selectImageStyle { - border: 3px solid #1293ff; - border-radius: 5px; - } - } -} - -// 尺寸 -.image-size { - width: 100%; - margin-top: 30px; - - .size-list { - display: flex; - flex-direction: row; - justify-content: space-between; - width: 100%; - margin-top: 20px; - - .size-item { - display: flex; - flex-direction: column; - align-items: center; - cursor: pointer; - - .size-wrapper { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - border-radius: 7px; - padding: 4px; - width: 50px; - height: 50px; - background-color: #fff; - border: 1px solid #fff; - } - - .size-font { - font-size: 14px; - color: #3e3e3e; - font-weight: bold; - } - } - } - - .selectImageSize { - border: 1px solid #1293ff !important; - } -} - -.btns { - display: flex; - justify-content: center; - margin-top: 50px; -} -</style> diff --git a/src/views/ai/image/index/components/midjourney/index.vue b/src/views/ai/image/index/components/midjourney/index.vue deleted file mode 100644 index 1d7fda1..0000000 --- a/src/views/ai/image/index/components/midjourney/index.vue +++ /dev/null @@ -1,326 +0,0 @@ -<!-- 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="image-size"> - <div> - <el-text tag="b">尺寸</el-text> - </div> - <el-space wrap class="size-list"> - <div - class="size-item" - v-for="imageSize in MidjourneySizeList" - :key="imageSize.key" - @click="handleSizeClick(imageSize)" - > - <div - :class="selectSize === imageSize.key ? 'size-wrapper selectImageSize' : 'size-wrapper'" - > - <div :style="imageSize.style"></div> - </div> - <div class="size-font">{{ imageSize.key }}</div> - </div> - </el-space> - </div> - <div class="model"> - <div> - <el-text tag="b">模型</el-text> - </div> - <el-space wrap class="model-list"> - <div - :class="selectModel === model.key ? 'modal-item selectModel' : 'modal-item'" - v-for="model in MidjourneyModels" - :key="model.key" - > - <el-image :src="model.image" fit="contain" @click="handleModelClick(model)" /> - <div class="model-font">{{ model.name }}</div> - </div> - </el-space> - </div> - <div class="version"> - <div> - <el-text tag="b">版本</el-text> - </div> - <el-space wrap class="version-list"> - <el-select - v-model="selectVersion" - class="version-select !w-350px" - clearable - placeholder="请选择版本" - > - <el-option - v-for="item in versionList" - :key="item.value" - :label="item.label" - :value="item.value" - /> - </el-select> - </el-space> - </div> - <div class="model"> - <div> - <el-text tag="b">参考图</el-text> - </div> - <el-space wrap class="model-list"> - <UploadImg v-model="referImageUrl" height="120px" width="120px" /> - </el-space> - </div> - <div class="btns"> - <el-button type="primary" size="large" round @click="handleGenerateImage"> - {{ drawIn ? '生成中' : '生成内容' }} - </el-button> - </div> -</template> -<script setup lang="ts"> -import { ImageApi, ImageMidjourneyImagineReqVO, ImageVO } from '@/api/ai/image' -import { - AiPlatformEnum, - ImageHotWords, - ImageSizeVO, - ImageModelVO, - MidjourneyModels, - MidjourneySizeList, - MidjourneyVersions, - NijiVersionList -} from '@/views/ai/utils/constants' - -const message = useMessage() // 消息弹窗 - -// 定义属性 -const drawIn = ref<boolean>(false) // 生成中 -const selectHotWord = ref<string>('') // 选中的热词 -// 表单 -const prompt = ref<string>('') // 提示词 -const referImageUrl = ref<any>() // 参考图 -const selectModel = ref<string>('midjourney') // 选中的模型 -const selectSize = ref<string>('1:1') // 选中 size -const selectVersion = ref<any>('6.0') // 选中的 version -const versionList = ref<any>(MidjourneyVersions) // version 列表 -const emits = defineEmits(['onDrawStart', 'onDrawComplete']) // 定义 emits - -/** 选择热词 */ -const handleHotWordClick = async (hotWord: string) => { - // 情况一:取消选中 - if (selectHotWord.value == hotWord) { - selectHotWord.value = '' - return - } - - // 情况二:选中 - selectHotWord.value = hotWord // 选中 - prompt.value = hotWord // 设置提示次 -} - -/** 点击 size 尺寸 */ -const handleSizeClick = async (imageSize: ImageSizeVO) => { - selectSize.value = imageSize.key -} - -/** 点击 model 模型 */ -const handleModelClick = async (model: ImageModelVO) => { - selectModel.value = model.key - if (model.key === 'niji') { - versionList.value = NijiVersionList // 默认选择 niji - } else { - versionList.value = MidjourneyVersions // 默认选择 midjourney - } - selectVersion.value = versionList.value[0].value -} - -/** 图片生成 */ -const handleGenerateImage = async () => { - // 二次确认 - await message.confirm(`确认生成内容?`) - try { - // 加载中 - drawIn.value = true - // 回调 - emits('onDrawStart', AiPlatformEnum.MIDJOURNEY) - // 发送请求 - const imageSize = MidjourneySizeList.find( - (item) => selectSize.value === item.key - ) as ImageSizeVO - const req = { - prompt: prompt.value, - model: selectModel.value, - width: imageSize.width, - height: imageSize.height, - version: selectVersion.value, - referImageUrl: referImageUrl.value - } as ImageMidjourneyImagineReqVO - await ImageApi.midjourneyImagine(req) - } finally { - // 回调 - emits('onDrawComplete', AiPlatformEnum.MIDJOURNEY) - // 加载结束 - drawIn.value = false - } -} - -/** 填充值 */ -const settingValues = async (detail: ImageVO) => { - // 提示词 - prompt.value = detail.prompt - // image size - const imageSize = MidjourneySizeList.find( - (item) => item.key === `${detail.width}:${detail.height}` - ) as ImageSizeVO - selectSize.value = imageSize.key - // 选中模型 - const model = MidjourneyModels.find((item) => item.key === detail.options?.model) as ImageModelVO - await handleModelClick(model) - // 版本 - selectVersion.value = versionList.value.find( - (item) => item.value === detail.options?.version - ).value - // image - referImageUrl.value = detail.options.referImageUrl -} - -/** 暴露组件方法 */ -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; - } - } -} - -// version -.version { - margin-top: 20px; - - .version-list { - margin-top: 20px; - width: 100%; - } -} - -// 模型 -.model { - margin-top: 30px; - - .model-list { - margin-top: 15px; - - .modal-item { - display: flex; - flex-direction: column; - align-items: center; - width: 150px; - //outline: 1px solid blue; - overflow: hidden; - border: 3px solid transparent; - cursor: pointer; - - .model-font { - font-size: 14px; - color: #3e3e3e; - font-weight: bold; - } - } - - .selectModel { - border: 3px solid #1293ff; - border-radius: 5px; - } - } -} - -// 尺寸 -.image-size { - width: 100%; - margin-top: 30px; - - .size-list { - display: flex; - flex-direction: row; - justify-content: space-between; - width: 100%; - margin-top: 20px; - - .size-item { - display: flex; - flex-direction: column; - align-items: center; - cursor: pointer; - - .size-wrapper { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - border-radius: 7px; - padding: 4px; - width: 50px; - height: 50px; - background-color: #fff; - border: 1px solid #fff; - } - - .size-font { - font-size: 14px; - color: #3e3e3e; - font-weight: bold; - } - } - } - - .selectImageSize { - border: 1px solid #1293ff !important; - } -} - -.btns { - display: flex; - justify-content: center; - margin-top: 50px; -} -</style> diff --git a/src/views/ai/image/index/components/other/index.vue b/src/views/ai/image/index/components/other/index.vue deleted file mode 100644 index a688be1..0000000 --- a/src/views/ai/image/index/components/other/index.vue +++ /dev/null @@ -1,216 +0,0 @@ -<!-- 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> diff --git a/src/views/ai/image/index/components/stableDiffusion/index.vue b/src/views/ai/image/index/components/stableDiffusion/index.vue deleted file mode 100644 index 169938f..0000000 --- a/src/views/ai/image/index/components/stableDiffusion/index.vue +++ /dev/null @@ -1,272 +0,0 @@ -<!-- 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 ImageHotEnglishWords" - :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="sampler" placeholder="Select" size="large" class="!w-350px"> - <el-option - v-for="item in StableDiffusionSamplers" - :key="item.key" - :label="item.name" - :value="item.key" - /> - </el-select> - </el-space> - </div> - <div class="group-item"> - <div> - <el-text tag="b">CLIP</el-text> - </div> - <el-space wrap class="group-item-body"> - <el-select v-model="clipGuidancePreset" placeholder="Select" size="large" class="!w-350px"> - <el-option - v-for="item in StableDiffusionClipGuidancePresets" - :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="stylePreset" placeholder="Select" size="large" class="!w-350px"> - <el-option - v-for="item in StableDiffusionStylePresets" - :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" class="w-170px" placeholder="图片宽度" /> - <el-input v-model="height" class="w-170px" placeholder="图片高度" /> - </el-space> - </div> - <div class="group-item"> - <div> - <el-text tag="b">迭代步数</el-text> - </div> - <el-space wrap class="group-item-body"> - <el-input - v-model="steps" - type="number" - size="large" - class="!w-350px" - placeholder="Please input" - /> - </el-space> - </div> - <div class="group-item"> - <div> - <el-text tag="b">引导系数</el-text> - </div> - <el-space wrap class="group-item-body"> - <el-input - v-model="scale" - type="number" - size="large" - class="!w-350px" - placeholder="Please input" - /> - </el-space> - </div> - <div class="group-item"> - <div> - <el-text tag="b">随机因子</el-text> - </div> - <el-space wrap class="group-item-body"> - <el-input - v-model="seed" - type="number" - size="large" - class="!w-350px" - placeholder="Please input" - /> - </el-space> - </div> - <div class="btns"> - <el-button type="primary" size="large" round :loading="drawIn" @click="handleGenerateImage"> - {{ drawIn ? '生成中' : '生成内容' }} - </el-button> - </div> -</template> -<script setup lang="ts"> -import { ImageApi, ImageDrawReqVO, ImageVO } from '@/api/ai/image' -import { hasChinese } from '@/views/ai/utils/utils' -import { - AiPlatformEnum, - ImageHotEnglishWords, - StableDiffusionClipGuidancePresets, - StableDiffusionSamplers, - StableDiffusionStylePresets -} 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 sampler = ref<string>('DDIM') // 采样方法 -const steps = ref<number>(20) // 迭代步数 -const seed = ref<number>(42) // 控制生成图像的随机性 -const scale = ref<number>(7.5) // 引导系数 -const clipGuidancePreset = ref<string>('NONE') // 文本提示相匹配的图像(clip_guidance_preset) 简称 CLIP -const stylePreset = ref<string>('3d-model') // 风格 - -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 () => { - // 二次确认 - if (hasChinese(prompt.value)) { - message.alert('暂不支持中文!') - return - } - await message.confirm(`确认生成内容?`) - - try { - // 加载中 - drawIn.value = true - // 回调 - emits('onDrawStart', AiPlatformEnum.STABLE_DIFFUSION) - // 发送请求 - const form = { - platform: AiPlatformEnum.STABLE_DIFFUSION, - model: 'stable-diffusion-v1-6', - prompt: prompt.value, // 提示词 - width: width.value, // 图片宽度 - height: height.value, // 图片高度 - options: { - seed: seed.value, // 随机种子 - steps: steps.value, // 图片生成步数 - scale: scale.value, // 引导系数 - sampler: sampler.value, // 采样算法 - clipGuidancePreset: clipGuidancePreset.value, // 文本提示相匹配的图像 CLIP - stylePreset: stylePreset.value // 风格 - } - } 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 - seed.value = detail.options?.seed - steps.value = detail.options?.steps - scale.value = detail.options?.scale - sampler.value = detail.options?.sampler - clipGuidancePreset.value = detail.options?.clipGuidancePreset - stylePreset.value = detail.options?.stylePreset -} - -/** 暴露组件方法 */ -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> diff --git a/src/views/ai/image/index/index.vue b/src/views/ai/image/index/index.vue deleted file mode 100644 index 1217e79..0000000 --- a/src/views/ai/image/index/index.vue +++ /dev/null @@ -1,141 +0,0 @@ -<!-- image --> -<template> - <div class="ai-image"> - <div class="left"> - <div class="segmented"> - <el-segmented v-model="selectPlatform" :options="platformOptions" /> - </div> - <div class="modal-switch-container"> - <Dall3 - v-if="selectPlatform === AiPlatformEnum.OPENAI" - ref="dall3Ref" - @on-draw-start="handleDrawStart" - @on-draw-complete="handleDrawComplete" - /> - <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 class="main"> - <ImageList ref="imageListRef" @on-regeneration="handleRegeneration" /> - </div> - </div> -</template> - -<script setup lang="ts"> -import ImageList from './components/ImageList.vue' -import { AiPlatformEnum } from '@/views/ai/utils/constants' -import { ImageVO } from '@/api/ai/image' -import Dall3 from './components/dall3/index.vue' -import Midjourney from './components/midjourney/index.vue' -import StableDiffusion from './components/stableDiffusion/index.vue' -import Other from './components/other/index.vue' - -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 selectPlatform = ref(AiPlatformEnum.MIDJOURNEY) -const platformOptions = [ - { - label: 'DALL3 绘画', - value: AiPlatformEnum.OPENAI - }, - { - label: 'MJ 绘画', - value: AiPlatformEnum.MIDJOURNEY - }, - { - label: 'Stable Diffusion', - value: AiPlatformEnum.STABLE_DIFFUSION - }, - { - label: '其它', - value: 'other' - } -] - -/** 绘画 start */ -const handleDrawStart = async (platform: string) => {} - -/** 绘画 complete */ -const handleDrawComplete = async (platform: string) => { - await imageListRef.value.getImageList() -} - -/** 重新生成:将画图详情填充到对应平台 */ -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> - -<style scoped lang="scss"> -.ai-image { - position: absolute; - left: 0; - right: 0; - bottom: 0; - top: 0; - - display: flex; - flex-direction: row; - height: 100%; - width: 100%; - - .left { - display: flex; - flex-direction: column; - padding: 20px; - width: 350px; - - .segmented { - } - - .segmented .el-segmented { - --el-border-radius-base: 16px; - --el-segmented-item-selected-color: #fff; - background-color: #ececec; - width: 350px; - } - - .modal-switch-container { - height: 100%; - overflow-y: auto; - margin-top: 30px; - } - } - - .main { - flex: 1; - background-color: #fff; - } - - .right { - width: 350px; - background-color: #f7f8fa; - } -} -</style> diff --git a/src/views/ai/image/manager/index.vue b/src/views/ai/image/manager/index.vue deleted file mode 100644 index 84403f3..0000000 --- a/src/views/ai/image/manager/index.vue +++ /dev/null @@ -1,251 +0,0 @@ -<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> diff --git a/src/views/ai/image/square/index.vue b/src/views/ai/image/square/index.vue deleted file mode 100644 index 3da6cde..0000000 --- a/src/views/ai/image/square/index.vue +++ /dev/null @@ -1,104 +0,0 @@ -<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> diff --git a/src/views/ai/mindmap/index/components/Left.vue b/src/views/ai/mindmap/index/components/Left.vue deleted file mode 100644 index e684b88..0000000 --- a/src/views/ai/mindmap/index/components/Left.vue +++ /dev/null @@ -1,78 +0,0 @@ -<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> diff --git a/src/views/ai/mindmap/index/components/Right.vue b/src/views/ai/mindmap/index/components/Right.vue deleted file mode 100644 index 0550650..0000000 --- a/src/views/ai/mindmap/index/components/Right.vue +++ /dev/null @@ -1,175 +0,0 @@ -<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' - -const md = markdownit() -const message = useMessage() // 消息弹窗 - -// TODO @hhero:mindmap 改成 mindMap 更精准哈 -const props = defineProps<{ - mindmapResult: string // 生成结果 TODO @hhero 改成 generatedContent 会不会好点 - 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, ({ mindmapResult, isGenerating, isEnd, isStart }) => { - // 开始生成的时候清空一下 markdown 的内容 - if (isStart) { - html.value = '' - } - // 生成内容的时候使用 markdown 来渲染 - if (isGenerating) { - html.value = md.render(mindmapResult) - } - if (isEnd) { - update() - } -}) - -/** 更新思维导图的展示 */ -const update = () => { - try { - const { root } = transformer.transform(processContent(props.mindmapResult)) - 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') -} - -/** 下载图片 */ -// TODO @hhhero:可以抽到 download 这个里面,src/utils/download.ts 么?复用 image 方法? -// 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 image = new Image() - image.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(source) - - // 将图片对象渲染 - const canvas = document.createElement('canvas') - canvas.width = svgElement?.offsetWidth || 0 - canvas.height = svgElement?.offsetHeight || 0 - let context = canvas.getContext('2d') - context?.clearRect(0, 0, canvas.width, canvas.height) - - image.onload = function () { - context?.drawImage(image, 0, 0) - const a = document.createElement('a') - a.download = 'mindmap.png' - a.href = canvas.toDataURL(`image/png`) - a.click() - } -} - -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; - } -} -// markmap的tool样式覆盖 -:deep(.markmap) { - width: 100%; -} -:deep(.mm-toolbar-brand) { - display: none; -} -:deep(.mm-toolbar) { - display: flex; - flex-direction: row; -} -</style> diff --git a/src/views/ai/mindmap/index/index.vue b/src/views/ai/mindmap/index/index.vue deleted file mode 100644 index bae7408..0000000 --- a/src/views/ai/mindmap/index/index.vue +++ /dev/null @@ -1,92 +0,0 @@ -<template> - <div class="absolute top-0 left-0 right-0 bottom-0 flex"> - <!--表单区域--> - <Left - ref="leftRef" - @submit="submit" - @direct-generate="directGenerate" - :is-generating="isGenerating" - /> - <!--右边生成思维导图区域--> - <Right - ref="rightRef" - :mindmapResult="mindmapResult" - :isEnd="isEnd" - :isGenerating="isGenerating" - :isStart="isStart" - /> - </div> -</template> - -<script setup lang="ts"> -import Left from './components/Left.vue' -import Right from './components/Right.vue' -import { AiMindMapApi, AiMindMapGenerateReqVO } from '@/api/ai/mindmap' -import { MindMapContentExample } from '@/views/ai/utils/constants' - -defineOptions({ - name: 'AiMindMap' -}) -const ctrl = ref<AbortController>() // 请求控制 -const isGenerating = ref(false) // 是否正在生成思维导图 -const isStart = ref(false) // 开始生成,用来清空思维导图 -const isEnd = ref(true) // 用来判断结束的时候渲染思维导图 -const message = useMessage() // 消息提示 - -const mindmapResult = ref('') // 生成思维导图结果 - -const leftRef = ref<InstanceType<typeof Left>>() // 左边组件 -const rightRef = ref<InstanceType<typeof Right>>() // 右边组件 - -/** 使用已有内容直接生成 **/ -const directGenerate = (existPrompt: string) => { - isEnd.value = false // 先设置为false再设置为true,让子组建的watch能够监听到 - mindmapResult.value = existPrompt - isEnd.value = true -} - -/** 停止 stream 生成 */ -const stopStream = () => { - isGenerating.value = false - isStart.value = false - ctrl.value?.abort() -} - -/** 提交生成 */ -const submit = (data: AiMindMapGenerateReqVO) => { - isGenerating.value = true - isStart.value = true - isEnd.value = false - ctrl.value = new AbortController() // 请求控制赋值 - mindmapResult.value = '' // 清空生成数据 - AiMindMapApi.generateMindMap({ - data, - onMessage: async (res) => { - const { code, data, msg } = JSON.parse(res.data) - if (code !== 0) { - message.alert(`生成思维导图异常! ${msg}`) - stopStream() - return - } - mindmapResult.value = mindmapResult.value + data - await nextTick() - rightRef.value?.scrollBottom() - }, - onClose() { - isEnd.value = true - leftRef.value?.setGeneratedContent(mindmapResult.value) - stopStream() - }, - onError(err) { - console.error('生成思维导图失败', err) - stopStream() - }, - ctrl: ctrl.value - }) -} - -/** 初始化 */ -onMounted(() => { - mindmapResult.value = MindMapContentExample -}) -</script> diff --git a/src/views/ai/model/apiKey/ApiKeyForm.vue b/src/views/ai/model/apiKey/ApiKeyForm.vue deleted file mode 100644 index a8fc012..0000000 --- a/src/views/ai/model/apiKey/ApiKeyForm.vue +++ /dev/null @@ -1,132 +0,0 @@ -<template> - <Dialog :title="dialogTitle" v-model="dialogVisible"> - <el-form - ref="formRef" - :model="formData" - :rules="formRules" - label-width="120px" - v-loading="formLoading" - > - <el-form-item label="所属平台" prop="platform"> - <el-select v-model="formData.platform" placeholder="请输入平台" clearable> - <el-option - v-for="dict in getStrDictOptions(DICT_TYPE.AI_PLATFORM)" - :key="dict.value" - :label="dict.label" - :value="dict.value" - /> - </el-select> - </el-form-item> - <el-form-item label="名称" prop="name"> - <el-input v-model="formData.name" placeholder="请输入名称" /> - </el-form-item> - <el-form-item label="密钥" prop="apiKey"> - <el-input v-model="formData.apiKey" placeholder="请输入密钥" /> - </el-form-item> - <el-form-item label="自定义 API URL" prop="url"> - <el-input v-model="formData.url" placeholder="请输入自定义 API URL" /> - </el-form-item> - <el-form-item label="状态" prop="status"> - <el-radio-group v-model="formData.status"> - <el-radio - v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" - :key="dict.value" - :label="dict.value" - > - {{ dict.label }} - </el-radio> - </el-radio-group> - </el-form-item> - </el-form> - <template #footer> - <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> - <el-button @click="dialogVisible = false">取 消</el-button> - </template> - </Dialog> -</template> -<script setup lang="ts"> -import { getIntDictOptions, DICT_TYPE, getStrDictOptions } from '@/utils/dict' -import { ApiKeyApi, ApiKeyVO } from '@/api/ai/model/apiKey' -import { CommonStatusEnum } from '@/utils/constants' - -/** AI API 密钥 表单 */ -defineOptions({ name: 'ApiKeyForm' }) - -const { t } = useI18n() // 国际化 -const message = useMessage() // 消息弹窗 - -const dialogVisible = ref(false) // 弹窗的是否展示 -const dialogTitle = ref('') // 弹窗的标题 -const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 -const formType = ref('') // 表单的类型:create - 新增;update - 修改 -const formData = ref({ - id: undefined, - name: undefined, - apiKey: undefined, - platform: undefined, - url: undefined, - status: CommonStatusEnum.ENABLE -}) -const formRules = reactive({ - name: [{ required: true, message: '名称不能为空', trigger: 'blur' }], - apiKey: [{ required: true, message: '密钥不能为空', trigger: 'blur' }], - platform: [{ required: true, message: '平台不能为空', trigger: 'blur' }], - status: [{ required: true, message: '状态不能为空', trigger: 'blur' }] -}) -const formRef = ref() // 表单 Ref - -/** 打开弹窗 */ -const open = async (type: string, id?: number) => { - dialogVisible.value = true - dialogTitle.value = t('action.' + type) - formType.value = type - resetForm() - // 修改时,设置数据 - if (id) { - formLoading.value = true - try { - formData.value = await ApiKeyApi.getApiKey(id) - } finally { - formLoading.value = false - } - } -} -defineExpose({ open }) // 提供 open 方法,用于打开弹窗 - -/** 提交表单 */ -const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 -const submitForm = async () => { - // 校验表单 - await formRef.value.validate() - // 提交请求 - formLoading.value = true - try { - const data = formData.value as unknown as ApiKeyVO - if (formType.value === 'create') { - await ApiKeyApi.createApiKey(data) - message.success(t('common.createSuccess')) - } else { - await ApiKeyApi.updateApiKey(data) - message.success(t('common.updateSuccess')) - } - dialogVisible.value = false - // 发送操作成功的事件 - emit('success') - } finally { - formLoading.value = false - } -} - -/** 重置表单 */ -const resetForm = () => { - formData.value = { - id: undefined, - name: undefined, - apiKey: undefined, - platform: undefined, - url: undefined, - status: CommonStatusEnum.ENABLE - } - formRef.value?.resetFields() -} -</script> diff --git a/src/views/ai/model/apiKey/index.vue b/src/views/ai/model/apiKey/index.vue deleted file mode 100644 index 6daf6a7..0000000 --- a/src/views/ai/model/apiKey/index.vue +++ /dev/null @@ -1,180 +0,0 @@ -<template> - <ContentWrap> - <!-- 搜索工作栏 --> - <el-form - class="-mb-15px" - :model="queryParams" - ref="queryFormRef" - :inline="true" - label-width="68px" - > - <el-form-item label="名称" prop="name"> - <el-input - v-model="queryParams.name" - placeholder="请输入名称" - clearable - @keyup.enter="handleQuery" - class="!w-240px" - /> - </el-form-item> - <el-form-item label="平台" prop="platform"> - <el-select - v-model="queryParams.platform" - 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.COMMON_STATUS)" - :key="dict.value" - :label="dict.label" - :value="dict.value" - /> - </el-select> - </el-form-item> - <el-form-item> - <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> - <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> - <el-button - type="primary" - plain - @click="openForm('create')" - v-hasPermi="['ai:api-key:create']" - > - <Icon icon="ep:plus" class="mr-5px" /> 新增 - </el-button> - </el-form-item> - </el-form> - </ContentWrap> - - <!-- 列表 --> - <ContentWrap> - <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"> - <el-table-column label="所属平台" align="center" prop="platform"> - <template #default="scope"> - <dict-tag :type="DICT_TYPE.AI_PLATFORM" :value="scope.row.platform" /> - </template> - </el-table-column> - <el-table-column label="名称" align="center" prop="name" /> - <el-table-column label="密钥" align="center" prop="apiKey" /> - <el-table-column label="自定义 API URL" align="center" prop="url" /> - <el-table-column label="状态" align="center" prop="status"> - <template #default="scope"> - <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" /> - </template> - </el-table-column> - <el-table-column label="操作" align="center"> - <template #default="scope"> - <el-button - link - type="primary" - @click="openForm('update', scope.row.id)" - v-hasPermi="['ai:api-key:update']" - > - 编辑 - </el-button> - <el-button - link - type="danger" - @click="handleDelete(scope.row.id)" - v-hasPermi="['ai:api-key: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> - - <!-- 表单弹窗:添加/修改 --> - <ApiKeyForm ref="formRef" @success="getList" /> -</template> - -<script setup lang="ts"> -import { getIntDictOptions, DICT_TYPE, getStrDictOptions } from '@/utils/dict' -import { ApiKeyApi, ApiKeyVO } from '@/api/ai/model/apiKey' -import ApiKeyForm from './ApiKeyForm.vue' - -/** AI API 密钥 列表 */ -defineOptions({ name: 'AiApiKey' }) - -const message = useMessage() // 消息弹窗 -const { t } = useI18n() // 国际化 - -const loading = ref(true) // 列表的加载中 -const list = ref<ApiKeyVO[]>([]) // 列表的数据 -const total = ref(0) // 列表的总页数 -const queryParams = reactive({ - pageNo: 1, - pageSize: 10, - name: undefined, - platform: undefined, - status: undefined -}) -const queryFormRef = ref() // 搜索的表单 - -/** 查询列表 */ -const getList = async () => { - loading.value = true - try { - const data = await ApiKeyApi.getApiKeyPage(queryParams) - list.value = data.list - total.value = data.total - } finally { - loading.value = false - } -} - -/** 搜索按钮操作 */ -const handleQuery = () => { - queryParams.pageNo = 1 - getList() -} - -/** 重置按钮操作 */ -const resetQuery = () => { - queryFormRef.value.resetFields() - handleQuery() -} - -/** 添加/修改操作 */ -const formRef = ref() -const openForm = (type: string, id?: number) => { - formRef.value.open(type, id) -} - -/** 删除按钮操作 */ -const handleDelete = async (id: number) => { - try { - // 删除的二次确认 - await message.delConfirm() - // 发起删除 - await ApiKeyApi.deleteApiKey(id) - message.success(t('common.delSuccess')) - // 刷新列表 - await getList() - } catch {} -} - -/** 初始化 **/ -onMounted(() => { - getList() -}) -</script> diff --git a/src/views/ai/model/chatModel/ChatModelForm.vue b/src/views/ai/model/chatModel/ChatModelForm.vue deleted file mode 100644 index e3f785c..0000000 --- a/src/views/ai/model/chatModel/ChatModelForm.vue +++ /dev/null @@ -1,181 +0,0 @@ -<template> - <Dialog :title="dialogTitle" v-model="dialogVisible"> - <el-form - ref="formRef" - :model="formData" - :rules="formRules" - label-width="120px" - v-loading="formLoading" - > - <el-form-item label="所属平台" prop="platform"> - <el-select v-model="formData.platform" placeholder="请输入平台" clearable> - <el-option - v-for="dict in getStrDictOptions(DICT_TYPE.AI_PLATFORM)" - :key="dict.value" - :label="dict.label" - :value="dict.value" - /> - </el-select> - </el-form-item> - <el-form-item label="API 秘钥" prop="keyId"> - <el-select v-model="formData.keyId" placeholder="请选择 API 秘钥" clearable> - <el-option - v-for="apiKey in apiKeyList" - :key="apiKey.id" - :label="apiKey.name" - :value="apiKey.id" - /> - </el-select> - </el-form-item> - <el-form-item label="模型名字" prop="name"> - <el-input v-model="formData.name" placeholder="请输入模型名字" /> - </el-form-item> - <el-form-item label="模型标识" prop="model"> - <el-input v-model="formData.model" placeholder="请输入模型标识" /> - </el-form-item> - <el-form-item label="模型排序" prop="sort"> - <el-input-number v-model="formData.sort" placeholder="请输入模型排序" class="!w-1/1" /> - </el-form-item> - <el-form-item label="开启状态" prop="status"> - <el-radio-group v-model="formData.status"> - <el-radio - v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" - :key="dict.value" - :label="dict.value" - > - {{ dict.label }} - </el-radio> - </el-radio-group> - </el-form-item> - <el-form-item label="温度参数" prop="temperature"> - <el-input-number - v-model="formData.temperature" - placeholder="请输入温度参数" - :min="0" - :max="2" - :precision="2" - /> - </el-form-item> - <el-form-item label="回复数 Token 数" prop="maxTokens"> - <el-input-number - v-model="formData.maxTokens" - placeholder="请输入回复数 Token 数" - :min="0" - :max="4096" - /> - </el-form-item> - <el-form-item label="上下文数量" prop="maxContexts"> - <el-input-number - v-model="formData.maxContexts" - placeholder="请输入上下文数量" - :min="0" - :max="20" - /> - </el-form-item> - </el-form> - <template #footer> - <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> - <el-button @click="dialogVisible = false">取 消</el-button> - </template> - </Dialog> -</template> -<script setup lang="ts"> -import { ChatModelApi, ChatModelVO } from '@/api/ai/model/chatModel' -import { ApiKeyApi, ApiKeyVO } from '@/api/ai/model/apiKey' -import { CommonStatusEnum } from '@/utils/constants' -import { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict' - -/** API 聊天模型 表单 */ -defineOptions({ name: 'ChatModelForm' }) - -const { t } = useI18n() // 国际化 -const message = useMessage() // 消息弹窗 - -const dialogVisible = ref(false) // 弹窗的是否展示 -const dialogTitle = ref('') // 弹窗的标题 -const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 -const formType = ref('') // 表单的类型:create - 新增;update - 修改 -const formData = ref({ - id: undefined, - keyId: undefined, - name: undefined, - model: undefined, - platform: undefined, - sort: undefined, - status: CommonStatusEnum.ENABLE, - temperature: undefined, - maxTokens: undefined, - maxContexts: undefined -}) -const formRules = reactive({ - keyId: [{ required: true, message: 'API 秘钥不能为空', trigger: 'blur' }], - name: [{ required: true, message: '模型名字不能为空', trigger: 'blur' }], - model: [{ required: true, message: '模型标识不能为空', trigger: 'blur' }], - platform: [{ required: true, message: '所属平台不能为空', trigger: 'blur' }], - sort: [{ required: true, message: '排序不能为空', trigger: 'blur' }], - status: [{ required: true, message: '状态不能为空', trigger: 'blur' }] -}) -const formRef = ref() // 表单 Ref -const apiKeyList = ref([] as ApiKeyVO[]) // API 密钥列表 - -/** 打开弹窗 */ -const open = async (type: string, id?: number) => { - dialogVisible.value = true - dialogTitle.value = t('action.' + type) - formType.value = type - resetForm() - // 修改时,设置数据 - if (id) { - formLoading.value = true - try { - formData.value = await ChatModelApi.getChatModel(id) - } finally { - formLoading.value = false - } - } - // 获得下拉数据 - apiKeyList.value = await ApiKeyApi.getApiKeySimpleList(CommonStatusEnum.ENABLE) -} -defineExpose({ open }) // 提供 open 方法,用于打开弹窗 - -/** 提交表单 */ -const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 -const submitForm = async () => { - // 校验表单 - await formRef.value.validate() - // 提交请求 - formLoading.value = true - try { - const data = formData.value as unknown as ChatModelVO - if (formType.value === 'create') { - await ChatModelApi.createChatModel(data) - message.success(t('common.createSuccess')) - } else { - await ChatModelApi.updateChatModel(data) - message.success(t('common.updateSuccess')) - } - dialogVisible.value = false - // 发送操作成功的事件 - emit('success') - } finally { - formLoading.value = false - } -} - -/** 重置表单 */ -const resetForm = () => { - formData.value = { - id: undefined, - keyId: undefined, - name: undefined, - model: undefined, - platform: undefined, - sort: undefined, - status: CommonStatusEnum.ENABLE, - temperature: undefined, - maxTokens: undefined, - maxContexts: undefined - } - formRef.value?.resetFields() -} -</script> diff --git a/src/views/ai/model/chatModel/index.vue b/src/views/ai/model/chatModel/index.vue deleted file mode 100644 index c550674..0000000 --- a/src/views/ai/model/chatModel/index.vue +++ /dev/null @@ -1,185 +0,0 @@ -<template> - <ContentWrap> - <!-- 搜索工作栏 --> - <el-form - class="-mb-15px" - :model="queryParams" - ref="queryFormRef" - :inline="true" - label-width="68px" - > - <el-form-item label="模型名字" prop="name"> - <el-input - v-model="queryParams.name" - placeholder="请输入模型名字" - clearable - @keyup.enter="handleQuery" - class="!w-240px" - /> - </el-form-item> - <el-form-item label="模型标识" prop="model"> - <el-input - v-model="queryParams.model" - placeholder="请输入模型标识" - clearable - @keyup.enter="handleQuery" - class="!w-240px" - /> - </el-form-item> - <el-form-item label="模型平台" prop="platform"> - <el-input - v-model="queryParams.platform" - placeholder="请输入模型平台" - clearable - @keyup.enter="handleQuery" - class="!w-240px" - /> - </el-form-item> - <el-form-item> - <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> - <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> - <el-button - type="primary" - plain - @click="openForm('create')" - v-hasPermi="['ai:chat-model:create']" - > - <Icon icon="ep:plus" class="mr-5px" /> 新增 - </el-button> - </el-form-item> - </el-form> - </ContentWrap> - - <!-- 列表 --> - <ContentWrap> - <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"> - <el-table-column label="所属平台" align="center" prop="platform"> - <template #default="scope"> - <dict-tag :type="DICT_TYPE.AI_PLATFORM" :value="scope.row.platform" /> - </template> - </el-table-column> - <el-table-column label="模型名字" align="center" prop="name" /> - <el-table-column label="模型标识" align="center" prop="model" /> - <el-table-column label="API 秘钥" align="center" prop="keyId" min-width="140"> - <template #default="scope"> - <span>{{ apiKeyList.find((item) => item.id === scope.row.keyId)?.name }}</span> - </template> - </el-table-column> - <el-table-column label="排序" align="center" prop="sort" /> - <el-table-column label="状态" align="center" prop="status"> - <template #default="scope"> - <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" /> - </template> - </el-table-column> - <el-table-column label="温度参数" align="center" prop="temperature" /> - <el-table-column label="回复数 Token 数" align="center" prop="maxTokens" min-width="140" /> - <el-table-column label="上下文数量" align="center" prop="maxContexts" /> - <el-table-column label="操作" align="center"> - <template #default="scope"> - <el-button - link - type="primary" - @click="openForm('update', scope.row.id)" - v-hasPermi="['ai:chat-model:update']" - > - 编辑 - </el-button> - <el-button - link - type="danger" - @click="handleDelete(scope.row.id)" - v-hasPermi="['ai:chat-model:delete']" - > - 删除 - </el-button> - </template> - </el-table-column> - </el-table> - <!-- 分页 --> - <Pagination - :total="total" - v-model:page="queryParams.pageNo" - v-model:limit="queryParams.pageSize" - @pagination="getList" - /> - </ContentWrap> - - <!-- 表单弹窗:添加/修改 --> - <ChatModelForm ref="formRef" @success="getList" /> -</template> - -<script setup lang="ts"> -import { ChatModelApi, ChatModelVO } from '@/api/ai/model/chatModel' -import ChatModelForm from './ChatModelForm.vue' -import { DICT_TYPE } from '@/utils/dict' -import { ApiKeyApi, ApiKeyVO } from '@/api/ai/model/apiKey' - -/** API 聊天模型 列表 */ -defineOptions({ name: 'AiChatModel' }) - -const message = useMessage() // 消息弹窗 -const { t } = useI18n() // 国际化 - -const loading = ref(true) // 列表的加载中 -const list = ref<ChatModelVO[]>([]) // 列表的数据 -const total = ref(0) // 列表的总页数 -const queryParams = reactive({ - pageNo: 1, - pageSize: 10, - name: undefined, - model: undefined, - platform: undefined -}) -const queryFormRef = ref() // 搜索的表单 -const apiKeyList = ref([] as ApiKeyVO[]) // API 密钥列表 - -/** 查询列表 */ -const getList = async () => { - loading.value = true - try { - const data = await ChatModelApi.getChatModelPage(queryParams) - list.value = data.list - total.value = data.total - } finally { - loading.value = false - } -} - -/** 搜索按钮操作 */ -const handleQuery = () => { - queryParams.pageNo = 1 - getList() -} - -/** 重置按钮操作 */ -const resetQuery = () => { - queryFormRef.value.resetFields() - handleQuery() -} - -/** 添加/修改操作 */ -const formRef = ref() -const openForm = (type: string, id?: number) => { - formRef.value.open(type, id) -} - -/** 删除按钮操作 */ -const handleDelete = async (id: number) => { - try { - // 删除的二次确认 - await message.delConfirm() - // 发起删除 - await ChatModelApi.deleteChatModel(id) - message.success(t('common.delSuccess')) - // 刷新列表 - await getList() - } catch {} -} - -/** 初始化 **/ -onMounted(async () => { - getList() - // 获得下拉数据 - apiKeyList.value = await ApiKeyApi.getApiKeySimpleList() -}) -</script> diff --git a/src/views/ai/model/chatRole/ChatRoleForm.vue b/src/views/ai/model/chatRole/ChatRoleForm.vue deleted file mode 100644 index 3c49e8e..0000000 --- a/src/views/ai/model/chatRole/ChatRoleForm.vue +++ /dev/null @@ -1,183 +0,0 @@ -<template> - <Dialog :title="dialogTitle" v-model="dialogVisible"> - <el-form - ref="formRef" - :model="formData" - :rules="formRules" - label-width="100px" - v-loading="formLoading" - > - <el-form-item label="角色名称" prop="name"> - <el-input v-model="formData.name" placeholder="请输入角色名称" /> - </el-form-item> - <el-form-item label="角色头像" prop="avatar"> - <UploadImg v-model="formData.avatar" height="60px" width="60px" /> - </el-form-item> - <el-form-item label="绑定模型" prop="modelId" v-if="!isUser"> - <el-select v-model="formData.modelId" placeholder="请选择模型" clearable> - <el-option - v-for="chatModel in chatModelList" - :key="chatModel.id" - :label="chatModel.name" - :value="chatModel.id" - /> - </el-select> - </el-form-item> - <el-form-item label="角色类别" prop="category" v-if="!isUser"> - <el-input v-model="formData.category" placeholder="请输入角色类别" /> - </el-form-item> - <el-form-item label="角色描述" prop="description"> - <el-input type="textarea" v-model="formData.description" placeholder="请输入角色描述" /> - </el-form-item> - <el-form-item label="角色设定" prop="systemMessage"> - <el-input type="textarea" v-model="formData.systemMessage" placeholder="请输入角色设定" /> - </el-form-item> - <el-form-item label="是否公开" prop="publicStatus" v-if="!isUser"> - <el-radio-group v-model="formData.publicStatus"> - <el-radio - v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)" - :key="dict.value" - :label="dict.value" - > - {{ dict.label }} - </el-radio> - </el-radio-group> - </el-form-item> - <el-form-item label="角色排序" prop="sort" v-if="!isUser"> - <el-input-number v-model="formData.sort" placeholder="请输入角色排序" class="!w-1/1" /> - </el-form-item> - <el-form-item label="开启状态" prop="status" v-if="!isUser"> - <el-radio-group v-model="formData.status"> - <el-radio - v-for="dict in getIntDictOptions(DICT_TYPE.COMMON_STATUS)" - :key="dict.value" - :label="dict.value" - > - {{ dict.label }} - </el-radio> - </el-radio-group> - </el-form-item> - </el-form> - <template #footer> - <el-button @click="submitForm" type="primary" :disabled="formLoading">确 定</el-button> - <el-button @click="dialogVisible = false">取 消</el-button> - </template> - </Dialog> -</template> -<script setup lang="ts"> -import { getIntDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict' -import { ChatRoleApi, ChatRoleVO } from '@/api/ai/model/chatRole' -import { CommonStatusEnum } from '@/utils/constants' -import { ChatModelApi, ChatModelVO } from '@/api/ai/model/chatModel' -import {FormRules} from "element-plus"; - -/** AI 聊天角色 表单 */ -defineOptions({ name: 'ChatRoleForm' }) - -const { t } = useI18n() // 国际化 -const message = useMessage() // 消息弹窗 - -const dialogVisible = ref(false) // 弹窗的是否展示 -const dialogTitle = ref('') // 弹窗的标题 -const formLoading = ref(false) // 表单的加载中:1)修改时的数据加载;2)提交的按钮禁用 -const formType = ref('') // 表单的类型:create - 新增;update - 修改 -const formData = ref({ - id: undefined, - modelId: undefined, - name: undefined, - avatar: undefined, - category: undefined, - sort: undefined, - description: undefined, - systemMessage: undefined, - publicStatus: true, - status: CommonStatusEnum.ENABLE -}) -const formRef = ref() // 表单 Ref -const chatModelList = ref([] as ChatModelVO[]) // 聊天模型列表 - -/** 是否【我】自己创建,私有角色 */ -const isUser = computed(() => { - return formType.value === 'my-create' || formType.value === 'my-update' -}) - -const formRules = reactive<FormRules>({ - name: [{ required: true, message: '角色名称不能为空', trigger: 'blur' }], - avatar: [{ required: true, message: '角色头像不能为空', trigger: 'blur' }], - category: [{ required: true, message: '角色类别不能为空', trigger: 'blur' }], - sort: [{ required: true, message: '角色排序不能为空', trigger: 'blur' }], - description: [{ required: true, message: '角色描述不能为空', trigger: 'blur' }], - systemMessage: [{ required: true, message: '角色设定不能为空', trigger: 'blur' }], - publicStatus: [{ required: true, message: '是否公开不能为空', trigger: 'blur' }] -}) - -/** 打开弹窗 */ -// TODO @fan:title 是不是收敛到 type 判断生成 title,会更合理 -const open = async (type: string, id?: number, title?: string) => { - dialogVisible.value = true - dialogTitle.value = title || t('action.' + type) - formType.value = type - resetForm() - // 修改时,设置数据 - if (id) { - formLoading.value = true - try { - formData.value = await ChatRoleApi.getChatRole(id) - } finally { - formLoading.value = false - } - } - // 获得下拉数据 - chatModelList.value = await ChatModelApi.getChatModelSimpleList(CommonStatusEnum.ENABLE) -} -defineExpose({ open }) // 提供 open 方法,用于打开弹窗 - -/** 提交表单 */ -const emit = defineEmits(['success']) // 定义 success 事件,用于操作成功后的回调 -const submitForm = async () => { - // 校验表单 - await formRef.value.validate() - // 提交请求 - formLoading.value = true - try { - const data = formData.value as unknown as ChatRoleVO - // tip: my-create、my-update 是 chat 角色仓库调用 - // tip: create、else 是后台管理调用 - if (formType.value === 'my-create') { - await ChatRoleApi.createMy(data) - message.success(t('common.createSuccess')) - } else if (formType.value === 'my-update') { - await ChatRoleApi.updateMy(data) - message.success(t('common.updateSuccess')) - } else if (formType.value === 'create') { - await ChatRoleApi.createChatRole(data) - message.success(t('common.createSuccess')) - } else { - await ChatRoleApi.updateChatRole(data) - message.success(t('common.updateSuccess')) - } - dialogVisible.value = false - // 发送操作成功的事件 - emit('success') - } finally { - formLoading.value = false - } -} - -/** 重置表单 */ -const resetForm = () => { - formData.value = { - id: undefined, - modelId: undefined, - name: undefined, - avatar: undefined, - category: undefined, - sort: undefined, - description: undefined, - systemMessage: undefined, - publicStatus: true, - status: CommonStatusEnum.ENABLE - } - formRef.value?.resetFields() -} -</script> diff --git a/src/views/ai/model/chatRole/index.vue b/src/views/ai/model/chatRole/index.vue deleted file mode 100644 index e870a55..0000000 --- a/src/views/ai/model/chatRole/index.vue +++ /dev/null @@ -1,187 +0,0 @@ -<template> - <ContentWrap> - <!-- 搜索工作栏 --> - <el-form - class="-mb-15px" - :model="queryParams" - ref="queryFormRef" - :inline="true" - label-width="68px" - > - <el-form-item label="角色名称" prop="name"> - <el-input - v-model="queryParams.name" - placeholder="请输入角色名称" - clearable - @keyup.enter="handleQuery" - class="!w-240px" - /> - </el-form-item> - <el-form-item label="角色类别" prop="category"> - <el-input - v-model="queryParams.category" - placeholder="请输入角色类别" - clearable - @keyup.enter="handleQuery" - class="!w-240px" - /> - </el-form-item> - <el-form-item label="是否公开" prop="publicStatus"> - <el-select - v-model="queryParams.publicStatus" - placeholder="请选择是否公开" - clearable - class="!w-240px" - > - <el-option - v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)" - :key="dict.value" - :label="dict.label" - :value="dict.value" - /> - </el-select> - </el-form-item> - <el-form-item> - <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> - <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> - <el-button - type="primary" - plain - @click="openForm('create')" - v-hasPermi="['ai:chat-role:create']" - > - <Icon icon="ep:plus" class="mr-5px" /> 新增 - </el-button> - </el-form-item> - </el-form> - </ContentWrap> - - <!-- 列表 --> - <ContentWrap> - <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"> - <el-table-column label="角色名称" align="center" prop="name" /> - <el-table-column label="绑定模型" align="center" prop="modelName" /> - <el-table-column label="角色头像" align="center" prop="avatar"> - <template #default="scope"> - <el-image :src="scope?.row.avatar" class="w-32px h-32px" /> - </template> - </el-table-column> - <el-table-column label="角色类别" align="center" prop="category" /> - <el-table-column label="角色描述" align="center" prop="description" /> - <el-table-column label="角色设定" align="center" prop="systemMessage" /> - <el-table-column label="是否公开" align="center" prop="publicStatus"> - <template #default="scope"> - <dict-tag :type="DICT_TYPE.INFRA_BOOLEAN_STRING" :value="scope.row.publicStatus" /> - </template> - </el-table-column> - <el-table-column label="状态" align="center" prop="status"> - <template #default="scope"> - <dict-tag :type="DICT_TYPE.COMMON_STATUS" :value="scope.row.status" /> - </template> - </el-table-column> - <el-table-column label="角色排序" align="center" prop="sort" /> - <el-table-column label="操作" align="center"> - <template #default="scope"> - <el-button - link - type="primary" - @click="openForm('update', scope.row.id)" - v-hasPermi="['ai:chat-role:update']" - > - 编辑 - </el-button> - <el-button - link - type="danger" - @click="handleDelete(scope.row.id)" - v-hasPermi="['ai:chat-role:delete']" - > - 删除 - </el-button> - </template> - </el-table-column> - </el-table> - <!-- 分页 --> - <Pagination - :total="total" - v-model:page="queryParams.pageNo" - v-model:limit="queryParams.pageSize" - @pagination="getList" - /> - </ContentWrap> - - <!-- 表单弹窗:添加/修改 --> - <ChatRoleForm ref="formRef" @success="getList" /> -</template> - -<script setup lang="ts"> -import { getBoolDictOptions, DICT_TYPE } from '@/utils/dict' -import { ChatRoleApi, ChatRoleVO } from '@/api/ai/model/chatRole' -import ChatRoleForm from './ChatRoleForm.vue' - -/** AI 聊天角色 列表 */ -defineOptions({ name: 'AiChatRole' }) - -const message = useMessage() // 消息弹窗 -const { t } = useI18n() // 国际化 - -const loading = ref(true) // 列表的加载中 -const list = ref<ChatRoleVO[]>([]) // 列表的数据 -const total = ref(0) // 列表的总页数 -const queryParams = reactive({ - pageNo: 1, - pageSize: 10, - name: undefined, - category: undefined, - publicStatus: true -}) -const queryFormRef = ref() // 搜索的表单 - -/** 查询列表 */ -const getList = async () => { - loading.value = true - try { - const data = await ChatRoleApi.getChatRolePage(queryParams) - list.value = data.list - total.value = data.total - } finally { - loading.value = false - } -} - -/** 搜索按钮操作 */ -const handleQuery = () => { - queryParams.pageNo = 1 - getList() -} - -/** 重置按钮操作 */ -const resetQuery = () => { - queryFormRef.value.resetFields() - handleQuery() -} - -/** 添加/修改操作 */ -const formRef = ref() -const openForm = (type: string, id?: number) => { - formRef.value.open(type, id) -} - -/** 删除按钮操作 */ -const handleDelete = async (id: number) => { - try { - // 删除的二次确认 - await message.delConfirm() - // 发起删除 - await ChatRoleApi.deleteChatRole(id) - message.success(t('common.delSuccess')) - // 刷新列表 - await getList() - } catch {} -} - -/** 初始化 **/ -onMounted(() => { - getList() -}) -</script> diff --git a/src/views/ai/music/index/index.vue b/src/views/ai/music/index/index.vue deleted file mode 100644 index 413792a..0000000 --- a/src/views/ai/music/index/index.vue +++ /dev/null @@ -1,26 +0,0 @@ -<template> -<div class="flex h-full items-stretch"> - <!-- 模式 --> - <Mode class="flex-none" @generate-music="generateMusic"/> - <!-- 音频列表 --> - <List ref="listRef" class="flex-auto"/> - </div> -</template> - -<script lang="ts" setup> -import Mode from './mode/index.vue' -import List from './list/index.vue' - -defineOptions({ name: 'Index' }) - -const listRef = ref<Nullable<{generateMusic: (...args) => void}>>(null) - -/* - *@Description: 拿到左侧配置信息调用右侧音乐生成的方法 - *@MethodAuthor: xiaohong - *@Date: 2024-07-19 11:13:38 -*/ -function generateMusic (args: {formData: Recordable}) { - unref(listRef)?.generateMusic(args.formData) -} -</script> diff --git a/src/views/ai/music/index/list/audioBar/index.vue b/src/views/ai/music/index/list/audioBar/index.vue deleted file mode 100644 index db7f767..0000000 --- a/src/views/ai/music/index/list/audioBar/index.vue +++ /dev/null @@ -1,70 +0,0 @@ -<template> - <div class="flex items-center justify-between px-2 h-72px bg-[var(--el-bg-color-overlay)] b-solid b-1 b-[var(--el-border-color)] b-l-none"> - <!-- 歌曲信息 --> - <div class="flex gap-[10px]"> - <el-image src="https://zos.alipayobjects.com/rmsportal/jkjgkEfvpUPVyRjUImniVslZfWPnJuuZ.png" class="w-[45px]"/> - <div> - <div>{{currentSong.name}}</div> - <div class="text-[12px] text-gray-400">{{currentSong.singer}}</div> - </div> - </div> - - <!-- 音频controls --> - <div class="flex gap-[12px] items-center"> - <Icon icon="majesticons:back-circle" :size="20" class="text-gray-300 cursor-pointer"/> - <Icon :icon="audioProps.paused ? 'mdi:arrow-right-drop-circle' : 'solar:pause-circle-bold'" :size="30" class=" cursor-pointer" @click="toggleStatus('paused')"/> - <Icon icon="majesticons:next-circle" :size="20" class="text-gray-300 cursor-pointer"/> - <div class="flex gap-[16px] items-center"> - <span>{{audioProps.currentTime}}</span> - <el-slider v-model="audioProps.duration" color="#409eff" class="w-[160px!important] "/> - <span>{{ audioProps.duration }}</span> - </div> - <!-- 音频 --> - <audio v-bind="audioProps" ref="audioRef" controls v-show="!audioProps" @timeupdate="audioTimeUpdate"> - <source :src="audioUrl"/> - </audio> - </div> - - <!-- 音量控制器 --> - <div class="flex gap-[16px] items-center"> - <Icon :icon="audioProps.muted ? 'tabler:volume-off' : 'tabler:volume'" :size="20" class="cursor-pointer" @click="toggleStatus('muted')"/> - <el-slider v-model="audioProps.volume" color="#409eff" class="w-[160px!important] "/> - </div> - </div> -</template> - -<script lang="ts" setup> -import { formatPast } from '@/utils/formatTime' -import audioUrl from '@/assets/audio/response.mp3' - -defineOptions({ name: 'Index' }) - -const currentSong = inject('currentSong', {}) - -const audioRef = ref<Nullable<HTMLElement>>(null) - // 音频相关属性https://www.runoob.com/tags/ref-av-dom.html -const audioProps = reactive({ - autoplay: true, - paused: false, - currentTime: '00:00', - duration: '00:00', - muted: false, - volume: 50, -}) - -function toggleStatus (type: string) { - audioProps[type] = !audioProps[type] - if (type === 'paused' && audioRef.value) { - if (audioProps[type]) { - audioRef.value.pause() - } else { - audioRef.value.play() - } - } -} - -// 更新播放位置 -function audioTimeUpdate (args) { - audioProps.currentTime = formatPast(new Date(args.timeStamp), 'mm:ss') -} -</script> diff --git a/src/views/ai/music/index/list/index.vue b/src/views/ai/music/index/list/index.vue deleted file mode 100644 index 6c33f56..0000000 --- a/src/views/ai/music/index/list/index.vue +++ /dev/null @@ -1,108 +0,0 @@ -<template> - <div class="flex flex-col"> - <div class="flex-auto flex overflow-hidden"> - <el-tabs v-model="currentType" class="flex-auto px-[var(--app-content-padding)]"> - <!-- 我的创作 --> - <el-tab-pane v-loading="loading" label="我的创作" name="mine"> - <el-row v-if="mySongList.length" :gutter="12"> - <el-col v-for="song in mySongList" :key="song.id" :span="24"> - <songCard :songInfo="song" @play="setCurrentSong(song)"/> - </el-col> - </el-row> - <el-empty v-else description="暂无音乐"/> - </el-tab-pane> - - <!-- 试听广场 --> - <el-tab-pane v-loading="loading" label="试听广场" name="square"> - <el-row v-if="squareSongList.length" v-loading="loading" :gutter="12"> - <el-col v-for="song in squareSongList" :key="song.id" :span="24"> - <songCard :songInfo="song" @play="setCurrentSong(song)"/> - </el-col> - </el-row> - <el-empty v-else description="暂无音乐"/> - </el-tab-pane> - </el-tabs> - <!-- songInfo --> - <songInfo class="flex-none"/> - </div> - <audioBar class="flex-none"/> - </div> -</template> - -<script lang="ts" setup> -import songCard from './songCard/index.vue' -import songInfo from './songInfo/index.vue' -import audioBar from './audioBar/index.vue' - -defineOptions({ name: 'Index' }) - - -const currentType = ref('mine') -// loading 状态 -const loading = ref(false) -// 当前音乐 -const currentSong = ref({}) - -const mySongList = ref<Recordable[]>([]) -const squareSongList = ref<Recordable[]>([]) - -provide('currentSong', currentSong) - -/* - *@Description: 调接口生成音乐列表 - *@MethodAuthor: xiaohong - *@Date: 2024-06-27 17:06:44 -*/ -function generateMusic (formData: Recordable) { - console.log(formData); - loading.value = true - setTimeout(() => { - mySongList.value = Array.from({ length: 20 }, (_, index) => { - return { - id: index, - audioUrl: '', - videoUrl: '', - title: '我走后' + index, - imageUrl: 'https://www.carsmp3.com/data/attachment/forum/201909/19/091020q5kgre20fidreqyt.jpg', - desc: 'Metal, symphony, film soundtrack, grand, majesticMetal, dtrack, grand, majestic', - date: '2024年04月30日 14:02:57', - lyric: `<div class="_words_17xen_66"><div>大江东去,浪淘尽,千古风流人物。 - </div><div>故垒西边,人道是,三国周郎赤壁。 - </div><div>乱石穿空,惊涛拍岸,卷起千堆雪。 - </div><div>江山如画,一时多少豪杰。 - </div><div> - </div><div>遥想公瑾当年,小乔初嫁了,雄姿英发。 - </div><div>羽扇纶巾,谈笑间,樯橹灰飞烟灭。 - </div><div>故国神游,多情应笑我,早生华发。 - </div><div>人生如梦,一尊还酹江月。</div></div>` - } - }) - loading.value = false - }, 3000) -} - -/* - *@Description: 设置当前播放的音乐 - *@MethodAuthor: xiaohong - *@Date: 2024-07-19 11:22:33 -*/ -function setCurrentSong (music: Recordable) { - currentSong.value = music -} - -defineExpose({ - generateMusic -}) -</script> - - -<style lang="scss" scoped> -:deep(.el-tabs) { - display: flex; - flex-direction: column; - .el-tabs__content { - padding: 0 7px; - overflow: auto; - } -} -</style> diff --git a/src/views/ai/music/index/list/songCard/index.vue b/src/views/ai/music/index/list/songCard/index.vue deleted file mode 100644 index 0534251..0000000 --- a/src/views/ai/music/index/list/songCard/index.vue +++ /dev/null @@ -1,36 +0,0 @@ -<template> - <div class="flex bg-[var(--el-bg-color-overlay)] p-12px mb-12px rounded-1"> - <div class="relative" @click="playSong"> - <el-image :src="songInfo.imageUrl" class="flex-none w-80px"/> - <div class="bg-black bg-op-40 absolute top-0 left-0 w-full h-full flex items-center justify-center cursor-pointer"> - <Icon :icon="currentSong.id === songInfo.id ? 'solar:pause-circle-bold':'mdi:arrow-right-drop-circle'" :size="30" /> - </div> - </div> - <div class="ml-8px"> - <div>{{ songInfo.title }}</div> - <div class="mt-8px text-12px text-[var(--el-text-color-secondary)] line-clamp-2"> - {{ songInfo.desc }} - </div> - </div> - </div> -</template> - -<script lang="ts" setup> - -defineOptions({ name: 'Index' }) - -defineProps({ - songInfo: { - type: Object, - default: () => ({}) - } -}) - -const emits = defineEmits(['play']) - -const currentSong = inject('currentSong', {}) - -function playSong () { - emits('play') -} -</script> diff --git a/src/views/ai/music/index/list/songInfo/index.vue b/src/views/ai/music/index/list/songInfo/index.vue deleted file mode 100644 index 8d67c4d..0000000 --- a/src/views/ai/music/index/list/songInfo/index.vue +++ /dev/null @@ -1,22 +0,0 @@ -<template> - <ContentWrap class="w-300px mb-[0!important] line-height-24px"> - <el-image :src="currentSong.imageUrl"/> - <div class="">{{ currentSong.title }}</div> - <div class="text-[var(--el-text-color-secondary)] text-12px line-clamp-1"> - {{ currentSong.desc }} - </div> - <div class="text-[var(--el-text-color-secondary)] text-12px"> - {{ currentSong.date }} - </div> - <el-button size="small" round class="my-6px">信息复用</el-button> - <div class="text-[var(--el-text-color-secondary)] text-12px" v-html="currentSong.lyric"></div> - </ContentWrap> -</template> - -<script lang="ts" setup> - -defineOptions({ name: 'Index' }) - -const currentSong = inject('currentSong', {}) - -</script> diff --git a/src/views/ai/music/index/mode/desc.vue b/src/views/ai/music/index/mode/desc.vue deleted file mode 100644 index 4488461..0000000 --- a/src/views/ai/music/index/mode/desc.vue +++ /dev/null @@ -1,55 +0,0 @@ -<template> - <div> - <Title title="音乐/歌词说明" desc="描述您想要的音乐风格和主题,使用流派和氛围而不是特定的艺术家和歌曲"> - <el-input - v-model="formData.desc" - :autosize="{ minRows: 6, maxRows: 6}" - resize="none" - type="textarea" - maxlength="1200" - show-word-limit - placeholder="一首关于糟糕分手的欢快歌曲" - /> - </Title> - - <Title title="纯音乐" desc="创建一首没有歌词的歌曲"> - <template #extra> - <el-switch v-model="formData.pure" size="small"/> - </template> - </Title> - - <Title title="版本" desc="描述您想要的音乐风格和主题,使用流派和氛围而不是特定的艺术家和歌曲"> - <el-select v-model="formData.version" placeholder="请选择"> - <el-option - v-for="item in [{ - value: '3', - label: 'V3' - }, { - value: '2', - label: 'V2' - }]" - :key="item.value" - :label="item.label" - :value="item.value" - /> - </el-select> - </Title> - </div> -</template> - -<script lang="ts" setup> -import Title from '../title/index.vue' - -defineOptions({ name: 'Desc' }) - -const formData = reactive({ - desc: '', - pure: false, - version: '3' -}) - -defineExpose({ - formData -}) - -</script> diff --git a/src/views/ai/music/index/mode/index.vue b/src/views/ai/music/index/mode/index.vue deleted file mode 100644 index 85ef893..0000000 --- a/src/views/ai/music/index/mode/index.vue +++ /dev/null @@ -1,41 +0,0 @@ -<template> - <ContentWrap class="w-300px h-full mb-[0!important]"> - <el-radio-group v-model="generateMode" class="mb-15px"> - <el-radio-button label="desc"> - 描述模式 - </el-radio-button> - <el-radio-button label="lyric"> - 歌词模式 - </el-radio-button> - </el-radio-group> - - <!-- 描述模式/歌词模式 切换 --> - <component :is="generateMode === 'desc' ? desc : lyric" ref="modeRef"/> - - <el-button type="primary" round class="w-full" @click="generateMusic"> - 创作音乐 - </el-button> - </ContentWrap> -</template> - -<script lang="ts" setup> -import desc from './desc.vue' -import lyric from './lyric.vue' - -defineOptions({ name: 'Index' }) - -const emits = defineEmits(['generate-music']) - -const generateMode = ref('lyric') - -const modeRef = ref<Nullable<{ formData: Recordable }>>(null) - -/* - *@Description: 根据信息生成音乐 - *@MethodAuthor: xiaohong - *@Date: 2024-06-27 16:40:16 -*/ -function generateMusic () { - emits('generate-music', {formData: unref(modeRef)?.formData}) -} -</script> diff --git a/src/views/ai/music/index/mode/lyric.vue b/src/views/ai/music/index/mode/lyric.vue deleted file mode 100644 index f774003..0000000 --- a/src/views/ai/music/index/mode/lyric.vue +++ /dev/null @@ -1,83 +0,0 @@ -<template> - <div class=""> - <Title title="歌词" desc="自己编写歌词或使用Ai生成歌词,两节/8行效果最佳"> - <el-input - v-model="formData.lyric" - :autosize="{ minRows: 6, maxRows: 6}" - resize="none" - type="textarea" - maxlength="1200" - show-word-limit - placeholder="请输入您自己的歌词" - /> - </Title> - - <Title title="音乐风格"> - <el-space class="flex-wrap"> - <el-tag v-for="tag in tags" :key="tag" round class="mb-8px">{{tag}}</el-tag> - </el-space> - - <el-button - :type="showCustom ? 'primary': 'default'" - round - size="small" - class="mb-6px" - @click="showCustom = !showCustom" - >自定义风格 - </el-button> - </Title> - - <Title v-show="showCustom" desc="描述您想要的音乐风格,Suno无法识别艺术家的名字,但可以理解流派和氛围" class="-mt-12px"> - <el-input - v-model="formData.style" - :autosize="{ minRows: 4, maxRows: 4}" - resize="none" - type="textarea" - maxlength="256" - show-word-limit - placeholder="输入音乐风格(英文)" - /> - </Title> - - <Title title="音乐/歌曲名称"> - <el-input v-model="formData.name" placeholder="请输入音乐/歌曲名称"/> - </Title> - - <Title title="版本"> - <el-select v-model="formData.version" placeholder="请选择"> - <el-option - v-for="item in [{ - value: '3', - label: 'V3' - }, { - value: '2', - label: 'V2' - }]" - :key="item.value" - :label="item.label" - :value="item.value" - /> - </el-select> - </Title> - </div> -</template> - -<script lang="ts" setup> -import Title from '../title/index.vue' -defineOptions({ name: 'Lyric' }) - -const tags = ['rock', 'punk', 'jazz', 'soul', 'country', 'kidsmusic', 'pop'] - -const showCustom = ref(false) - -const formData = reactive({ - lyric: '', - style: '', - name: '', - version: '' -}) - -defineExpose({ - formData -}) -</script> diff --git a/src/views/ai/music/index/title/index.vue b/src/views/ai/music/index/title/index.vue deleted file mode 100644 index a065802..0000000 --- a/src/views/ai/music/index/title/index.vue +++ /dev/null @@ -1,25 +0,0 @@ -<template> - <div class="mb-12px"> - <div class="flex text-[var(--el-text-color-primary)] justify-between items-center"> - <span>{{title}}</span> - <slot name="extra"></slot> - </div> - <div class="text-[var(--el-text-color-secondary)] text-12px my-8px"> - {{desc}} - </div> - <slot></slot> - </div> -</template> - -<script lang="ts" setup> -defineOptions({ name: 'Index' }) - -defineProps({ - title: { - type: String - }, - desc: { - type: String - } -}) -</script> diff --git a/src/views/ai/music/manager/index.vue b/src/views/ai/music/manager/index.vue deleted file mode 100644 index 462a88d..0000000 --- a/src/views/ai/music/manager/index.vue +++ /dev/null @@ -1,292 +0,0 @@ -<template> - <ContentWrap> - <!-- 搜索工作栏 --> - <el-form - class="-mb-15px" - :model="queryParams" - ref="queryFormRef" - :inline="true" - label-width="68px" - > - <el-form-item label="用户编号" prop="userId"> - <el-select - v-model="queryParams.userId" - clearable - placeholder="请输入用户编号" - class="!w-240px" - > - <el-option - v-for="item in userList" - :key="item.id" - :label="item.nickname" - :value="item.id" - /> - </el-select> - </el-form-item> - <el-form-item label="音乐名称" prop="title"> - <el-input - v-model="queryParams.title" - placeholder="请输入音乐名称" - clearable - @keyup.enter="handleQuery" - class="!w-240px" - /> - </el-form-item> - <el-form-item label="音乐状态" prop="status"> - <el-select - v-model="queryParams.status" - placeholder="请选择音乐状态" - clearable - class="!w-240px" - > - <el-option - v-for="dict in getIntDictOptions(DICT_TYPE.AI_MUSIC_STATUS)" - :key="dict.value" - :label="dict.label" - :value="dict.value" - /> - </el-select> - </el-form-item> - <el-form-item label="生成模式" prop="generateMode"> - <el-select - v-model="queryParams.generateMode" - placeholder="请选择生成模式" - clearable - class="!w-240px" - > - <el-option - v-for="dict in getIntDictOptions(DICT_TYPE.AI_GENERATE_MODE)" - :key="dict.value" - :label="dict.label" - :value="dict.value" - /> - </el-select> - </el-form-item> - <el-form-item label="创建时间" prop="createTime"> - <el-date-picker - v-model="queryParams.createTime" - value-format="YYYY-MM-DD HH:mm:ss" - type="daterange" - start-placeholder="开始日期" - end-placeholder="结束日期" - :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" - class="!w-220px" - /> - </el-form-item> - <el-form-item label="是否发布" prop="publicStatus"> - <el-select - v-model="queryParams.publicStatus" - placeholder="请选择是否发布" - clearable - class="!w-240px" - > - <el-option - v-for="dict in getBoolDictOptions(DICT_TYPE.INFRA_BOOLEAN_STRING)" - :key="dict.value" - :label="dict.label" - :value="dict.value" - /> - </el-select> - </el-form-item> - <el-form-item> - <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> - <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> - </el-form-item> - </el-form> - </ContentWrap> - - <!-- 列表 --> - <ContentWrap> - <el-table v-loading="loading" :data="list" :stripe="true" :show-overflow-tooltip="true"> - <el-table-column label="编号" align="center" prop="id" width="180" fixed="left" /> - <el-table-column label="音乐名称" align="center" prop="title" width="180px" fixed="left" /> - <el-table-column label="用户" align="center" prop="userId" width="180"> - <template #default="scope"> - <span>{{ userList.find((item) => item.id === scope.row.userId)?.nickname }}</span> - </template> - </el-table-column> - <el-table-column label="音乐状态" align="center" prop="status" width="100"> - <template #default="scope"> - <dict-tag :type="DICT_TYPE.AI_MUSIC_STATUS" :value="scope.row.status" /> - </template> - </el-table-column> - <el-table-column label="模型" align="center" prop="model" width="180" /> - <el-table-column label="内容" align="center" width="180"> - <template #default="{ row }"> - <el-link - v-if="row.audioUrl?.length > 0" - type="primary" - :href="row.audioUrl" - target="_blank" - > - 音乐 - </el-link> - <el-link - v-if="row.videoUrl?.length > 0" - type="primary" - :href="row.videoUrl" - target="_blank" - class="!pl-5px" - > - 视频 - </el-link> - <el-link - v-if="row.imageUrl?.length > 0" - type="primary" - :href="row.imageUrl" - target="_blank" - class="!pl-5px" - > - 封面 - </el-link> - </template> - </el-table-column> - <el-table-column label="时长(秒)" align="center" prop="duration" width="100" /> - <el-table-column label="提示词" align="center" prop="prompt" width="180" /> - <el-table-column label="歌词" align="center" prop="lyric" width="180" /> - <el-table-column label="描述" align="center" prop="gptDescriptionPrompt" width="180" /> - <el-table-column label="生成模式" align="center" prop="generateMode" width="100"> - <template #default="scope"> - <dict-tag :type="DICT_TYPE.AI_GENERATE_MODE" :value="scope.row.generateMode" /> - </template> - </el-table-column> - <el-table-column label="风格标签" align="center" prop="tags" width="180"> - <template #default="scope"> - <el-tag v-for="tag in scope.row.tags" :key="tag" round class="ml-2px"> - {{ tag }} - </el-tag> - </template> - </el-table-column> - <el-table-column label="是否发布" align="center" prop="publicStatus"> - <template #default="scope"> - <el-switch - v-model="scope.row.publicStatus" - :active-value="true" - :inactive-value="false" - @change="handleUpdatePublicStatusChange(scope.row)" - :disabled="scope.row.status !== AiMusicStatusEnum.SUCCESS" - /> - </template> - </el-table-column> - <el-table-column label="任务编号" align="center" prop="taskId" width="180" /> - <el-table-column label="错误信息" align="center" prop="errorMessage" /> - <el-table-column - label="创建时间" - align="center" - prop="createTime" - :formatter="dateFormatter" - width="180px" - /> - <el-table-column label="操作" align="center" width="100" fixed="right"> - <template #default="scope"> - <el-button - link - type="danger" - @click="handleDelete(scope.row.id)" - v-hasPermi="['ai:music:delete']" - > - 删除 - </el-button> - </template> - </el-table-column> - </el-table> - <!-- 分页 --> - <Pagination - :total="total" - v-model:page="queryParams.pageNo" - v-model:limit="queryParams.pageSize" - @pagination="getList" - /> - </ContentWrap> -</template> - -<script setup lang="ts"> -import { getIntDictOptions, getBoolDictOptions, DICT_TYPE } from '@/utils/dict' -import { dateFormatter } from '@/utils/formatTime' -import { MusicApi, MusicVO } from '@/api/ai/music' -import * as UserApi from '@/api/system/user' -import { AiMusicStatusEnum } from '@/views/ai/utils/constants' - -/** AI 音乐 列表 */ -defineOptions({ name: 'AiMusicManager' }) - -const message = useMessage() // 消息弹窗 -const { t } = useI18n() // 国际化 - -const loading = ref(true) // 列表的加载中 -const list = ref<MusicVO[]>([]) // 列表的数据 -const total = ref(0) // 列表的总页数 -const queryParams = reactive({ - pageNo: 1, - pageSize: 10, - userId: undefined, - title: undefined, - status: undefined, - generateMode: undefined, - createTime: [], - publicStatus: undefined -}) -const queryFormRef = ref() // 搜索的表单 -const userList = ref<UserApi.UserVO[]>([]) // 用户列表 - -/** 查询列表 */ -const getList = async () => { - loading.value = true - try { - const data = await MusicApi.getMusicPage(queryParams) - list.value = data.list - total.value = data.total - } finally { - loading.value = false - } -} - -/** 搜索按钮操作 */ -const handleQuery = () => { - queryParams.pageNo = 1 - getList() -} - -/** 重置按钮操作 */ -const resetQuery = () => { - queryFormRef.value.resetFields() - handleQuery() -} - -/** 删除按钮操作 */ -const handleDelete = async (id: number) => { - try { - // 删除的二次确认 - await message.delConfirm() - // 发起删除 - await MusicApi.deleteMusic(id) - message.success(t('common.delSuccess')) - // 刷新列表 - await getList() - } catch {} -} - -/** 修改是否发布 */ -const handleUpdatePublicStatusChange = async (row: MusicVO) => { - try { - // 修改状态的二次确认 - const text = row.publicStatus ? '公开' : '私有' - await message.confirm('确认要"' + text + '"该音乐吗?') - // 发起修改状态 - await MusicApi.updateMusic({ - id: row.id, - publicStatus: row.publicStatus - }) - await getList() - } catch { - row.publicStatus = !row.publicStatus - } -} - -/** 初始化 **/ -onMounted(async () => { - getList() - // 获得用户列表 - userList.value = await UserApi.getSimpleUserList() -}) -</script> diff --git a/src/views/ai/utils/constants.ts b/src/views/ai/utils/constants.ts deleted file mode 100644 index 8888662..0000000 --- a/src/views/ai/utils/constants.ts +++ /dev/null @@ -1,481 +0,0 @@ -/** - * Created by iailab - * - * AI 枚举类 - * - * 问题:为什么不放在 src/utils/constants.ts 呢? - * 回答:主要 AI 是可选模块,考虑到独立、解耦,所以放在了 /views/ai/utils/constants.ts - */ - -/** - * AI 平台的枚举 - */ -export const AiPlatformEnum = { - TONG_YI: 'TongYi', // 阿里 - YI_YAN: 'YiYan', // 百度 - DEEP_SEEK: 'DeepSeek', // DeepSeek - ZHI_PU: 'ZhiPu', // 智谱 AI - XING_HUO: 'XingHuo', // 讯飞 - OPENAI: 'OpenAI', - Ollama: 'Ollama', - STABLE_DIFFUSION: 'StableDiffusion', // Stability AI - MIDJOURNEY: 'Midjourney', // Midjourney - SUNO: 'Suno' // Suno AI -} - -export const OtherPlatformEnum: ImageModelVO[] = [ - { - key: AiPlatformEnum.TONG_YI, - name: '通义万相' - }, - { - key: AiPlatformEnum.YI_YAN, - name: '百度千帆' - }, - { - key: AiPlatformEnum.ZHI_PU, - name: '智谱 AI' - } -] - -/** - * AI 图像生成状态的枚举 - */ -export const AiImageStatusEnum = { - IN_PROGRESS: 10, // 进行中 - SUCCESS: 20, // 已完成 - FAIL: 30 // 已失败 -} - -/** - * AI 音乐生成状态的枚举 - */ -export const AiMusicStatusEnum = { - IN_PROGRESS: 10, // 进行中 - SUCCESS: 20, // 已完成 - FAIL: 30 // 已失败 -} - -/** - * AI 写作类型的枚举 - */ -export enum AiWriteTypeEnum { - WRITING = 1, // 撰写 - REPLY // 回复 -} - -// 表格展示对照map -export const AiWriteTypeTableRender = { - [AiWriteTypeEnum.WRITING]: '撰写', - [AiWriteTypeEnum.REPLY]: '回复' -} - -// ========== 【图片 UI】相关的枚举 ========== - -export const ImageHotWords = [ - '中国旗袍', - '古装美女', - '卡通头像', - '机甲战士', - '童话小屋', - '中国长城' -] // 图片热词 - -export const ImageHotEnglishWords = [ - 'Chinese Cheongsam', - 'Ancient Beauty', - 'Cartoon Avatar', - 'Mech Warrior', - 'Fairy Tale Cottage', - 'The Great Wall of China' -] // 图片热词(英文) - -export interface ImageModelVO { - key: string - name: string - image?: string -} - -export const StableDiffusionSamplers: ImageModelVO[] = [ - { - key: 'DDIM', - name: 'DDIM' - }, - { - key: 'DDPM', - name: 'DDPM' - }, - { - key: 'K_DPMPP_2M', - name: 'K_DPMPP_2M' - }, - { - key: 'K_DPMPP_2S_ANCESTRAL', - name: 'K_DPMPP_2S_ANCESTRAL' - }, - { - key: 'K_DPM_2', - name: 'K_DPM_2' - }, - { - key: 'K_DPM_2_ANCESTRAL', - name: 'K_DPM_2_ANCESTRAL' - }, - { - key: 'K_EULER', - name: 'K_EULER' - }, - { - key: 'K_EULER_ANCESTRAL', - name: 'K_EULER_ANCESTRAL' - }, - { - key: 'K_HEUN', - name: 'K_HEUN' - }, - { - key: 'K_LMS', - name: 'K_LMS' - } -] - -export const StableDiffusionStylePresets: ImageModelVO[] = [ - { - key: '3d-model', - name: '3d-model' - }, - { - key: 'analog-film', - name: 'analog-film' - }, - { - key: 'anime', - name: 'anime' - }, - { - key: 'cinematic', - name: 'cinematic' - }, - { - key: 'comic-book', - name: 'comic-book' - }, - { - key: 'digital-art', - name: 'digital-art' - }, - { - key: 'enhance', - name: 'enhance' - }, - { - key: 'fantasy-art', - name: 'fantasy-art' - }, - { - key: 'isometric', - name: 'isometric' - }, - { - key: 'line-art', - name: 'line-art' - }, - { - key: 'low-poly', - name: 'low-poly' - }, - { - key: 'modeling-compound', - name: 'modeling-compound' - }, - // neon-punk origami photographic pixel-art tile-texture - { - key: 'neon-punk', - name: 'neon-punk' - }, - { - key: 'origami', - name: 'origami' - }, - { - key: 'photographic', - name: 'photographic' - }, - { - key: 'pixel-art', - name: 'pixel-art' - }, - { - key: 'tile-texture', - name: 'tile-texture' - } -] - -export const TongYiWanXiangModels: ImageModelVO[] = [ - { - key: 'wanx-v1', - name: 'wanx-v1' - }, - { - key: 'wanx-sketch-to-image-v1', - name: 'wanx-sketch-to-image-v1' - } -] - -export const QianFanModels: ImageModelVO[] = [ - { - key: 'sd_xl', - name: 'sd_xl' - } -] - -export const ChatGlmModels: ImageModelVO[] = [ - { - key: 'cogview-3', - name: 'cogview-3' - } -] - -export const StableDiffusionClipGuidancePresets: ImageModelVO[] = [ - { - key: 'NONE', - name: 'NONE' - }, - { - key: 'FAST_BLUE', - name: 'FAST_BLUE' - }, - { - key: 'FAST_GREEN', - name: 'FAST_GREEN' - }, - { - key: 'SIMPLE', - name: 'SIMPLE' - }, - { - key: 'SLOW', - name: 'SLOW' - }, - { - key: 'SLOWER', - name: 'SLOWER' - }, - { - key: 'SLOWEST', - name: 'SLOWEST' - } -] - -export const Dall3Models: ImageModelVO[] = [ - { - key: 'dall-e-3', - name: 'DALL·E 3', - image: `/src/assets/ai/dall2.jpg` - }, - { - key: 'dall-e-2', - name: 'DALL·E 2', - image: `/src/assets/ai/dall3.jpg` - } -] - -export const Dall3StyleList: ImageModelVO[] = [ - { - key: 'vivid', - name: '清晰', - image: `/src/assets/ai/qingxi.jpg` - }, - { - key: 'natural', - name: '自然', - image: `/src/assets/ai/ziran.jpg` - } -] - -export interface ImageSizeVO { - key: string - name?: string - style: string - width: string - height: string -} - -export const Dall3SizeList: ImageSizeVO[] = [ - { - key: '1024x1024', - name: '1:1', - width: '1024', - height: '1024', - style: 'width: 30px; height: 30px;background-color: #dcdcdc;' - }, - { - key: '1024x1792', - name: '3:5', - width: '1024', - height: '1792', - style: 'width: 30px; height: 50px;background-color: #dcdcdc;' - }, - { - key: '1792x1024', - name: '5:3', - width: '1792', - height: '1024', - style: 'width: 50px; height: 30px;background-color: #dcdcdc;' - } -] - -export const MidjourneyModels: ImageModelVO[] = [ - { - key: 'midjourney', - name: 'MJ', - image: 'https://bigpt8.com/pc/_nuxt/mj.34a61377.png' - }, - { - key: 'niji', - name: 'NIJI', - image: 'https://bigpt8.com/pc/_nuxt/nj.ca79b143.png' - } -] - -export const MidjourneySizeList: ImageSizeVO[] = [ - { - key: '1:1', - width: '1', - height: '1', - style: 'width: 30px; height: 30px;background-color: #dcdcdc;' - }, - { - key: '3:4', - width: '3', - height: '4', - style: 'width: 30px; height: 40px;background-color: #dcdcdc;' - }, - { - key: '4:3', - width: '4', - height: '3', - style: 'width: 40px; height: 30px;background-color: #dcdcdc;' - }, - { - key: '9:16', - width: '9', - height: '16', - style: 'width: 30px; height: 50px;background-color: #dcdcdc;' - }, - { - key: '16:9', - width: '16', - height: '9', - style: 'width: 50px; height: 30px;background-color: #dcdcdc;' - } -] - -export const MidjourneyVersions = [ - { - value: '6.0', - label: 'v6.0' - }, - { - value: '5.2', - label: 'v5.2' - }, - { - value: '5.1', - label: 'v5.1' - }, - { - value: '5.0', - label: 'v5.0' - }, - { - value: '4.0', - label: 'v4.0' - } -] - -export const NijiVersionList = [ - { - value: '5', - label: 'v5' - } -] - -// ========== 【写作 UI】相关的枚举 ========== - -/** 写作点击示例时的数据 **/ -export const WriteExample = { - write: { - prompt: 'vue', - data: 'Vue.js 是一种用于构建用户界面的渐进式 JavaScript 框架。它的核心库只关注视图层,易于上手,同时也便于与其他库或已有项目整合。\n\nVue.js 的特点包括:\n- 响应式的数据绑定:Vue.js 会自动将数据与 DOM 同步,使得状态管理变得更加简单。\n- 组件化:Vue.js 允许开发者通过小型、独立和通常可复用的组件构建大型应用。\n- 虚拟 DOM:Vue.js 使用虚拟 DOM 实现快速渲染,提高了性能。\n\n在 Vue.js 中,一个典型的应用结构可能包括:\n1. 根实例:每个 Vue 应用都需要一个根实例作为入口点。\n2. 组件系统:可以创建自定义的可复用组件。\n3. 指令:特殊的带有前缀 v- 的属性,为 DOM 元素提供特殊的行为。\n4. 插值:用于文本内容,将数据动态地插入到 HTML。\n5. 计算属性和侦听器:用于处理数据的复杂逻辑和响应数据变化。\n6. 条件渲染:根据条件决定元素的渲染。\n7. 列表渲染:用于显示列表数据。\n8. 事件处理:响应用户交互。\n9. 表单输入绑定:处理表单输入和验证。\n10. 组件生命周期钩子:在组件的不同阶段执行特定的函数。\n\nVue.js 还提供了官方的路由器 Vue Router 和状态管理库 Vuex,以支持构建复杂的单页应用(SPA)。\n\n在开发过程中,开发者通常会使用 Vue CLI,这是一个强大的命令行工具,用于快速生成 Vue 项目脚手架,集成了诸如 Babel、Webpack 等现代前端工具,以及热重载、代码检测等开发体验优化功能。\n\nVue.js 的生态系统还包括大量的第三方库和插件,如 Vuetify(UI 组件库)、Vue Test Utils(测试工具)等,这些都极大地丰富了 Vue.js 的开发生态。\n\n总的来说,Vue.js 是一个灵活、高效的前端框架,适合从小型项目到大型企业级应用的开发。它的易用性、灵活性和强大的社区支持使其成为许多开发者的首选框架之一。' - }, - reply: { - originalContent: '领导,我想请假', - prompt: '不批', - data: '您的请假申请已收悉,经核实和考虑,暂时无法批准您的请假申请。\n\n如有特殊情况或紧急事务,请及时与我联系。\n\n祝工作顺利。\n\n谢谢。' - } -} - -// ========== 【思维导图 UI】相关的枚举 ========== - -/** 思维导图已有内容生成示例 **/ -export const MindMapContentExample = `# Java 技术栈 - -## 核心技术 -### Java SE -### Java EE - -## 框架 -### Spring -#### Spring Boot -#### Spring MVC -#### Spring Data -### Hibernate -### MyBatis - -## 构建工具 -### Maven -### Gradle - -## 版本控制 -### Git -### SVN - -## 测试工具 -### JUnit -### Mockito -### Selenium - -## 应用服务器 -### Tomcat -### Jetty -### WildFly - -## 数据库 -### MySQL -### PostgreSQL -### Oracle -### MongoDB - -## 消息队列 -### Kafka -### RabbitMQ -### ActiveMQ - -## 微服务 -### Spring Cloud -### Dubbo - -## 容器化 -### Docker -### Kubernetes - -## 云服务 -### AWS -### Azure -### Google Cloud - -## 开发工具 -### IntelliJ IDEA -### Eclipse -### Visual Studio Code` diff --git a/src/views/ai/utils/utils.ts b/src/views/ai/utils/utils.ts deleted file mode 100644 index 4a70484..0000000 --- a/src/views/ai/utils/utils.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Created by iailab - * - * AI 枚举类 - * - * 问题:为什么不放在 src/utils/common-utils.ts 呢? - * 回答:主要 AI 是可选模块,考虑到独立、解耦,所以放在了 /views/ai/utils/common-utils.ts - */ - -/** 判断字符串是否包含中文 */ -export const hasChinese = (str: string) => { - return /[\u4e00-\u9fa5]/.test(str) -} diff --git a/src/views/ai/write/index/components/Left.vue b/src/views/ai/write/index/components/Left.vue deleted file mode 100644 index 05cc04a..0000000 --- a/src/views/ai/write/index/components/Left.vue +++ /dev/null @@ -1,213 +0,0 @@ -<template> - <!-- 定义 tab 组件:撰写/回复等 --> - <DefineTab v-slot="{ active, text, itemClick }"> - <span - class="inline-block w-1/2 rounded-full cursor-pointer text-center leading-[30px] relative z-1 text-[5C6370] hover:text-black" - :class="active ? 'text-black shadow-md' : 'hover:bg-[#DDDFE3]'" - @click="itemClick" - > - {{ text }} - </span> - </DefineTab> - <!-- 定义 label 组件:长度/格式/语气/语言等 --> - <DefineLabel v-slot="{ label, hint, hintClick }"> - <h3 class="mt-5 mb-3 flex items-center justify-between text-[14px]"> - <span>{{ label }}</span> - <span - @click="hintClick" - v-if="hint" - class="flex items-center text-[12px] text-[#846af7] cursor-pointer select-none" - > - <Icon icon="ep:question-filled" /> - {{ hint }} - </span> - </h3> - </DefineLabel> - - <div class="flex flex-col" v-bind="$attrs"> - <!-- tab --> - <div class="w-full pt-2 bg-[#f5f7f9] flex justify-center"> - <div class="w-[303px] rounded-full bg-[#DDDFE3] p-1 z-10"> - <div - class="flex items-center relative after:content-[''] after:block after:bg-white after:h-[30px] after:w-1/2 after:absolute after:top-0 after:left-0 after:transition-transform after:rounded-full" - :class=" - selectedTab === AiWriteTypeEnum.REPLY && 'after:transform after:translate-x-[100%]' - " - > - <ReuseTab - v-for="tab in tabs" - :key="tab.value" - :text="tab.text" - :active="tab.value === selectedTab" - :itemClick="() => switchTab(tab.value)" - /> - </div> - </div> - </div> - <div - class="px-7 pb-2 flex-grow overflow-y-auto lg:block w-[380px] box-border bg-[#f5f7f9] h-full" - > - <div> - <template v-if="selectedTab === 1"> - <ReuseLabel label="写作内容" hint="示例" :hint-click="() => example('write')" /> - <el-input - type="textarea" - :rows="5" - :maxlength="500" - v-model="formData.prompt" - placeholder="请输入写作内容" - showWordLimit - /> - </template> - - <template v-else> - <ReuseLabel label="原文" hint="示例" :hint-click="() => example('reply')" /> - <el-input - type="textarea" - :rows="5" - :maxlength="500" - v-model="formData.originalContent" - placeholder="请输入原文" - showWordLimit - /> - - <ReuseLabel label="回复内容" /> - <el-input - type="textarea" - :rows="5" - :maxlength="500" - v-model="formData.prompt" - placeholder="请输入回复内容" - showWordLimit - /> - </template> - - <ReuseLabel label="长度" /> - <Tag v-model="formData.length" :tags="getIntDictOptions(DICT_TYPE.AI_WRITE_LENGTH)" /> - <ReuseLabel label="格式" /> - <Tag v-model="formData.format" :tags="getIntDictOptions(DICT_TYPE.AI_WRITE_FORMAT)" /> - <ReuseLabel label="语气" /> - <Tag v-model="formData.tone" :tags="getIntDictOptions(DICT_TYPE.AI_WRITE_TONE)" /> - <ReuseLabel label="语言" /> - <Tag v-model="formData.language" :tags="getIntDictOptions(DICT_TYPE.AI_WRITE_LANGUAGE)" /> - - <div class="flex items-center justify-center mt-3"> - <el-button :disabled="isWriting" @click="reset">重置</el-button> - <el-button :loading="isWriting" @click="submit" color="#846af7">生成</el-button> - </div> - </div> - </div> - </div> -</template> - -<script setup lang="ts"> -import { createReusableTemplate } from '@vueuse/core' -import { ref } from 'vue' -import Tag from './Tag.vue' -import { WriteVO } from 'src/api/ai/write' -import { omit } from 'lodash-es' -import { DICT_TYPE, getIntDictOptions } from '@/utils/dict' -import { AiWriteTypeEnum, WriteExample } from '@/views/ai/utils/constants' - -type TabType = WriteVO['type'] - -const message = useMessage() // 消息弹窗 - -defineProps<{ - isWriting: boolean -}>() - -const emits = defineEmits<{ - (e: 'submit', params: Partial<WriteVO>) - (e: 'example', param: 'write' | 'reply') - (e: 'reset') -}>() - -/** 点击示例的时候,将定义好的文章作为示例展示出来 **/ -const example = (type: 'write' | 'reply') => { - formData.value = { - ...initData, - ...omit(WriteExample[type], ['data']) - } - emits('example', type) -} - -/** 重置,将表单值作为初选值 **/ -const reset = () => { - formData.value = { ...initData } - emits('reset') -} - -const selectedTab = ref<TabType>(AiWriteTypeEnum.WRITING) -const tabs: { - text: string - value: TabType -}[] = [ - { text: '撰写', value: AiWriteTypeEnum.WRITING }, - { text: '回复', value: AiWriteTypeEnum.REPLY } -] -const [DefineTab, ReuseTab] = createReusableTemplate<{ - active?: boolean - text: string - itemClick: () => void -}>() - -/** - * 可以在 template 里边定义可复用的组件,DefineLabel,ReuseLabel 是采用的解构赋值,都是 Vue 组件 - * - * 直接通过组件的形式使用,<DefineLabel v-slot="{ label, hint, hintClick }"> 中间是需要复用的组件代码 <DefineLabel />,通过 <ReuseLabel /> 来使用定义的组件 - * DefineLabel 里边的 v-slot="{ label, hint, hintClick }"相当于是解构了组件的 prop,需要注意的是 boolean 类型,需要显式的赋值比如 <ReuseLabel :flag="true" /> - * 事件也得以 prop 形式传入,不能是 @event的形式,比如下面的 hintClick 需要<ReuseLabel :hintClick="() => { doSomething }"/> - * - * @see https://vueuse.org/createReusableTemplate - */ -const [DefineLabel, ReuseLabel] = createReusableTemplate<{ - label: string - class?: string - hint?: string - hintClick?: () => void -}>() - -const initData: WriteVO = { - type: 1, - prompt: '', - originalContent: '', - tone: 1, - language: 1, - length: 1, - format: 1 -} -const formData = ref<WriteVO>({ ...initData }) - -/** 用来记录切换之前所填写的数据,切换的时候给赋值回来 **/ -const recordFormData = {} as Record<AiWriteTypeEnum, WriteVO> - -/** 切换tab **/ -const switchTab = (value: TabType) => { - if (value !== selectedTab.value) { - // 保存之前的久数据 - recordFormData[selectedTab.value] = formData.value - selectedTab.value = value - // 将之前的旧数据赋值回来 - formData.value = { ...initData, ...recordFormData[value] } - } -} - -/** 提交写作 */ -const submit = () => { - if (selectedTab.value === 2 && !formData.value.originalContent) { - message.warning('请输入原文') - return - } - if (!formData.value.prompt) { - message.warning(`请输入${selectedTab.value === 1 ? '写作' : '回复'}内容`) - return - } - emits('submit', { - /** 撰写的时候没有 originalContent 字段**/ - ...(selectedTab.value === 1 ? omit(formData.value, ['originalContent']) : formData.value), - /** 使用选中 tab 值覆盖当前的 type 类型 **/ - type: selectedTab.value - }) -} -</script> diff --git a/src/views/ai/write/index/components/Right.vue b/src/views/ai/write/index/components/Right.vue deleted file mode 100644 index d0aada5..0000000 --- a/src/views/ai/write/index/components/Right.vue +++ /dev/null @@ -1,120 +0,0 @@ -<template> - <el-card class="my-card h-full"> - <template #header> - <h3 class="m-0 px-7 shrink-0 flex items-center justify-between"> - <span>预览</span> - <!-- 展示在右上角 --> - <el-button color="#846af7" v-show="showCopy" @click="copyContent" 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 overflow-y-auto"> - <div class="w-full min-h-full relative flex-grow bg-white box-border p-3 sm:p-7"> - <!-- 终止生成内容的按钮 --> - <el-button - v-show="isWriting" - class="absolute bottom-2 sm:bottom-5 left-1/2 -translate-x-1/2 z-36" - @click="emits('stopStream')" - size="small" - > - <template #icon> - <Icon icon="material-symbols:stop" /> - </template> - 终止生成 - </el-button> - <el-input - id="inputId" - type="textarea" - v-model="compContent" - autosize - :input-style="{ boxShadow: 'none' }" - resize="none" - placeholder="生成的内容……" - /> - </div> - </div> - </el-card> -</template> - -<script setup lang="ts"> -import { useClipboard } from '@vueuse/core' - -const message = useMessage() // 消息弹窗 -const { copied, copy } = useClipboard() // 粘贴板 - -const props = defineProps({ - content: { - // 生成的结果 - type: String, - default: '' - }, - isWriting: { - // 是否正在生成文章 - type: Boolean, - default: false - } -}) - -const emits = defineEmits(['update:content', 'stopStream']) - -/** 通过计算属性,双向绑定,更改生成的内容,考虑到用户想要更改生成文章的情况 */ -const compContent = computed({ - get() { - return props.content - }, - set(val) { - emits('update:content', val) - } -}) - -/** 滚动 */ -const contentRef = ref<HTMLDivElement>() -defineExpose({ - scrollToBottom() { - contentRef.value?.scrollTo(0, contentRef.value?.scrollHeight) - } -}) - -/** 点击复制的时候复制内容 */ -const showCopy = computed(() => props.content && !props.isWriting) // 是否展示复制按钮,在生成内容完成的时候展示 -const copyContent = () => { - copy(props.content) -} - -/** 复制成功的时候 copied.value 为 true */ -watch(copied, (val) => { - if (val) { - message.success('复制成功') - } -}) -</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; - } -} -</style> diff --git a/src/views/ai/write/index/components/Tag.vue b/src/views/ai/write/index/components/Tag.vue deleted file mode 100644 index 3d616be..0000000 --- a/src/views/ai/write/index/components/Tag.vue +++ /dev/null @@ -1,32 +0,0 @@ -<!-- 标签选项 --> -<template> - <div class="flex flex-wrap gap-[8px]"> - <span - v-for="tag in props.tags" - :key="tag.value" - class="tag mb-2 border-[2px] border-solid border-[#DDDFE3] px-2 leading-6 text-[12px] bg-[#DDDFE3] rounded-[4px] cursor-pointer" - :class="modelValue === tag.value && '!border-[#846af7] text-[#846af7]'" - @click="emits('update:modelValue', tag.value)" - > - {{ tag.label }} - </span> - </div> -</template> - -<script setup lang="ts"> -const props = withDefaults( - defineProps<{ - tags: { label: string; value: string }[] - modelValue: string - [k: string]: any - }>(), - { - tags: () => [] - } -) - -const emits = defineEmits<{ - (e: 'update:modelValue', value: string): void -}>() -</script> -<style scoped></style> diff --git a/src/views/ai/write/index/index.vue b/src/views/ai/write/index/index.vue deleted file mode 100644 index 0dfda74..0000000 --- a/src/views/ai/write/index/index.vue +++ /dev/null @@ -1,76 +0,0 @@ -<template> - <div class="absolute top-0 left-0 right-0 bottom-0 flex"> - <Left - :is-writing="isWriting" - class="h-full" - @submit="submit" - @reset="reset" - @example="handleExampleClick" - /> - <Right - :is-writing="isWriting" - @stop-stream="stopStream" - ref="rightRef" - class="flex-grow" - v-model:content="writeResult" - /> - </div> -</template> - -<script setup lang="ts"> -import Left from './components/Left.vue' -import Right from './components/Right.vue' -import { WriteApi, WriteVO } from '@/api/ai/write' -import { WriteExample } from '@/views/ai/utils/constants' - -const message = useMessage() - -const writeResult = ref('') // 写作结果 -const isWriting = ref(false) // 是否正在写作中 -const abortController = ref<AbortController>() // // 写作进行中 abort 控制器(控制 stream 写作) - -/** 停止 stream 生成 */ -const stopStream = () => { - abortController.value?.abort() - isWriting.value = false -} - -/** 执行写作 */ -const rightRef = ref<InstanceType<typeof Right>>() -const submit = (data: WriteVO) => { - abortController.value = new AbortController() - writeResult.value = '' - isWriting.value = true - WriteApi.writeStream({ - data, - onMessage: async (res) => { - const { code, data, msg } = JSON.parse(res.data) - if (code !== 0) { - message.alert(`写作异常! ${msg}`) - stopStream() - return - } - writeResult.value = writeResult.value + data - // 滚动到底部 - await nextTick() - rightRef.value?.scrollToBottom() - }, - ctrl: abortController.value, - onClose: stopStream, - onError: (...err) => { - console.error('写作异常', ...err) - stopStream() - } - }) -} - -/** 点击示例触发 */ -const handleExampleClick = (type: keyof typeof WriteExample) => { - writeResult.value = WriteExample[type].data -} - -/** 点击重置的时候清空写作的结果**/ -const reset = () => { - writeResult.value = '' -} -</script> diff --git a/src/views/ai/write/manager/index.vue b/src/views/ai/write/manager/index.vue deleted file mode 100644 index 169f6b1..0000000 --- a/src/views/ai/write/manager/index.vue +++ /dev/null @@ -1,256 +0,0 @@ -<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="type"> - <el-select - v-model="queryParams.type" - placeholder="请选择写作类型" - clearable - class="!w-240px" - > - <el-option - v-for="dict in getIntDictOptions(DICT_TYPE.AI_WRITE_TYPE)" - :key="dict.value" - :label="dict.label" - :value="dict.value" - /> - </el-select> - </el-form-item> - <el-form-item label="平台" prop="platform"> - <el-select v-model="queryParams.platform" 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="createTime"> - <el-date-picker - v-model="queryParams.createTime" - value-format="YYYY-MM-DD HH:mm:ss" - type="daterange" - start-placeholder="开始日期" - end-placeholder="结束日期" - :default-time="[new Date('1 00:00:00'), new Date('1 23:59:59')]" - class="!w-240px" - /> - </el-form-item> - <el-form-item> - <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" /> 搜索</el-button> - <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" /> 重置</el-button> - <el-button - type="primary" - plain - @click="openForm('create')" - v-hasPermi="['ai:write:create']" - > - <Icon icon="ep:plus" class="mr-5px" /> 新增 - </el-button> - <!-- TODO @iailab 目前没有导出接口,需要导出吗 --> - <el-button - type="success" - plain - @click="handleExport" - :loading="exportLoading" - v-hasPermi="['ai:write:export']" - > - <Icon icon="ep:download" 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="120" fixed="left" /> - <el-table-column label="用户" align="center" prop="userId" width="180"> - <template #default="scope"> - <span>{{ userList.find((item) => item.id === scope.row.userId)?.nickname }}</span> - </template> - </el-table-column> - <el-table-column label="写作类型" align="center" prop="type"> - <template #default="scope"> - <dict-tag :type="DICT_TYPE.AI_WRITE_TYPE" :value="scope.row.type" /> - </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="prompt" - width="180" - show-overflow-tooltip - /> - <el-table-column label="生成的内容" align="center" prop="generatedContent" width="180" /> - <el-table-column label="原文" align="center" prop="originalContent" width="180" /> - <el-table-column label="长度" align="center" prop="length"> - <template #default="scope"> - <dict-tag :type="DICT_TYPE.AI_WRITE_LENGTH" :value="scope.row.length" /> - </template> - </el-table-column> - <el-table-column label="格式" align="center" prop="format"> - <template #default="scope"> - <dict-tag :type="DICT_TYPE.AI_WRITE_FORMAT" :value="scope.row.format" /> - </template> - </el-table-column> - <el-table-column label="语气" align="center" prop="tone"> - <template #default="scope"> - <dict-tag :type="DICT_TYPE.AI_WRITE_TONE" :value="scope.row.tone" /> - </template> - </el-table-column> - <el-table-column label="语言" align="center" prop="language"> - <template #default="scope"> - <dict-tag :type="DICT_TYPE.AI_WRITE_LANGUAGE" :value="scope.row.language" /> - </template> - </el-table-column> - <el-table-column - label="创建时间" - align="center" - prop="createTime" - :formatter="dateFormatter" - width="180px" - /> - <el-table-column label="错误信息" align="center" prop="errorMessage" /> - <el-table-column label="操作" align="center"> - <template #default="scope"> -<!-- TODO @iailab 目前没有修改接口,写作要可以更改吗--> - <el-button - link - type="primary" - @click="openForm('update', scope.row.id)" - v-hasPermi="['ai:write:update']" - > - 编辑 - </el-button> - <el-button - link - type="danger" - @click="handleDelete(scope.row.id)" - v-hasPermi="['ai:write: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 { DICT_TYPE, getIntDictOptions, getStrDictOptions } from '@/utils/dict' -import { dateFormatter } from '@/utils/formatTime' -import { useRouter } from 'vue-router' -import { WriteApi, AiWritePageReqVO, AiWriteRespVo } from '@/api/ai/write' -import * as UserApi from '@/api/system/user' - -/** AI 写作列表 */ -defineOptions({ name: 'AiWriteManager' }) - -const message = useMessage() // 消息弹窗 -const { t } = useI18n() // 国际化 -const router = useRouter() // 路由 - -const loading = ref(true) // 列表的加载中 -const list = ref<AiWriteRespVo[]>([]) // 列表的数据 -const total = ref(0) // 列表的总页数 -const queryParams = reactive<AiWritePageReqVO>({ - pageNo: 1, - pageSize: 10, - userId: undefined, - type: undefined, - platform: undefined, - createTime: undefined -}) -const queryFormRef = ref() // 搜索的表单 -const userList = ref<UserApi.UserVO[]>([]) // 用户列表 - -/** 查询列表 */ -const getList = async () => { - loading.value = true - try { - const data = await WriteApi.getWritePage(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 openForm = (type: string, id?: number) => { - switch (type) { - case 'create': - router.push('/ai/write') - break - } -} - -/** 删除按钮操作 */ -const handleDelete = async (id: number) => { - try { - // 删除的二次确认 - await message.delConfirm() - // 发起删除 - await WriteApi.deleteWrite(id) - message.success(t('common.delSuccess')) - // 刷新列表 - await getList() - } catch {} -} - -/** 初始化 **/ -onMounted(async () => { - getList() - // 获得用户列表 - userList.value = await UserApi.getSimpleUserList() -}) -</script> -- Gitblit v1.9.3