diff --git a/package.json b/package.json index 1fa88646..18be2c2e 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "private": false, "scripts": { "i": "pnpm install", - "dev": "vite", + "dev": "vite --mode env.local", "dev-server": "vite --mode dev", "ts:check": "vue-tsc --noEmit", "build:local": "node --max_old_space_size=8192 ./node_modules/vite/bin/vite.js build", @@ -29,6 +29,7 @@ "@form-create/designer": "^3.1.3", "@form-create/element-ui": "^3.1.24", "@iconify/iconify": "^3.1.1", + "@microsoft/fetch-event-source": "^2.0.1", "@videojs-player/vue": "^1.0.0", "@vueuse/core": "^10.9.0", "@wangeditor/editor": "^5.1.23", @@ -46,11 +47,12 @@ "driver.js": "^1.3.1", "echarts": "^5.5.0", "echarts-wordcloud": "^2.1.0", - "element-plus": "2.6.1", + "element-plus": "2.7.0", "fast-xml-parser": "^4.3.2", "highlight.js": "^11.9.0", "jsencrypt": "^3.3.2", "lodash-es": "^4.17.21", + "marked": "^12.0.2", "min-dash": "^4.1.1", "mitt": "^3.0.1", "nprogress": "^0.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0c2351e0..55dceb8d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ dependencies: '@iconify/iconify': specifier: ^3.1.1 version: 3.1.1 + '@microsoft/fetch-event-source': + specifier: ^2.0.1 + version: 2.0.1 '@videojs-player/vue': specifier: ^1.0.0 version: 1.0.0(@types/video.js@7.3.58)(video.js@7.21.5)(vue@3.4.21) @@ -69,8 +72,8 @@ dependencies: specifier: ^2.1.0 version: 2.1.0(echarts@5.5.0) element-plus: - specifier: 2.6.1 - version: 2.6.1(vue@3.4.21) + specifier: 2.7.0 + version: 2.7.0(vue@3.4.21) fast-xml-parser: specifier: ^4.3.2 version: 4.3.6 @@ -83,6 +86,9 @@ dependencies: lodash-es: specifier: ^4.17.21 version: 4.17.21 + marked: + specifier: ^12.0.2 + version: 12.0.2 min-dash: specifier: ^4.1.1 version: 4.2.1 @@ -2360,6 +2366,10 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true + /@microsoft/fetch-event-source@2.0.1: + resolution: {integrity: sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==, tarball: https://registry.npmmirror.com/@microsoft/fetch-event-source/-/fetch-event-source-2.0.1.tgz} + dev: false + /@nodelib/fs.scandir@2.1.5: resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==, tarball: https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz} engines: {node: '>= 8'} @@ -5082,8 +5092,8 @@ packages: resolution: {integrity: sha512-9ItEpeu15hW5m8jKdriL+BQrgwDTXEL9pn4SkillWFu73ZNNNQ2BKKLS+ZHv2vC9UkNhosAeyfxOf/5OSeTCPA==, tarball: https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.4.750.tgz} dev: true - /element-plus@2.6.1(vue@3.4.21): - resolution: {integrity: sha512-6VRpLjwtIVdtUuITJPPKtpOH1NM6nuAkRE3q5O4Lrx0N1bYMhTkiqb2Jy7zfQuDPbOIkkF2OABTzegpNnzgsnQ==, tarball: https://registry.npmmirror.com/element-plus/-/element-plus-2.6.1.tgz} + /element-plus@2.7.0(vue@3.4.21): + resolution: {integrity: sha512-WAiaFLavuWFxof9qwkC27jvkh9nRcNnB506g1vvJSiVaVqjCBWUFCIyJKeN11M1qcv2cS5VV5PfSLjTIkrw87A==, tarball: https://registry.npmmirror.com/element-plus/-/element-plus-2.7.0.tgz} peerDependencies: vue: ^3.2.0 dependencies: @@ -6917,6 +6927,12 @@ packages: object-visit: 1.0.1 dev: true + /marked@12.0.2: + resolution: {integrity: sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==, tarball: https://registry.npmmirror.com/marked/-/marked-12.0.2.tgz} + engines: {node: '>= 18'} + hasBin: true + dev: false + /matches-selector@1.2.0: resolution: {integrity: sha512-c4vLwYWyl+Ji+U43eU/G5FwxWd4ZH0ePUsFs5y0uwD9HUEFBXUQ1zUUan+78IpRD+y4pUfG0nAzNM292K7ItvA==, tarball: https://registry.npmmirror.com/matches-selector/-/matches-selector-1.2.0.tgz} dev: true diff --git a/src/App.vue b/src/App.vue index 7407d97a..1f5f36d7 100644 --- a/src/App.vue +++ b/src/App.vue @@ -54,4 +54,8 @@ body { .#{$prefix-cls}-grey-mode { filter: grayscale(100%); } + +.scrollbar__view { + height: 99%!important; +} diff --git a/src/api/ai/chat/conversation/index.ts b/src/api/ai/chat/conversation/index.ts new file mode 100644 index 00000000..08ad26a1 --- /dev/null +++ b/src/api/ai/chat/conversation/index.ts @@ -0,0 +1,64 @@ +import request from '@/config/axios' + +// AI 聊天对话 VO +export interface ChatConversationVO { + id: string // ID 编号 + userId: number // 用户编号 + title: string // 对话标题 + pinned: boolean // 是否置顶 + roleId: number // 角色编号 + modelId: number // 模型编号 + model: string // 模型标志 + temperature: number // 温度参数 + maxTokens: number // 单条回复的最大 Token 数量 + maxContexts: number // 上下文的最大 Message 数量 + // 额外字段 + systemMessage?: string // 角色设定 + modelName?: string // 模型名字 + roleAvatar?: string // 角色头像 + modelMaxTokens?: string // 模型的单条回复的最大 Token 数量 + modelMaxContexts?: string // 模型的上下文的最大 Message 数量 +} + +// AI 聊天对话 API +export const ChatConversationApi = { + // 获得【我的】聊天对话 + getChatConversationMy: async (id: string) => { + return await request.get({ url: `/ai/chat/conversation/get-my?id=${id}` }) + }, + + // 新增【我的】聊天对话 + createChatConversationMy: async (data?: ChatConversationVO) => { + return await request.post({ url: `/ai/chat/conversation/create-my`, data }) + }, + + // 更新【我的】聊天对话 + updateChatConversationMy: async (data: ChatConversationVO) => { + return await request.put({ url: `/ai/chat/conversation/update-my`, data }) + }, + + // 删除【我的】聊天对话 + deleteChatConversationMy: async (id: string) => { + return await request.delete({ url: `/ai/chat/conversation/delete-my?id=${id}` }) + }, + + // 删除【我的】所有对话,置顶除外 + deleteMyAllExceptPinned: async () => { + return await request.delete({ url: `/ai/chat/conversation/delete-my-all-except-pinned` }) + }, + + // 获得【我的】聊天对话列表 + getChatConversationMyList: async () => { + return await request.get({ url: `/ai/chat/conversation/my-list` }) + }, + + // 获得对话分页 + 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 new file mode 100644 index 00000000..f9e807a1 --- /dev/null +++ b/src/api/ai/chat/message/index.ts @@ -0,0 +1,88 @@ +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 // 创建时间 +} + +export interface ChatMessageSendVO { + conversationId: string // 对话编号 + content: number // 聊天内容 +} + +// AI chat 聊天 +export const ChatMessageApi = { + // 消息列表 + messageList: async (conversationId: string | null) => { + return await request.get({ + url: `/ai/chat/message/list-by-conversation-id?conversationId=${conversationId}` + }) + }, + + // 发送 send stream 消息 + // TODO axios 可以么? https://apifox.com/apiskills/how-to-create-axios-stream/ + sendStream: async ( + conversationId: number, + content: string, + ctrl, + 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 + }) + }, + + // 删除消息 + delete: async (id: string) => { + return await request.delete({ url: `/ai/chat/message/delete?id=${id}` }) + }, + + // 删除消息 - 对话所有消息 + deleteByConversationId: async (conversationId: string) => { + return await request.delete({ + url: `/ai/chat/message/delete-by-conversation-id?conversationId=${conversationId}` + }) + }, + + // 获得消息分页 + getChatMessagePage: async (params: any) => { + return await request.get({ url: '/ai/chat/message/page', params }) + }, + + // 管理员删除消息 + deleteChatMessageByAdmin: async (id: number) => { + return await request.delete({ url: `/ai/chat/message/delete-by-admin?id=${id}` }) + } +} diff --git a/src/api/ai/image/index.ts b/src/api/ai/image/index.ts new file mode 100644 index 00000000..4fd35abe --- /dev/null +++ b/src/api/ai/image/index.ts @@ -0,0 +1,98 @@ +import request from '@/config/axios' + +// AI API 密钥 VO +// TODO @fan:要不前端不弄太多 VO,就用这个 ImageDetailVO?! +export interface ImageDetailVO { + id: number // 编号 + prompt: string // 提示词 + status: number // 状态 + errorMessage: string // 错误信息 + type: string // 模型下分不同的类型(清晰、真实...) + taskId: number // dr 任务id + picUrl: string // 任务地址 + originalPicUrl: string // 绘制图片地址 + platform: string // 平台 + model: string // 模型 + style: string // 图像生成的风格 + size: string // 图片尺寸 + buttons: ImageMjButtonsVO[] // mj 操作按钮 + createTime: string // 创建时间 + updateTime: string // 更新事件 +} + +export interface ImageMjButtonsVO { + customId: string // MJ::JOB::upsample::1::85a4b4c1-8835-46c5-a15c-aea34fad1862 动作标识 + emoji: string // 图标 emoji + label: string // Make Variations 文本 + style: number // 样式: 2(Primary)、3(Green) +} + +export interface ImageMjActionVO { + id: string // MJ::JOB::upsample::1::85a4b4c1-8835-46c5-a15c-aea34fad1862 动作标识 + customId: string // MJ::JOB::upsample::1::85a4b4c1-8835-46c5-a15c-aea34fad1862 动作标识 +} + + +export interface ImagePageReqVO { + pageNo: number // 分页编号 + pageSize: number // 分页大小 +} + +export interface ImageDallReqVO { + prompt: string // 提示词 + model: string // 模型 + style: string // 图像生成的风格 + width: string // 图片宽度 + height: string // 图片高度 +} + +export interface ImageDrawReqVO { + platform: string // 平台 + prompt: string // 提示词 + model: string // 模型 + style: string // 图像生成的风格 + width: string // 图片宽度 + height: string // 图片高度 + options: object // 绘制参数,Map +} + +export interface ImageMidjourneyImagineReqVO { + prompt: string // 提示词 + model: string // 模型 mj nijj + base64Array: string[] // size不能为空 + width: string // 图片宽度 + height: string // 图片高度 + version: string // 版本 +} + +// TODO 芋艿:review 下整体注释、方法名 +// AI API 密钥 API +export const ImageApi = { + // 获取 image 列表 + getImageList: async (params: ImagePageReqVO) => { + return await request.get({ url: `/ai/image/my-page`, params }) + }, + // 获取 image 详细信息 + getImageDetail: async (id: number) => { + return await request.get({ url: `/ai/image/get-my?id=${id}`}) + }, + // 生成图片 + drawImage: async (data: ImageDrawReqVO)=> { + return await request.post({ url: `/ai/image/draw`, data }) + }, + // 删除 + deleteImage: async (id: number)=> { + return await request.delete({ url: `/ai/image/delete-my?id=${id}`}) + }, + + // ------------ midjourney + + // midjourney - imagine + midjourneyImagine: async (data: ImageMidjourneyImagineReqVO)=> { + return await request.post({ url: `/ai/image/midjourney/imagine`, data }) + }, + // midjourney - action + midjourneyAction: async (params: ImageMjActionVO)=> { + return await request.get({ url: `/ai/image/midjourney/action`, params }) + }, +} diff --git a/src/api/ai/model/apiKey/index.ts b/src/api/ai/model/apiKey/index.ts new file mode 100644 index 00000000..ed94836e --- /dev/null +++ b/src/api/ai/model/apiKey/index.ts @@ -0,0 +1,44 @@ +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 new file mode 100644 index 00000000..c2ef4c8d --- /dev/null +++ b/src/api/ai/model/chatModel/index.ts @@ -0,0 +1,53 @@ +import request from '@/config/axios' + +// AI 聊天模型 VO +export interface ChatModelVO { + id: number // 编号 + keyId: number // API 秘钥编号 + name: string // 模型名字 + model: string // 模型标识 + platform: string // 模型平台 + sort: number // 排序 + status: number // 状态 + temperature: number // 温度参数 + maxTokens: number // 单条回复的最大 Token 数量 + maxContexts: number // 上下文的最大 Message 数量 +} + +// AI 聊天模型 API +export const ChatModelApi = { + // 查询聊天模型分页 + getChatModelPage: async (params: any) => { + return await request.get({ url: `/ai/chat-model/page`, params }) + }, + + // 获得聊天模型列表 + getChatModelSimpleList: async (status?: number) => { + return await request.get({ + url: `/ai/chat-model/simple-list`, + params: { + status + } + }) + }, + + // 查询聊天模型详情 + getChatModel: async (id: number) => { + return await request.get({ url: `/ai/chat-model/get?id=` + id }) + }, + + // 新增聊天模型 + createChatModel: async (data: ChatModelVO) => { + return await request.post({ url: `/ai/chat-model/create`, data }) + }, + + // 修改聊天模型 + updateChatModel: async (data: ChatModelVO) => { + return await request.put({ url: `/ai/chat-model/update`, data }) + }, + + // 删除聊天模型 + deleteChatModel: async (id: number) => { + return await request.delete({ url: `/ai/chat-model/delete?id=` + id }) + } +} diff --git a/src/api/ai/model/chatRole/index.ts b/src/api/ai/model/chatRole/index.ts new file mode 100644 index 00000000..a9fce13c --- /dev/null +++ b/src/api/ai/model/chatRole/index.ts @@ -0,0 +1,80 @@ +import request from '@/config/axios' + +// AI 聊天角色 VO +export interface ChatRoleVO { + id: number // 角色编号 + modelId: number // 模型编号 + name: string // 角色名称 + avatar: string // 角色头像 + category: string // 角色类别 + sort: number // 角色排序 + description: string // 角色描述 + systemMessage: string // 角色设定 + welcomeMessage: string // 角色设定 + publicStatus: boolean // 是否公开 + status: number // 状态 +} + +// AI 聊天角色 分页请求 vo +export interface ChatRolePageReqVO { + name?: string // 角色名称 + category?: string // 角色类别 + publicStatus: boolean // 是否公开 + pageNo: number // 是否公开 + pageSize: number // 是否公开 +} + +// AI 聊天角色 API +export const ChatRoleApi = { + // 查询聊天角色分页 + getChatRolePage: async (params: any) => { + return await request.get({ url: `/ai/chat-role/page`, params }) + }, + + // 查询聊天角色详情 + getChatRole: async (id: number) => { + return await request.get({ url: `/ai/chat-role/get?id=` + id }) + }, + + // 新增聊天角色 + createChatRole: async (data: ChatRoleVO) => { + return await request.post({ url: `/ai/chat-role/create`, data }) + }, + + // 修改聊天角色 + updateChatRole: async (data: ChatRoleVO) => { + return await request.put({ url: `/ai/chat-role/update`, data }) + }, + + // 删除聊天角色 + deleteChatRole: async (id: number) => { + return await request.delete({ url: `/ai/chat-role/delete?id=` + id }) + }, + + // ======= chat 聊天 + + // 获取 my role + getMyPage: async (params: ChatRolePageReqVO) => { + return await request.get({ url: `/ai/chat-role/my-page`, params}) + }, + + // 获取角色分类 + getCategoryList: async () => { + return await request.get({ url: `/ai/chat-role/category-list`}) + }, + + // 创建角色 + createMy: async (data: ChatRoleVO) => { + return await request.post({ url: `/ai/chat-role/create-my`, data}) + }, + + // 更新角色 + updateMy: async (data: ChatRoleVO) => { + return await request.put({ url: `/ai/chat-role/update-my`, data}) + }, + + // 删除角色 my + deleteMy: async (id: number) => { + return await request.delete({ url: `/ai/chat-role/delete-my?id=` + id }) + }, +} diff --git a/src/assets/ai/clear.svg b/src/assets/ai/clear.svg new file mode 100644 index 00000000..e75a4e8a --- /dev/null +++ b/src/assets/ai/clear.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/ai/copy-style2.svg b/src/assets/ai/copy-style2.svg new file mode 100644 index 00000000..2d56a87f --- /dev/null +++ b/src/assets/ai/copy-style2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/ai/copy.svg b/src/assets/ai/copy.svg new file mode 100644 index 00000000..f51f8d81 --- /dev/null +++ b/src/assets/ai/copy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/ai/delete.svg b/src/assets/ai/delete.svg new file mode 100644 index 00000000..d2ee18ed --- /dev/null +++ b/src/assets/ai/delete.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/ai/gpt.svg b/src/assets/ai/gpt.svg new file mode 100644 index 00000000..603e2e95 --- /dev/null +++ b/src/assets/ai/gpt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/FormCreate/src/components/useApiSelect.tsx b/src/components/FormCreate/src/components/useApiSelect.tsx index a67e6f68..29cd3027 100644 --- a/src/components/FormCreate/src/components/useApiSelect.tsx +++ b/src/components/FormCreate/src/components/useApiSelect.tsx @@ -27,6 +27,11 @@ export const useApiSelect = (option: ApiSelectProps) => { type: String, default: 'GET' }, + // 选项解析函数 + parseFunc: { + type: String, + default: '' + }, // 请求参数 data: { type: String, @@ -41,35 +46,121 @@ export const useApiSelect = (option: ApiSelectProps) => { multiple: { type: Boolean, default: false + }, + // 是否远程搜索 + remote: { + type: Boolean, + default: false + }, + // 远程搜索时携带的参数 + remoteField: { + type: String, + default: 'label' } }, setup(props) { const attrs = useAttrs() const options = ref([]) // 下拉数据 + const loading = ref(false) // 是否正在从远程获取数据 + const queryParam = ref() // 当前输入的值 const getOptions = async () => { options.value = [] // 接口选择器 if (isEmpty(props.url)) { return } - let data = [] switch (props.method) { case 'GET': - data = await request.get({ url: props.url }) + let url: string = props.url + if (props.remote) { + url = `${url}?${props.remoteField}=${queryParam.value}` + } + parseOptions(await request.get({ url: url })) break case 'POST': - data = await request.post({ url: props.url, data: jsonParse(props.data) }) + const data: any = jsonParse(props.data) + if (props.remote) { + data[props.remoteField] = queryParam.value + } + parseOptions(await request.post({ url: props.url, data: data })) break } + } + function parseOptions(data: any) { + // 情况一:如果有自定义解析函数优先使用自定义解析 + if (!isEmpty(props.parseFunc)) { + options.value = parseFunc()?.(data) + return + } + // 情况二:返回的直接是一个列表 + if (Array.isArray(data)) { + parseOptions0(data) + return + } + // 情况二:返回的是分页数据,尝试读取 list + data = data.list + if (!!data && Array.isArray(data)) { + parseOptions0(data) + return + } + // 情况三:不是 yudao-vue-pro 标准返回 + console.warn( + `接口[${props.url}] 返回结果不是 yudao-vue-pro 标准返回建议采用自定义解析函数处理` + ) + } + + function parseOptions0(data: any[]) { if (Array.isArray(data)) { options.value = data.map((item: any) => ({ - label: item[props.labelField], - value: item[props.valueField] + label: parseExpression(item, props.labelField), + value: parseExpression(item, props.valueField) })) return } - console.error(`接口[${props.url}] 返回结果不是一个数组`) + console.warn(`接口[${props.url}] 返回结果不是一个数组`) + } + + function parseFunc() { + let parse: any = null + if (!!props.parseFunc) { + // 解析字符串函数 + parse = new Function(`return ${props.parseFunc}`)() + } + return parse + } + + function parseExpression(data: any, template: string) { + // 检测是否使用了表达式 + if (template.indexOf('${') === -1) { + return data[template] + } + // 正则表达式匹配模板字符串中的 ${...} + const pattern = /\$\{([^}]*)}/g + // 使用replace函数配合正则表达式和回调函数来进行替换 + return template.replace(pattern, (_, expr) => { + // expr 是匹配到的 ${} 内的表达式(这里是属性名),从 data 中获取对应的值 + const result = data[expr.trim()] // 去除前后空白,以防用户输入带空格的属性名 + if (!result) { + console.warn( + `接口选择器选项模版[${template}][${expr.trim()}] 解析值失败结果为[${result}], 请检查属性名称是否存在于接口返回值中,存在则忽略此条!!!` + ) + } + return result + }) + } + + const remoteMethod = async (query: any) => { + if (!query) { + return + } + loading.value = true + try { + queryParam.value = query + await getOptions() + } finally { + loading.value = false + } } onMounted(async () => { @@ -80,15 +171,29 @@ export const useApiSelect = (option: ApiSelectProps) => { if (props.multiple) { // fix:多写此步是为了解决 multiple 属性问题 return ( - + {options.value.map((item, index) => ( ))} ) } + debugger return ( - + {options.value.map((item, index) => ( ))} diff --git a/src/components/FormCreate/src/config/selectRule.ts b/src/components/FormCreate/src/config/selectRule.ts index 281d3739..a6f3841f 100644 --- a/src/components/FormCreate/src/config/selectRule.ts +++ b/src/components/FormCreate/src/config/selectRule.ts @@ -13,12 +13,30 @@ const selectRule = [ control: [ { value: 'select', - condition: '=', + condition: '==', method: 'hidden', - rule: ['multiple'] + rule: [ + 'multiple', + 'clearable', + 'collapseTags', + 'multipleLimit', + 'allowCreate', + 'filterable', + 'noMatchText', + 'remote', + 'remoteMethod', + 'reserveKeyword', + 'defaultFirstOption', + 'automaticDropdown' + ] } ] }, + { + type: 'switch', + field: 'filterable', + title: '是否可搜索' + }, { type: 'switch', field: 'multiple', title: '是否多选' }, { type: 'switch', @@ -43,27 +61,12 @@ const selectRule = [ title: 'autocomplete 属性' }, { type: 'input', field: 'placeholder', title: '占位符' }, - { - type: 'switch', - field: 'filterable', - title: '是否可搜索' - }, { type: 'switch', field: 'allowCreate', title: '是否允许用户创建新条目' }, { type: 'input', field: 'noMatchText', title: '搜索条件无匹配时显示的文字' }, - { - type: 'switch', - field: 'remote', - title: '其中的选项是否从服务器远程加载' - }, - { - type: 'Struct', - field: 'remoteMethod', - title: '自定义远程搜索方法' - }, { type: 'input', field: 'noDataText', title: '选项为空时显示的文字' }, { type: 'switch', @@ -130,6 +133,7 @@ const apiSelectRule = [ type: 'input', field: 'labelField', title: 'label 属性', + info: '可以使用 el 表达式:${属性},来实现复杂数据组合。如:${nickname}-${id}', props: { placeholder: 'nickname' } @@ -138,9 +142,39 @@ const apiSelectRule = [ type: 'input', field: 'valueField', title: 'value 属性', + info: '可以使用 el 表达式:${属性},来实现复杂数据组合。如:${nickname}-${id}', props: { placeholder: 'id' } + }, + { + type: 'input', + field: 'parseFunc', + title: '选项解析函数', + info: `data 为接口返回值,需要写一个匿名函数解析返回值为选择器 options 列表 + (data: any)=>{ label: string; value: any }[]`, + props: { + autosize: true, + rows: { minRows: 2, maxRows: 6 }, + type: 'textarea', + placeholder: ` + function (data) { + console.log(data) + return data.list.map(item=> ({label: item.nickname,value: item.id})) + }` + } + }, + { + type: 'switch', + field: 'remote', + info: '是否可搜索', + title: '其中的选项是否从服务器远程加载' + }, + { + type: 'input', + field: 'remoteField', + title: '请求参数', + info: '远程请求时请求携带的参数名称,如:name' } ] diff --git a/src/components/FormCreate/src/config/useDictSelectRule.ts b/src/components/FormCreate/src/config/useDictSelectRule.ts index 3db630bc..5c5e8cad 100644 --- a/src/components/FormCreate/src/config/useDictSelectRule.ts +++ b/src/components/FormCreate/src/config/useDictSelectRule.ts @@ -2,6 +2,7 @@ import { generateUUID } from '@/utils' import * as DictDataApi from '@/api/system/dict/dict.type' import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils' import { selectRule } from '@/components/FormCreate/src/config/selectRule' +import { cloneDeep } from 'lodash-es' /** * 字典选择器规则,如果规则使用到动态数据则需要单独配置不能使用 useSelectRule @@ -9,6 +10,7 @@ import { selectRule } from '@/components/FormCreate/src/config/selectRule' export const useDictSelectRule = () => { const label = '字典选择器' const name = 'DictSelect' + const rules = cloneDeep(selectRule) const dictOptions = ref<{ label: string; value: string }[]>([]) // 字典类型下拉数据 onMounted(async () => { const data = await DictDataApi.getSimpleDictTypeList() @@ -55,7 +57,7 @@ export const useDictSelectRule = () => { { label: '布尔值', value: 'bool' } ] }, - ...selectRule + ...rules ]) } } diff --git a/src/components/FormCreate/src/config/useSelectRule.ts b/src/components/FormCreate/src/config/useSelectRule.ts index bf20588d..ff21a223 100644 --- a/src/components/FormCreate/src/config/useSelectRule.ts +++ b/src/components/FormCreate/src/config/useSelectRule.ts @@ -2,6 +2,7 @@ import { generateUUID } from '@/utils' import { localeProps, makeRequiredRule } from '@/components/FormCreate/src/utils' import { selectRule } from '@/components/FormCreate/src/config/selectRule' import { SelectRuleOption } from '@/components/FormCreate/src/type' +import { cloneDeep } from 'lodash-es' /** * 通用选择器规则 hook @@ -11,6 +12,7 @@ import { SelectRuleOption } from '@/components/FormCreate/src/type' export const useSelectRule = (option: SelectRuleOption) => { const label = option.label const name = option.name + const rules = cloneDeep(selectRule) return { icon: option.icon, label, @@ -28,7 +30,7 @@ export const useSelectRule = (option: SelectRuleOption) => { if (!option.props) { option.props = [] } - return localeProps(t, name + '.props', [makeRequiredRule(), ...option.props, ...selectRule]) + return localeProps(t, name + '.props', [makeRequiredRule(), ...option.props, ...rules]) } } } diff --git a/src/components/FormCreate/src/useFormCreateDesigner.ts b/src/components/FormCreate/src/useFormCreateDesigner.ts index 69c8f314..53fee78e 100644 --- a/src/components/FormCreate/src/useFormCreateDesigner.ts +++ b/src/components/FormCreate/src/useFormCreateDesigner.ts @@ -71,9 +71,9 @@ export const useFormCreateDesigner = async (designer: Ref) => { */ const buildSystemMenu = () => { // 移除自带的下拉选择器组件,使用 currencySelectRule 替代 - designer.value?.removeMenuItem('select') - designer.value?.removeMenuItem('radio') - designer.value?.removeMenuItem('checkbox') + // designer.value?.removeMenuItem('select') + // designer.value?.removeMenuItem('radio') + // designer.value?.removeMenuItem('checkbox') const components = [userSelectRule, deptSelectRule, dictSelectRule, apiSelectRule0] const menu: Menu = { name: 'system', diff --git a/src/components/FormCreate/src/utils/index.ts b/src/components/FormCreate/src/utils/index.ts index e5480981..2d4a6fd7 100644 --- a/src/components/FormCreate/src/utils/index.ts +++ b/src/components/FormCreate/src/utils/index.ts @@ -1,4 +1,3 @@ -// TODO puhui999: 借鉴一下 form-create-designer utils 方法 🤣 (导入不了只能先 copy 过来用下) export function makeRequiredRule() { return { type: 'Required', @@ -17,63 +16,3 @@ export const localeProps = (t, prefix, rules) => { return rule }) } - -export function upper(str) { - return str.replace(str[0], str[0].toLocaleUpperCase()) -} - -export function makeOptionsRule(t, to, userOptions) { - console.log(userOptions[0]) - const options = [ - { label: t('props.optionsType.struct'), value: 0 }, - { label: t('props.optionsType.json'), value: 1 }, - { label: '用户数据', value: 2 } - ] - - const control = [ - { - value: 0, - rule: [ - { - type: 'TableOptions', - field: 'formCreate' + upper(to).replace('.', '>'), - props: { defaultValue: [] } - } - ] - }, - { - value: 1, - rule: [ - { - type: 'Struct', - field: 'formCreate' + upper(to).replace('.', '>'), - props: { defaultValue: [] } - } - ] - }, - { - value: 2, - rule: [ - { - type: 'TableOptions', - field: 'formCreate' + upper(to).replace('.', '>'), - props: { modelValue: [] } - } - ] - } - ] - options.splice(0, 0) - control.push() - - return { - type: 'radio', - title: t('props.options'), - field: '_optionType', - value: 0, - options, - props: { - type: 'button' - }, - control - } -} diff --git a/src/components/Icon/src/Icon.vue b/src/components/Icon/src/Icon.vue index 4246539f..a90bb37e 100644 --- a/src/components/Icon/src/Icon.vue +++ b/src/components/Icon/src/Icon.vue @@ -22,7 +22,7 @@ const props = defineProps({ const elRef = ref(null) -const isLocal = computed(() => props.icon.startsWith('svg-icon:')) +const isLocal = computed(() => props.icon?.startsWith('svg-icon:')) const symbolId = computed(() => { return unref(isLocal) ? `#icon-${props.icon.split('svg-icon:')[1]}` : props.icon diff --git a/src/components/MarkdownView/index.vue b/src/components/MarkdownView/index.vue new file mode 100644 index 00000000..bf9ddd60 --- /dev/null +++ b/src/components/MarkdownView/index.vue @@ -0,0 +1,235 @@ + + + + + + + diff --git a/src/layout/Layout.vue b/src/layout/Layout.vue index 43f9b69d..af519707 100644 --- a/src/layout/Layout.vue +++ b/src/layout/Layout.vue @@ -72,7 +72,7 @@ $prefix-cls: #{$namespace}-layout; .#{$prefix-cls} { background-color: var(--app-content-bg-color); :deep(.#{$elNamespace}-scrollbar__view) { - height: 100% !important; + height: 99% !important; } } diff --git a/src/permission.ts b/src/permission.ts index d538303b..b04bc3c1 100644 --- a/src/permission.ts +++ b/src/permission.ts @@ -83,7 +83,7 @@ router.beforeEach(async (to, from, next) => { const redirectPath = from.query.redirect || to.path // 修复跳转时不带参数的问题 const redirect = decodeURIComponent(redirectPath as string) - const { basePath, paramsObject: query } = parseURL(redirect) + const { paramsObject: query } = parseURL(redirect) const nextData = to.path === redirect ? { ...to, replace: true } : { path: redirect, query } next(nextData) } else { diff --git a/src/utils/dict.ts b/src/utils/dict.ts index 631a40b0..9400208a 100644 --- a/src/utils/dict.ts +++ b/src/utils/dict.ts @@ -1,8 +1,8 @@ /** * 数据字典工具类 */ -import {useDictStoreWithOut} from '@/store/modules/dict' -import {ElementPlusInfoType} from '@/types/elementPlus' +import { useDictStoreWithOut } from '@/store/modules/dict' +import { ElementPlusInfoType } from '@/types/elementPlus' const dictStore = useDictStoreWithOut() @@ -24,6 +24,10 @@ export interface NumberDictDataType extends DictDataType { value: number } +export interface StringDictDataType extends DictDataType { + value: string +} + export const getDictOptions = (dictType: string) => { return dictStore.getDictByType(dictType) || [] } @@ -44,8 +48,11 @@ export const getIntDictOptions = (dictType: string): NumberDictDataType[] => { } export const getStrDictOptions = (dictType: string) => { - const dictOption: DictDataType[] = [] + // 获得通用的 DictDataType 列表 const dictOptions: DictDataType[] = getDictOptions(dictType) + // 转换成 string 类型的 StringDictDataType 类型 + // why 需要特殊转换:避免 IDEA 在 v-for="dict in getStrDictOptions(...)" 时,el-option 的 key 会告警 + const dictOption: StringDictDataType[] = [] dictOptions.forEach((dict: DictDataType) => { dictOption.push({ ...dict, @@ -209,5 +216,8 @@ export enum DICT_TYPE { // ========== ERP - 企业资源计划模块 ========== ERP_AUDIT_STATUS = 'erp_audit_status', // ERP 审批状态 - ERP_STOCK_RECORD_BIZ_TYPE = 'erp_stock_record_biz_type' // 库存明细的业务类型 + ERP_STOCK_RECORD_BIZ_TYPE = 'erp_stock_record_biz_type', // 库存明细的业务类型 + + // ========== AI - 人工智能模块 ========== + AI_PLATFORM = 'ai_platform' // AI 平台 } diff --git a/src/utils/download.ts b/src/utils/download.ts index ab200149..fe24ee27 100644 --- a/src/utils/download.ts +++ b/src/utils/download.ts @@ -29,7 +29,7 @@ const download = { html: (data: Blob, fileName: string) => { download0(data, fileName, 'text/html') }, - // 下载 Markdown 方法 + // 下载 MarkdownView 方法 markdown: (data: Blob, fileName: string) => { download0(data, fileName, 'text/markdown') } diff --git a/src/views/ai/chat/ChatEmpty.vue b/src/views/ai/chat/ChatEmpty.vue new file mode 100644 index 00000000..4268b3d7 --- /dev/null +++ b/src/views/ai/chat/ChatEmpty.vue @@ -0,0 +1,79 @@ + + + diff --git a/src/views/ai/chat/Conversation.vue b/src/views/ai/chat/Conversation.vue new file mode 100644 index 00000000..2afeb9ac --- /dev/null +++ b/src/views/ai/chat/Conversation.vue @@ -0,0 +1,521 @@ + + + + + + diff --git a/src/views/ai/chat/Message.vue b/src/views/ai/chat/Message.vue new file mode 100644 index 00000000..942961ed --- /dev/null +++ b/src/views/ai/chat/Message.vue @@ -0,0 +1,322 @@ + + + + diff --git a/src/views/ai/chat/MessageLoading.vue b/src/views/ai/chat/MessageLoading.vue new file mode 100644 index 00000000..f3198cbd --- /dev/null +++ b/src/views/ai/chat/MessageLoading.vue @@ -0,0 +1,15 @@ + + + + + diff --git a/src/views/ai/chat/MessageNewChat.vue b/src/views/ai/chat/MessageNewChat.vue new file mode 100644 index 00000000..aac5f905 --- /dev/null +++ b/src/views/ai/chat/MessageNewChat.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/src/views/ai/chat/components/ChatConversationUpdateForm.vue b/src/views/ai/chat/components/ChatConversationUpdateForm.vue new file mode 100644 index 00000000..c9f5c849 --- /dev/null +++ b/src/views/ai/chat/components/ChatConversationUpdateForm.vue @@ -0,0 +1,141 @@ + + diff --git a/src/views/ai/chat/components/Header.vue b/src/views/ai/chat/components/Header.vue new file mode 100644 index 00000000..17b1693b --- /dev/null +++ b/src/views/ai/chat/components/Header.vue @@ -0,0 +1,48 @@ + + + + + + diff --git a/src/views/ai/chat/index.vue b/src/views/ai/chat/index.vue index bc846a3d..0910497a 100644 --- a/src/views/ai/chat/index.vue +++ b/src/views/ai/chat/index.vue @@ -1,125 +1,676 @@ + - diff --git a/src/views/ai/chat/manager/ChatConversationList.vue b/src/views/ai/chat/manager/ChatConversationList.vue new file mode 100644 index 00000000..23933f01 --- /dev/null +++ b/src/views/ai/chat/manager/ChatConversationList.vue @@ -0,0 +1,163 @@ + + + diff --git a/src/views/ai/chat/manager/ChatMessageList.vue b/src/views/ai/chat/manager/ChatMessageList.vue new file mode 100644 index 00000000..0d841840 --- /dev/null +++ b/src/views/ai/chat/manager/ChatMessageList.vue @@ -0,0 +1,175 @@ + + + diff --git a/src/views/ai/chat/manager/index.vue b/src/views/ai/chat/manager/index.vue new file mode 100644 index 00000000..30dc2dfd --- /dev/null +++ b/src/views/ai/chat/manager/index.vue @@ -0,0 +1,20 @@ + + + diff --git a/src/views/ai/chat/role/RoleCategoryList.vue b/src/views/ai/chat/role/RoleCategoryList.vue new file mode 100644 index 00000000..a2587998 --- /dev/null +++ b/src/views/ai/chat/role/RoleCategoryList.vue @@ -0,0 +1,47 @@ + + + diff --git a/src/views/ai/chat/role/RoleList.vue b/src/views/ai/chat/role/RoleList.vue new file mode 100644 index 00000000..25206cff --- /dev/null +++ b/src/views/ai/chat/role/RoleList.vue @@ -0,0 +1,188 @@ + + + + + + diff --git a/src/views/ai/chat/role/index.vue b/src/views/ai/chat/role/index.vue new file mode 100644 index 00000000..f6096fc6 --- /dev/null +++ b/src/views/ai/chat/role/index.vue @@ -0,0 +1,296 @@ + + + + + + + diff --git a/src/views/ai/image/ImageDetailDrawer.vue b/src/views/ai/image/ImageDetailDrawer.vue new file mode 100644 index 00000000..e26a719a --- /dev/null +++ b/src/views/ai/image/ImageDetailDrawer.vue @@ -0,0 +1,145 @@ + + + + diff --git a/src/views/ai/image/ImageTask.vue b/src/views/ai/image/ImageTask.vue new file mode 100644 index 00000000..d458db00 --- /dev/null +++ b/src/views/ai/image/ImageTask.vue @@ -0,0 +1,210 @@ + + + + + + diff --git a/src/views/ai/image/ImageTaskCard.vue b/src/views/ai/image/ImageTaskCard.vue new file mode 100644 index 00000000..c73c3c1a --- /dev/null +++ b/src/views/ai/image/ImageTaskCard.vue @@ -0,0 +1,135 @@ + + + + diff --git a/src/views/ai/image/dall3/index.vue b/src/views/ai/image/dall3/index.vue new file mode 100644 index 00000000..92071b7b --- /dev/null +++ b/src/views/ai/image/dall3/index.vue @@ -0,0 +1,398 @@ + + + + diff --git a/src/views/ai/image/index.vue b/src/views/ai/image/index.vue new file mode 100644 index 00000000..50d4b4a9 --- /dev/null +++ b/src/views/ai/image/index.vue @@ -0,0 +1,105 @@ + + + + + + diff --git a/src/views/ai/image/midjourney/index.vue b/src/views/ai/image/midjourney/index.vue new file mode 100644 index 00000000..b3cc1cb7 --- /dev/null +++ b/src/views/ai/image/midjourney/index.vue @@ -0,0 +1,388 @@ + + + + diff --git a/src/views/ai/image/stable-diffusion/index.vue b/src/views/ai/image/stable-diffusion/index.vue new file mode 100644 index 00000000..7b1db7ab --- /dev/null +++ b/src/views/ai/image/stable-diffusion/index.vue @@ -0,0 +1,297 @@ + + + + diff --git a/src/views/ai/model/apiKey/ApiKeyForm.vue b/src/views/ai/model/apiKey/ApiKeyForm.vue new file mode 100644 index 00000000..a8fc0128 --- /dev/null +++ b/src/views/ai/model/apiKey/ApiKeyForm.vue @@ -0,0 +1,132 @@ + + diff --git a/src/views/ai/model/apiKey/index.vue b/src/views/ai/model/apiKey/index.vue new file mode 100644 index 00000000..6daf6a7d --- /dev/null +++ b/src/views/ai/model/apiKey/index.vue @@ -0,0 +1,180 @@ + + + diff --git a/src/views/ai/model/chatModel/ChatModelForm.vue b/src/views/ai/model/chatModel/ChatModelForm.vue new file mode 100644 index 00000000..e3f785c0 --- /dev/null +++ b/src/views/ai/model/chatModel/ChatModelForm.vue @@ -0,0 +1,181 @@ + + diff --git a/src/views/ai/model/chatModel/index.vue b/src/views/ai/model/chatModel/index.vue new file mode 100644 index 00000000..c5506746 --- /dev/null +++ b/src/views/ai/model/chatModel/index.vue @@ -0,0 +1,185 @@ + + + diff --git a/src/views/ai/model/chatRole/ChatRoleForm.vue b/src/views/ai/model/chatRole/ChatRoleForm.vue new file mode 100644 index 00000000..c33d289d --- /dev/null +++ b/src/views/ai/model/chatRole/ChatRoleForm.vue @@ -0,0 +1,190 @@ + + diff --git a/src/views/ai/model/chatRole/index.vue b/src/views/ai/model/chatRole/index.vue new file mode 100644 index 00000000..e870a556 --- /dev/null +++ b/src/views/ai/model/chatRole/index.vue @@ -0,0 +1,187 @@ + + + diff --git a/src/views/crm/statistics/performance/components/ContractCountPerformance.vue b/src/views/crm/statistics/performance/components/ContractCountPerformance.vue index f911bb2d..fa5a897b 100644 --- a/src/views/crm/statistics/performance/components/ContractCountPerformance.vue +++ b/src/views/crm/statistics/performance/components/ContractCountPerformance.vue @@ -1,5 +1,4 @@ -