commit
8f6fff6e83
Binary file not shown.
After Width: | Height: | Size: 108 KiB |
Binary file not shown.
After Width: | Height: | Size: 87 KiB |
Binary file not shown.
After Width: | Height: | Size: 86 KiB |
Binary file not shown.
After Width: | Height: | Size: 121 KiB |
|
@ -12,6 +12,7 @@ import {
|
|||
} from '@vben/plugins/vxe-table';
|
||||
import {
|
||||
erpNumberFormatter,
|
||||
formatPast2,
|
||||
formatToFractionDigit,
|
||||
isFunction,
|
||||
isString,
|
||||
|
@ -296,32 +297,7 @@ setupVbenVxeTable({
|
|||
|
||||
vxeUI.formats.add('formatPast2', {
|
||||
tableCellFormatMethod({ cellValue }) {
|
||||
if (cellValue === null || cellValue === undefined) {
|
||||
return '';
|
||||
}
|
||||
// 定义时间单位常量,便于维护
|
||||
const SECOND = 1000;
|
||||
const MINUTE = 60 * SECOND;
|
||||
const HOUR = 60 * MINUTE;
|
||||
const DAY = 24 * HOUR;
|
||||
|
||||
// 计算各时间单位
|
||||
const day = Math.floor(cellValue / DAY);
|
||||
const hour = Math.floor((cellValue % DAY) / HOUR);
|
||||
const minute = Math.floor((cellValue % HOUR) / MINUTE);
|
||||
const second = Math.floor((cellValue % MINUTE) / SECOND);
|
||||
|
||||
// 根据时间长短返回不同格式
|
||||
if (day > 0) {
|
||||
return `${day} 天${hour} 小时 ${minute} 分钟`;
|
||||
}
|
||||
if (hour > 0) {
|
||||
return `${hour} 小时 ${minute} 分钟`;
|
||||
}
|
||||
if (minute > 0) {
|
||||
return `${minute} 分钟`;
|
||||
}
|
||||
return second > 0 ? `${second} 秒` : `${0} 秒`;
|
||||
return formatPast2(cellValue);
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
import type { PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace AiChatConversationApi {
|
||||
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 数量
|
||||
}
|
||||
}
|
||||
|
||||
// 获得【我的】聊天对话
|
||||
export function getChatConversationMy(id: number) {
|
||||
return requestClient.get<AiChatConversationApi.ChatConversationVO>(
|
||||
`/ai/chat/conversation/get-my?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
// 新增【我的】聊天对话
|
||||
export function createChatConversationMy(
|
||||
data: AiChatConversationApi.ChatConversationVO,
|
||||
) {
|
||||
return requestClient.post('/ai/chat/conversation/create-my', data);
|
||||
}
|
||||
|
||||
// 更新【我的】聊天对话
|
||||
export function updateChatConversationMy(
|
||||
data: AiChatConversationApi.ChatConversationVO,
|
||||
) {
|
||||
return requestClient.put(`/ai/chat/conversation/update-my`, data);
|
||||
}
|
||||
|
||||
// 删除【我的】聊天对话
|
||||
export function deleteChatConversationMy(id: number) {
|
||||
return requestClient.delete(`/ai/chat/conversation/delete-my?id=${id}`);
|
||||
}
|
||||
|
||||
// 删除【我的】所有对话,置顶除外
|
||||
export function deleteChatConversationMyByUnpinned() {
|
||||
return requestClient.delete(`/ai/chat/conversation/delete-by-unpinned`);
|
||||
}
|
||||
|
||||
// 获得【我的】聊天对话列表
|
||||
export function getChatConversationMyList() {
|
||||
return requestClient.get<AiChatConversationApi.ChatConversationVO[]>(
|
||||
`/ai/chat/conversation/my-list`,
|
||||
);
|
||||
}
|
||||
|
||||
// 获得【我的】聊天对话列表
|
||||
export function getChatConversationPage(params: any) {
|
||||
return requestClient.get<
|
||||
PageResult<AiChatConversationApi.ChatConversationVO[]>
|
||||
>(`/ai/chat/conversation/page`, { params });
|
||||
}
|
||||
|
||||
// 管理员删除消息
|
||||
export function deleteChatConversationByAdmin(id: number) {
|
||||
return requestClient.delete(`/ai/chat/conversation/delete-by-admin?id=${id}`);
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
import type { PageResult } from '@vben/request';
|
||||
|
||||
import { useAppConfig } from '@vben/hooks';
|
||||
import { fetchEventSource } from '@vben/request';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||
const accessStore = useAccessStore();
|
||||
export namespace AiChatMessageApi {
|
||||
export interface ChatMessageVO {
|
||||
id: number; // 编号
|
||||
conversationId: number; // 对话编号
|
||||
type: string; // 消息类型
|
||||
userId: string; // 用户编号
|
||||
roleId: string; // 角色编号
|
||||
model: number; // 模型标志
|
||||
modelId: number; // 模型编号
|
||||
content: string; // 聊天内容
|
||||
tokens: number; // 消耗 Token 数量
|
||||
segmentIds?: number[]; // 段落编号
|
||||
segments?: {
|
||||
content: string; // 段落内容
|
||||
documentId: number; // 文档编号
|
||||
documentName: string; // 文档名称
|
||||
id: number; // 段落编号
|
||||
}[];
|
||||
createTime: Date; // 创建时间
|
||||
roleAvatar: string; // 角色头像
|
||||
userAvatar: string; // 用户头像
|
||||
}
|
||||
}
|
||||
|
||||
// 消息列表
|
||||
export function getChatMessageListByConversationId(
|
||||
conversationId: null | number,
|
||||
) {
|
||||
return requestClient.get<AiChatMessageApi.ChatMessageVO[]>(
|
||||
`/ai/chat/message/list-by-conversation-id?conversationId=${conversationId}`,
|
||||
);
|
||||
}
|
||||
|
||||
// 发送 Stream 消息
|
||||
export function sendChatMessageStream(
|
||||
conversationId: number,
|
||||
content: string,
|
||||
ctrl: any,
|
||||
enableContext: boolean,
|
||||
onMessage: any,
|
||||
onError: any,
|
||||
onClose: any,
|
||||
) {
|
||||
const token = accessStore.accessToken;
|
||||
return fetchEventSource(`${apiURL}/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,
|
||||
});
|
||||
}
|
||||
|
||||
// 删除消息
|
||||
export function deleteChatMessage(id: number) {
|
||||
return requestClient.delete(`/ai/chat/message/delete?id=${id}`);
|
||||
}
|
||||
|
||||
// 删除指定对话的消息
|
||||
export function deleteByConversationId(conversationId: number) {
|
||||
return requestClient.delete(
|
||||
`/ai/chat/message/delete-by-conversation-id?conversationId=${conversationId}`,
|
||||
);
|
||||
}
|
||||
// 获得消息分页
|
||||
export function getChatMessagePage(params: any) {
|
||||
return requestClient.get<PageResult<AiChatMessageApi.ChatMessageVO>>(
|
||||
'/ai/chat/message/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
// 管理员删除消息
|
||||
export function deleteChatMessageByAdmin(id: number) {
|
||||
return requestClient.delete(`/ai/chat/message/delete-by-admin?id=${id}`);
|
||||
}
|
|
@ -0,0 +1,112 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace AiImageApi {
|
||||
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 绘图 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 {
|
||||
prompt: string; // 提示词
|
||||
modelId: number; // 模型
|
||||
style: string; // 图像生成的风格
|
||||
width: string; // 图片宽度
|
||||
height: string; // 图片高度
|
||||
options: object; // 绘制参数,Map<String, String>
|
||||
}
|
||||
|
||||
export interface ImageMidjourneyImagineReqVO {
|
||||
prompt: string; // 提示词
|
||||
modelId: number; // 模型
|
||||
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 function getImagePageMy(params: PageParam) {
|
||||
return requestClient.get<PageResult<AiImageApi.ImageVO>>(
|
||||
'/ai/image/my-page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
// 获取【我的】绘图记录
|
||||
export function getImageMy(id: number) {
|
||||
return requestClient.get<AiImageApi.ImageVO>(`/ai/image/get-my?id=${id}`);
|
||||
}
|
||||
|
||||
// 获取【我的】绘图记录列表
|
||||
export function getImageListMyByIds(ids: number[]) {
|
||||
return requestClient.get<AiImageApi.ImageVO[]>(`/ai/image/my-list-by-ids`, {
|
||||
params: { ids: ids.join(',') },
|
||||
});
|
||||
}
|
||||
|
||||
// 生成图片
|
||||
export function drawImage(data: AiImageApi.ImageDrawReqVO) {
|
||||
return requestClient.post(`/ai/image/draw`, data);
|
||||
}
|
||||
|
||||
// 删除【我的】绘画记录
|
||||
export function deleteImageMy(id: number) {
|
||||
return requestClient.delete(`/ai/image/delete-my?id=${id}`);
|
||||
}
|
||||
|
||||
// ================ midjourney 专属 ================
|
||||
// 【Midjourney】生成图片
|
||||
export function midjourneyImagine(
|
||||
data: AiImageApi.ImageMidjourneyImagineReqVO,
|
||||
) {
|
||||
return requestClient.post(`/ai/image/midjourney/imagine`, data);
|
||||
}
|
||||
|
||||
// 【Midjourney】Action 操作(二次生成图片)
|
||||
export function midjourneyAction(data: AiImageApi.ImageMidjourneyActionVO) {
|
||||
return requestClient.post(`/ai/image/midjourney/action`, data);
|
||||
}
|
||||
|
||||
// ================ 绘图管理 ================
|
||||
// 查询绘画分页
|
||||
export function getImagePage(params: any) {
|
||||
return requestClient.get<AiImageApi.ImageVO[]>(`/ai/image/page`, { params });
|
||||
}
|
||||
|
||||
// 更新绘画发布状态
|
||||
export function updateImage(data: any) {
|
||||
return requestClient.put(`/ai/image/update`, data);
|
||||
}
|
||||
|
||||
// 删除绘画
|
||||
export function deleteImage(id: number) {
|
||||
return requestClient.delete(`/ai/image/delete?id=${id}`);
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace AiKnowledgeDocumentApi {
|
||||
export interface KnowledgeDocumentVO {
|
||||
id: number; // 编号
|
||||
knowledgeId: number; // 知识库编号
|
||||
name: string; // 文档名称
|
||||
contentLength: number; // 字符数
|
||||
tokens: number; // token 数
|
||||
segmentMaxTokens: number; // 分片最大 token 数
|
||||
retrievalCount: number; // 召回次数
|
||||
status: number; // 是否启用
|
||||
}
|
||||
}
|
||||
|
||||
// 查询知识库文档分页
|
||||
export function getKnowledgeDocumentPage(params: PageParam) {
|
||||
return requestClient.get<
|
||||
PageResult<AiKnowledgeDocumentApi.KnowledgeDocumentVO>
|
||||
>('/ai/knowledge/document/page', { params });
|
||||
}
|
||||
|
||||
// 查询知识库文档详情
|
||||
export function getKnowledgeDocument(id: number) {
|
||||
return requestClient.get(`/ai/knowledge/document/get?id=${id}`);
|
||||
}
|
||||
// 新增知识库文档(单个)
|
||||
export function createKnowledge(data: any) {
|
||||
return requestClient.post('/ai/knowledge/document/create', data);
|
||||
}
|
||||
// 新增知识库文档(多个)
|
||||
export function createKnowledgeDocumentList(data: any) {
|
||||
return requestClient.post('/ai/knowledge/document/create-list', data);
|
||||
}
|
||||
|
||||
// 修改知识库文档
|
||||
export function updateKnowledgeDocument(data: any) {
|
||||
return requestClient.put('/ai/knowledge/document/update', data);
|
||||
}
|
||||
|
||||
// 修改知识库文档状态
|
||||
export function updateKnowledgeDocumentStatus(data: any) {
|
||||
return requestClient.put('/ai/knowledge/document/update-status', data);
|
||||
}
|
||||
// 删除知识库文档
|
||||
export function deleteKnowledgeDocument(id: number) {
|
||||
return requestClient.delete(`/ai/knowledge/document/delete?id=${id}`);
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace AiKnowledgeKnowledgeApi {
|
||||
export interface KnowledgeVO {
|
||||
id: number; // 编号
|
||||
name: string; // 知识库名称
|
||||
description: string; // 知识库描述
|
||||
embeddingModelId: number; // 嵌入模型编号,高质量模式时维护
|
||||
topK: number; // topK
|
||||
similarityThreshold: number; // 相似度阈值
|
||||
}
|
||||
}
|
||||
|
||||
// 查询知识库分页
|
||||
export function getKnowledgePage(params: PageParam) {
|
||||
return requestClient.get<PageResult<AiKnowledgeKnowledgeApi.KnowledgeVO>>(
|
||||
'/ai/knowledge/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
// 查询知识库详情
|
||||
export function getKnowledge(id: number) {
|
||||
return requestClient.get<AiKnowledgeKnowledgeApi.KnowledgeVO>(
|
||||
`/ai/knowledge/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
// 新增知识库
|
||||
export function createKnowledge(data: AiKnowledgeKnowledgeApi.KnowledgeVO) {
|
||||
return requestClient.post('/ai/knowledge/create', data);
|
||||
}
|
||||
|
||||
// 修改知识库
|
||||
export function updateKnowledge(data: AiKnowledgeKnowledgeApi.KnowledgeVO) {
|
||||
return requestClient.put('/ai/knowledge/update', data);
|
||||
}
|
||||
|
||||
// 删除知识库
|
||||
export function deleteKnowledge(id: number) {
|
||||
return requestClient.delete(`/ai/knowledge/delete?id=${id}`);
|
||||
}
|
||||
|
||||
// 获取知识库简单列表
|
||||
export function getSimpleKnowledgeList() {
|
||||
return requestClient.get<AiKnowledgeKnowledgeApi.KnowledgeVO[]>(
|
||||
'/ai/knowledge/simple-list',
|
||||
);
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace AiKnowledgeSegmentApi {
|
||||
// AI 知识库分段 VO
|
||||
export interface KnowledgeSegmentVO {
|
||||
id: number; // 编号
|
||||
documentId: number; // 文档编号
|
||||
knowledgeId: number; // 知识库编号
|
||||
vectorId: string; // 向量库编号
|
||||
content: string; // 切片内容
|
||||
contentLength: number; // 切片内容长度
|
||||
tokens: number; // token 数量
|
||||
retrievalCount: number; // 召回次数
|
||||
status: number; // 文档状态
|
||||
createTime: number; // 创建时间
|
||||
}
|
||||
}
|
||||
|
||||
// 查询知识库分段分页
|
||||
export function getKnowledgeSegmentPage(params: PageParam) {
|
||||
return requestClient.get<
|
||||
PageResult<AiKnowledgeSegmentApi.KnowledgeSegmentVO>
|
||||
>('/ai/knowledge/segment/page', { params });
|
||||
}
|
||||
|
||||
// 查询知识库分段详情
|
||||
export function getKnowledgeSegment(id: number) {
|
||||
return requestClient.get<AiKnowledgeSegmentApi.KnowledgeSegmentVO>(
|
||||
`/ai/knowledge/segment/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
// 新增知识库分段
|
||||
export function createKnowledgeSegment(
|
||||
data: AiKnowledgeSegmentApi.KnowledgeSegmentVO,
|
||||
) {
|
||||
return requestClient.post('/ai/knowledge/segment/create', data);
|
||||
}
|
||||
|
||||
// 修改知识库分段
|
||||
export function updateKnowledgeSegment(
|
||||
data: AiKnowledgeSegmentApi.KnowledgeSegmentVO,
|
||||
) {
|
||||
return requestClient.put('/ai/knowledge/segment/update', data);
|
||||
}
|
||||
|
||||
// 修改知识库分段状态
|
||||
export function updateKnowledgeSegmentStatus(data: any) {
|
||||
return requestClient.put('/ai/knowledge/segment/update-status', data);
|
||||
}
|
||||
// 删除知识库分段
|
||||
export function deleteKnowledgeSegment(id: number) {
|
||||
return requestClient.delete(`/ai/knowledge/segment/delete?id=${id}`);
|
||||
}
|
||||
|
||||
// 切片内容
|
||||
export function splitContent(url: string, segmentMaxTokens: number) {
|
||||
return requestClient.get('/ai/knowledge/segment/split', {
|
||||
params: { url, segmentMaxTokens },
|
||||
});
|
||||
}
|
||||
|
||||
// 获取文档处理列表
|
||||
export function getKnowledgeSegmentProcessList(documentIds: number[]) {
|
||||
return requestClient.get('/ai/knowledge/segment/get-process-list', {
|
||||
params: { documentIds: documentIds.join(',') },
|
||||
});
|
||||
}
|
||||
|
||||
// 搜索知识库分段
|
||||
export function searchKnowledgeSegment(params: any) {
|
||||
return requestClient.get('/ai/knowledge/segment/search', {
|
||||
params,
|
||||
});
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
import { useAppConfig } from '@vben/hooks';
|
||||
import { fetchEventSource } from '@vben/request';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||
const accessStore = useAccessStore();
|
||||
export namespace AiMindmapApi {
|
||||
// AI 思维导图 VO
|
||||
export interface MindMapVO {
|
||||
id: number; // 编号
|
||||
userId: number; // 用户编号
|
||||
prompt: string; // 生成内容提示
|
||||
generatedContent: string; // 生成的思维导图内容
|
||||
platform: string; // 平台
|
||||
model: string; // 模型
|
||||
errorMessage: string; // 错误信息
|
||||
}
|
||||
|
||||
// AI 思维导图生成 VO
|
||||
export interface AiMindMapGenerateReqVO {
|
||||
prompt: string;
|
||||
}
|
||||
}
|
||||
|
||||
export function generateMindMap({
|
||||
data,
|
||||
onClose,
|
||||
onMessage,
|
||||
onError,
|
||||
ctrl,
|
||||
}: {
|
||||
ctrl: AbortController;
|
||||
data: AiMindmapApi.AiMindMapGenerateReqVO;
|
||||
onClose?: (...args: any[]) => void;
|
||||
onError?: (...args: any[]) => void;
|
||||
onMessage?: (res: any) => void;
|
||||
}) {
|
||||
const token = accessStore.accessToken;
|
||||
return fetchEventSource(`${apiURL}/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,
|
||||
});
|
||||
}
|
||||
|
||||
// 查询思维导图分页
|
||||
export function getMindMapPage(params: any) {
|
||||
return requestClient.get(`/ai/mind-map/page`, { params });
|
||||
}
|
||||
|
||||
// 删除思维导图
|
||||
export function deleteMindMap(id: number) {
|
||||
return requestClient.delete(`/ai/mind-map/delete?id=${id}`);
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace AiModelApiKeyApi {
|
||||
export interface ApiKeyVO {
|
||||
id: number; // 编号
|
||||
name: string; // 名称
|
||||
apiKey: string; // 密钥
|
||||
platform: string; // 平台
|
||||
url: string; // 自定义 API 地址
|
||||
status: number; // 状态
|
||||
}
|
||||
}
|
||||
|
||||
// 查询 API 密钥分页
|
||||
export function getApiKeyPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<AiModelApiKeyApi.ApiKeyVO>>(
|
||||
'/ai/api-key/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
// 获得 API 密钥列表
|
||||
export function getApiKeySimpleList() {
|
||||
return requestClient.get<AiModelApiKeyApi.ApiKeyVO[]>(
|
||||
'/ai/api-key/simple-list',
|
||||
);
|
||||
}
|
||||
|
||||
// 查询 API 密钥详情
|
||||
export function getApiKey(id: number) {
|
||||
return requestClient.get<AiModelApiKeyApi.ApiKeyVO>(
|
||||
`/ai/api-key/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
// 新增 API 密钥
|
||||
export function createApiKey(data: AiModelApiKeyApi.ApiKeyVO) {
|
||||
return requestClient.post('/ai/api-key/create', data);
|
||||
}
|
||||
|
||||
// 修改 API 密钥
|
||||
export function updateApiKey(data: AiModelApiKeyApi.ApiKeyVO) {
|
||||
return requestClient.put('/ai/api-key/update', data);
|
||||
}
|
||||
|
||||
// 删除 API 密钥
|
||||
export function deleteApiKey(id: number) {
|
||||
return requestClient.delete(`/ai/api-key/delete?id=${id}`);
|
||||
}
|
|
@ -0,0 +1,85 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace AiModelChatRoleApi {
|
||||
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; // 状态
|
||||
knowledgeIds?: number[]; // 引用的知识库 ID 列表
|
||||
toolIds?: number[]; // 引用的工具 ID 列表
|
||||
}
|
||||
|
||||
// AI 聊天角色 分页请求 vo
|
||||
export interface ChatRolePageReqVO {
|
||||
name?: string; // 角色名称
|
||||
category?: string; // 角色类别
|
||||
publicStatus: boolean; // 是否公开
|
||||
pageNo: number; // 是否公开
|
||||
pageSize: number; // 是否公开
|
||||
}
|
||||
}
|
||||
|
||||
// 查询聊天角色分页
|
||||
export function getChatRolePage(params: PageParam) {
|
||||
return requestClient.get<PageResult<AiModelChatRoleApi.ChatRoleVO>>(
|
||||
'/ai/chat-role/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
// 查询聊天角色详情
|
||||
export function getChatRole(id: number) {
|
||||
return requestClient.get<AiModelChatRoleApi.ChatRoleVO>(
|
||||
`/ai/chat-role/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
// 新增聊天角色
|
||||
export function createChatRole(data: AiModelChatRoleApi.ChatRoleVO) {
|
||||
return requestClient.post('/ai/chat-role/create', data);
|
||||
}
|
||||
|
||||
// 修改聊天角色
|
||||
export function updateChatRole(data: AiModelChatRoleApi.ChatRoleVO) {
|
||||
return requestClient.put('/ai/chat-role/update', data);
|
||||
}
|
||||
|
||||
// 删除聊天角色
|
||||
export function deleteChatRole(id: number) {
|
||||
return requestClient.delete(`/ai/chat-role/delete?id=${id}`);
|
||||
}
|
||||
|
||||
// ======= chat 聊天
|
||||
// 获取 my role
|
||||
export function getMyPage(params: AiModelChatRoleApi.ChatRolePageReqVO) {
|
||||
return requestClient.get('/ai/chat-role/my-page', { params });
|
||||
}
|
||||
|
||||
// 获取角色分类
|
||||
export function getCategoryList() {
|
||||
return requestClient.get('/ai/chat-role/category-list');
|
||||
}
|
||||
|
||||
// 创建角色
|
||||
export function createMy(data: AiModelChatRoleApi.ChatRoleVO) {
|
||||
return requestClient.post('/ai/chat-role/create-my', data);
|
||||
}
|
||||
|
||||
// 更新角色
|
||||
export function updateMy(data: AiModelChatRoleApi.ChatRoleVO) {
|
||||
return requestClient.put('/ai/chat-role/update', data);
|
||||
}
|
||||
|
||||
// 删除角色 my
|
||||
export function deleteMy(id: number) {
|
||||
return requestClient.delete(`/ai/chat-role/delete-my?id=${id}`);
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace AiModelModelApi {
|
||||
export interface ModelVO {
|
||||
id: number; // 编号
|
||||
keyId: number; // API 秘钥编号
|
||||
name: string; // 模型名字
|
||||
model: string; // 模型标识
|
||||
platform: string; // 模型平台
|
||||
type: number; // 模型类型
|
||||
sort: number; // 排序
|
||||
status: number; // 状态
|
||||
temperature?: number; // 温度参数
|
||||
maxTokens?: number; // 单条回复的最大 Token 数量
|
||||
maxContexts?: number; // 上下文的最大 Message 数量
|
||||
}
|
||||
}
|
||||
|
||||
// 查询模型分页
|
||||
export function getModelPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<AiModelModelApi.ModelVO>>(
|
||||
'/ai/model/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
// 获得模型列表
|
||||
export function getModelSimpleList(type?: number) {
|
||||
return requestClient.get<AiModelModelApi.ModelVO[]>('/ai/model/simple-list', {
|
||||
params: {
|
||||
type,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 查询模型详情
|
||||
export function getModel(id: number) {
|
||||
return requestClient.get<AiModelModelApi.ModelVO>(`/ai/model/get?id=${id}`);
|
||||
}
|
||||
// 新增模型
|
||||
export function createModel(data: AiModelModelApi.ModelVO) {
|
||||
return requestClient.post('/ai/model/create', data);
|
||||
}
|
||||
|
||||
// 修改模型
|
||||
export function updateModel(data: AiModelModelApi.ModelVO) {
|
||||
return requestClient.put('/ai/model/update', data);
|
||||
}
|
||||
|
||||
// 删除模型
|
||||
export function deleteModel(id: number) {
|
||||
return requestClient.delete(`/ai/model/delete?id=${id}`);
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace AiModelToolApi {
|
||||
export interface ToolVO {
|
||||
id: number; // 工具编号
|
||||
name: string; // 工具名称
|
||||
description: string; // 工具描述
|
||||
status: number; // 状态
|
||||
}
|
||||
}
|
||||
|
||||
// 查询工具分页
|
||||
export function getToolPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<AiModelToolApi.ToolVO>>('/ai/tool/page', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 查询工具详情
|
||||
export function getTool(id: number) {
|
||||
return requestClient.get<AiModelToolApi.ToolVO>(`/ai/tool/get?id=${id}`);
|
||||
}
|
||||
// 新增工具
|
||||
export function createTool(data: AiModelToolApi.ToolVO) {
|
||||
return requestClient.post('/ai/tool/create', data);
|
||||
}
|
||||
|
||||
// 修改工具
|
||||
export function updateTool(data: AiModelToolApi.ToolVO) {
|
||||
return requestClient.put('/ai/tool/update', data);
|
||||
}
|
||||
|
||||
// 删除工具
|
||||
export function deleteTool(id: number) {
|
||||
return requestClient.delete(`/ai/tool/delete?id=${id}`);
|
||||
}
|
||||
|
||||
// 获取工具简单列表
|
||||
export function getToolSimpleList() {
|
||||
return requestClient.get<AiModelToolApi.ToolVO[]>('/ai/tool/simple-list');
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace AiMusicApi {
|
||||
// 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; // 错误信息
|
||||
}
|
||||
}
|
||||
|
||||
// 查询音乐分页
|
||||
export function getMusicPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<AiMusicApi.MusicVO>>(`/ai/music/page`, {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
// 更新音乐
|
||||
export function updateMusic(data: any) {
|
||||
return requestClient.put('/ai/music/update', data);
|
||||
}
|
||||
|
||||
// 删除音乐
|
||||
export function deleteMusic(id: number) {
|
||||
return requestClient.delete(`/ai/music/delete?id=${id}`);
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export function getWorkflowPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<any>>('/ai/workflow/page', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
||||
export const getWorkflow = (id: number | string) => {
|
||||
return requestClient.get(`/ai/workflow/get?id=${id}`);
|
||||
};
|
||||
|
||||
export const createWorkflow = (data: any) => {
|
||||
return requestClient.post('/ai/workflow/create', data);
|
||||
};
|
||||
|
||||
export const updateWorkflow = (data: any) => {
|
||||
return requestClient.put('/ai/workflow/update', data);
|
||||
};
|
||||
|
||||
export const deleteWorkflow = (id: number | string) => {
|
||||
return requestClient.delete(`/ai/workflow/delete?id=${id}`);
|
||||
};
|
||||
|
||||
export const testWorkflow = (data: any) => {
|
||||
return requestClient.post('/ai/workflow/test', data);
|
||||
};
|
|
@ -0,0 +1,95 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import type { AiWriteTypeEnum } from '#/utils';
|
||||
|
||||
import { useAppConfig } from '@vben/hooks';
|
||||
import { fetchEventSource } from '@vben/request';
|
||||
import { useAccessStore } from '@vben/stores';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
const { apiURL } = useAppConfig(import.meta.env, import.meta.env.PROD);
|
||||
const accessStore = useAccessStore();
|
||||
export namespace AiWriteApi {
|
||||
export interface WriteVO {
|
||||
type: AiWriteTypeEnum.REPLY | AiWriteTypeEnum.WRITING; // 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 function writeStream({
|
||||
data,
|
||||
onClose,
|
||||
onMessage,
|
||||
onError,
|
||||
ctrl,
|
||||
}: {
|
||||
ctrl: AbortController;
|
||||
data: Partial<AiWriteApi.WriteVO>;
|
||||
onClose?: (...args: any[]) => void;
|
||||
onError?: (...args: any[]) => void;
|
||||
onMessage?: (res: any) => void;
|
||||
}) {
|
||||
const token = accessStore.accessToken;
|
||||
return fetchEventSource(`${apiURL}/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,
|
||||
});
|
||||
}
|
||||
|
||||
// 获取写作列表
|
||||
export function getWritePage(params: any) {
|
||||
return requestClient.get<PageResult<AiWriteApi.AiWritePageReqVO>>(
|
||||
`/ai/write/page`,
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
// 删除音乐
|
||||
export function deleteWrite(id: number) {
|
||||
return requestClient.delete(`/ai/write/delete`, { params: { id } });
|
||||
}
|
|
@ -0,0 +1,57 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace ProductUnitApi {
|
||||
/** 产品单位信息 */
|
||||
export interface ProductUnit {
|
||||
id: number; // 编号
|
||||
groupId?: number; // 分组编号
|
||||
name?: string; // 单位名称
|
||||
basic?: number; // 基础单位
|
||||
number?: number; // 单位数量/相对于基础单位
|
||||
usageType: number; // 用途
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询产品单位分页 */
|
||||
export function getProductUnitPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<ProductUnitApi.ProductUnit>>(
|
||||
'/basic/product-unit/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询产品单位详情 */
|
||||
export function getProductUnit(id: number) {
|
||||
return requestClient.get<ProductUnitApi.ProductUnit>(
|
||||
`/basic/product-unit/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 新增产品单位 */
|
||||
export function createProductUnit(data: ProductUnitApi.ProductUnit) {
|
||||
return requestClient.post('/basic/product-unit/create', data);
|
||||
}
|
||||
|
||||
/** 修改产品单位 */
|
||||
export function updateProductUnit(data: ProductUnitApi.ProductUnit) {
|
||||
return requestClient.put('/basic/product-unit/update', data);
|
||||
}
|
||||
|
||||
/** 删除产品单位 */
|
||||
export function deleteProductUnit(id: number) {
|
||||
return requestClient.delete(`/basic/product-unit/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除产品单位 */
|
||||
export function deleteProductUnitListByIds(ids: number[]) {
|
||||
return requestClient.delete(
|
||||
`/basic/product-unit/delete-list?ids=${ids.join(',')}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 导出产品单位 */
|
||||
export function exportProductUnit(params: any) {
|
||||
return requestClient.download('/basic/product-unit/export-excel', params);
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
import type { PageParam, PageResult } from '@vben/request';
|
||||
|
||||
import { requestClient } from '#/api/request';
|
||||
|
||||
export namespace ProductUnitGroupApi {
|
||||
/** 产品单位组信息 */
|
||||
export interface ProductUnitGroup {
|
||||
id: number; // 编号
|
||||
name?: string; // 产品单位组名称
|
||||
status?: number; // 开启状态
|
||||
}
|
||||
}
|
||||
|
||||
/** 查询产品单位组分页 */
|
||||
export function getProductUnitGroupPage(params: PageParam) {
|
||||
return requestClient.get<PageResult<ProductUnitGroupApi.ProductUnitGroup>>(
|
||||
'/basic/product-unit-group/page',
|
||||
{ params },
|
||||
);
|
||||
}
|
||||
|
||||
/** 查询产品单位组详情 */
|
||||
export function getProductUnitGroup(id: number) {
|
||||
return requestClient.get<ProductUnitGroupApi.ProductUnitGroup>(
|
||||
`/basic/product-unit-group/get?id=${id}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 新增产品单位组 */
|
||||
export function createProductUnitGroup(
|
||||
data: ProductUnitGroupApi.ProductUnitGroup,
|
||||
) {
|
||||
return requestClient.post('/basic/product-unit-group/create', data);
|
||||
}
|
||||
|
||||
/** 修改产品单位组 */
|
||||
export function updateProductUnitGroup(
|
||||
data: ProductUnitGroupApi.ProductUnitGroup,
|
||||
) {
|
||||
return requestClient.put('/basic/product-unit-group/update', data);
|
||||
}
|
||||
|
||||
/** 删除产品单位组 */
|
||||
export function deleteProductUnitGroup(id: number) {
|
||||
return requestClient.delete(`/basic/product-unit-group/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除产品单位组 */
|
||||
export function deleteProductUnitGroupListByIds(ids: number[]) {
|
||||
return requestClient.delete(
|
||||
`/basic/product-unit-group/delete-list?ids=${ids.join(',')}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 导出产品单位组 */
|
||||
export function exportProductUnitGroup(params: any) {
|
||||
return requestClient.download(
|
||||
'/basic/product-unit-group/export-excel',
|
||||
params,
|
||||
);
|
||||
}
|
|
@ -146,3 +146,10 @@ export function deleteCodegenTable(tableId: number) {
|
|||
params: { tableId },
|
||||
});
|
||||
}
|
||||
|
||||
/** 批量删除代码生成表定义 */
|
||||
export function deleteCodegenTableList(tableIds: number[]) {
|
||||
return requestClient.delete(
|
||||
`/infra/codegen/delete-list?tableIds=${tableIds.join(',')}`,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -54,9 +54,14 @@ export function deleteConfig(id: number) {
|
|||
return requestClient.delete(`/infra/config/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除参数 */
|
||||
export function deleteConfigList(ids: number[]) {
|
||||
return requestClient.delete(`/infra/config/delete-list?ids=${ids.join(',')}`);
|
||||
}
|
||||
|
||||
/** 导出参数 */
|
||||
export function exportConfig(params: any) {
|
||||
return requestClient.download('/infra/config/export', {
|
||||
return requestClient.download('/infra/config/export-excel', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -69,6 +69,13 @@ export function deleteFileConfig(id: number) {
|
|||
return requestClient.delete(`/infra/file-config/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除文件配置 */
|
||||
export function deleteFileConfigList(ids: number[]) {
|
||||
return requestClient.delete(
|
||||
`/infra/file-config/delete-list?ids=${ids.join(',')}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 测试文件配置 */
|
||||
export function testFileConfig(id: number) {
|
||||
return requestClient.get(`/infra/file-config/test?id=${id}`);
|
||||
|
|
|
@ -45,6 +45,11 @@ export function deleteFile(id: number) {
|
|||
return requestClient.delete(`/infra/file/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除文件 */
|
||||
export function deleteFileList(ids: number[]) {
|
||||
return requestClient.delete(`/infra/file/delete-list?ids=${ids.join(',')}`);
|
||||
}
|
||||
|
||||
/** 获取文件预签名地址 */
|
||||
export function getFilePresignedUrl(name: string, directory?: string) {
|
||||
return requestClient.get<InfraFileApi.FilePresignedUrlResp>(
|
||||
|
|
|
@ -46,6 +46,11 @@ export function deleteJob(id: number) {
|
|||
return requestClient.delete(`/infra/job/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除定时任务调度 */
|
||||
export function deleteJobList(ids: number[]) {
|
||||
return requestClient.delete(`/infra/job/delete-list?ids=${ids.join(',')}`);
|
||||
}
|
||||
|
||||
/** 导出定时任务调度 */
|
||||
export function exportJob(params: any) {
|
||||
return requestClient.download('/infra/job/export-excel', { params });
|
||||
|
|
|
@ -168,7 +168,7 @@ export function deleteSpu(id: number) {
|
|||
|
||||
/** 导出商品 SPU Excel */
|
||||
export function exportSpu(params: PageParam) {
|
||||
return requestClient.download('/product/spu/export', { params });
|
||||
return requestClient.download('/product/spu/export-excel', { params });
|
||||
}
|
||||
|
||||
/** 获得商品 SPU 精简列表 */
|
||||
|
|
|
@ -45,3 +45,8 @@ export async function updateDept(data: SystemDeptApi.Dept) {
|
|||
export async function deleteDept(id: number) {
|
||||
return requestClient.delete(`/system/dept/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除部门 */
|
||||
export async function deleteDeptList(ids: number[]) {
|
||||
return requestClient.delete(`/system/dept/delete-list?ids=${ids.join(',')}`);
|
||||
}
|
||||
|
|
|
@ -48,7 +48,14 @@ export function deleteDictData(id: number) {
|
|||
return requestClient.delete(`/system/dict-data/delete?id=${id}`);
|
||||
}
|
||||
|
||||
// 批量删除字典数据
|
||||
export function deleteDictDataList(ids: number[]) {
|
||||
return requestClient.delete(
|
||||
`/system/dict-data/delete-list?ids=${ids.join(',')}`,
|
||||
);
|
||||
}
|
||||
|
||||
// 导出字典类型数据
|
||||
export function exportDictData(params: any) {
|
||||
return requestClient.download('/system/dict-data/export', { params });
|
||||
return requestClient.download('/system/dict-data/export-excel', { params });
|
||||
}
|
||||
|
|
|
@ -42,7 +42,14 @@ export function deleteDictType(id: number) {
|
|||
return requestClient.delete(`/system/dict-type/delete?id=${id}`);
|
||||
}
|
||||
|
||||
// 批量删除字典
|
||||
export function deleteDictTypeList(ids: number[]) {
|
||||
return requestClient.delete(
|
||||
`/system/dict-type/delete-list?ids=${ids.join(',')}`,
|
||||
);
|
||||
}
|
||||
|
||||
// 导出字典类型
|
||||
export function exportDictType(params: any) {
|
||||
return requestClient.download('/system/dict-type/export', { params });
|
||||
return requestClient.download('/system/dict-type/export-excel', { params });
|
||||
}
|
||||
|
|
|
@ -49,6 +49,13 @@ export function deleteMailAccount(id: number) {
|
|||
return requestClient.delete(`/system/mail-account/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除邮箱账号 */
|
||||
export function deleteMailAccountList(ids: number[]) {
|
||||
return requestClient.delete(
|
||||
`/system/mail-account/delete-list?ids=${ids.join(',')}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 获得邮箱账号精简列表 */
|
||||
export function getSimpleMailAccountList() {
|
||||
return requestClient.get<SystemMailAccountApi.MailAccount[]>(
|
||||
|
|
|
@ -56,6 +56,13 @@ export function deleteMailTemplate(id: number) {
|
|||
return requestClient.delete(`/system/mail-template/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除邮件模板 */
|
||||
export function deleteMailTemplateList(ids: number[]) {
|
||||
return requestClient.delete(
|
||||
`/system/mail-template/delete-list?ids=${ids.join(',')}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 发送邮件 */
|
||||
export function sendMail(data: SystemMailTemplateApi.MailSendReq) {
|
||||
return requestClient.post('/system/mail-template/send-mail', data);
|
||||
|
|
|
@ -52,3 +52,8 @@ export async function updateMenu(data: SystemMenuApi.Menu) {
|
|||
export async function deleteMenu(id: number) {
|
||||
return requestClient.delete(`/system/menu/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除菜单 */
|
||||
export async function deleteMenuList(ids: number[]) {
|
||||
return requestClient.delete(`/system/menu/delete-list?ids=${ids.join(',')}`);
|
||||
}
|
||||
|
|
|
@ -46,6 +46,13 @@ export function deleteNotice(id: number) {
|
|||
return requestClient.delete(`/system/notice/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除公告 */
|
||||
export function deleteNoticeList(ids: number[]) {
|
||||
return requestClient.delete(
|
||||
`/system/notice/delete-list?ids=${ids.join(',')}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 推送公告 */
|
||||
export function pushNotice(id: number) {
|
||||
return requestClient.post(`/system/notice/push?id=${id}`);
|
||||
|
|
|
@ -59,6 +59,13 @@ export function deleteNotifyTemplate(id: number) {
|
|||
return requestClient.delete(`/system/notify-template/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除站内信模板 */
|
||||
export function deleteNotifyTemplateList(ids: number[]) {
|
||||
return requestClient.delete(
|
||||
`/system/notify-template/delete-list?ids=${ids.join(',')}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 导出站内信模板 */
|
||||
export function exportNotifyTemplate(params: any) {
|
||||
return requestClient.download('/system/notify-template/export-excel', {
|
||||
|
|
|
@ -32,3 +32,10 @@ export function deleteOAuth2Token(accessToken: string) {
|
|||
`/system/oauth2-token/delete?accessToken=${accessToken}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 批量删除 OAuth2.0 令牌 */
|
||||
export function deleteOAuth2TokenList(accessTokens: string[]) {
|
||||
return requestClient.delete(
|
||||
`/system/oauth2-token/delete-list?accessTokens=${accessTokens.join(',')}`,
|
||||
);
|
||||
}
|
||||
|
|
|
@ -50,9 +50,14 @@ export function deletePost(id: number) {
|
|||
return requestClient.delete(`/system/post/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除岗位 */
|
||||
export function deletePostList(ids: number[]) {
|
||||
return requestClient.delete(`/system/post/delete-list?ids=${ids.join(',')}`);
|
||||
}
|
||||
|
||||
/** 导出岗位 */
|
||||
export function exportPost(params: any) {
|
||||
return requestClient.download('/system/post/export', {
|
||||
return requestClient.download('/system/post/export-excel', {
|
||||
params,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -50,6 +50,11 @@ export function deleteRole(id: number) {
|
|||
return requestClient.delete(`/system/role/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除角色 */
|
||||
export function deleteRoleList(ids: number[]) {
|
||||
return requestClient.delete(`/system/role/delete-list?ids=${ids.join(',')}`);
|
||||
}
|
||||
|
||||
/** 导出角色 */
|
||||
export function exportRole(params: any) {
|
||||
return requestClient.download('/system/role/export-excel', {
|
||||
|
|
|
@ -54,7 +54,14 @@ export function deleteSmsChannel(id: number) {
|
|||
return requestClient.delete(`/system/sms-channel/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除短信渠道 */
|
||||
export function deleteSmsChannelList(ids: number[]) {
|
||||
return requestClient.delete(
|
||||
`/system/sms-channel/delete-list?ids=${ids.join(',')}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 导出短信渠道 */
|
||||
export function exportSmsChannel(params: any) {
|
||||
return requestClient.download('/system/sms-channel/export', { params });
|
||||
return requestClient.download('/system/sms-channel/export-excel', { params });
|
||||
}
|
||||
|
|
|
@ -57,6 +57,13 @@ export function deleteSmsTemplate(id: number) {
|
|||
return requestClient.delete(`/system/sms-template/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除短信模板 */
|
||||
export function deleteSmsTemplateList(ids: number[]) {
|
||||
return requestClient.delete(
|
||||
`/system/sms-template/delete-list?ids=${ids.join(',')}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 导出短信模板 */
|
||||
export function exportSmsTemplate(params: any) {
|
||||
return requestClient.download('/system/sms-template/export-excel', {
|
||||
|
|
|
@ -49,6 +49,13 @@ export function deleteTenantPackage(id: number) {
|
|||
return requestClient.delete(`/system/tenant-package/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除租户套餐 */
|
||||
export function deleteTenantPackageList(ids: number[]) {
|
||||
return requestClient.delete(
|
||||
`/system/tenant-package/delete-list?ids=${ids.join(',')}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 获取租户套餐精简信息列表 */
|
||||
export function getTenantPackageList() {
|
||||
return requestClient.get<SystemTenantPackageApi.TenantPackage[]>(
|
||||
|
|
|
@ -61,6 +61,13 @@ export function deleteTenant(id: number) {
|
|||
return requestClient.delete(`/system/tenant/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除租户 */
|
||||
export function deleteTenantList(ids: number[]) {
|
||||
return requestClient.delete(
|
||||
`/system/tenant/delete-list?ids=${ids.join(',')}`,
|
||||
);
|
||||
}
|
||||
|
||||
/** 导出租户 */
|
||||
export function exportTenant(params: any) {
|
||||
return requestClient.download('/system/tenant/export-excel', {
|
||||
|
|
|
@ -49,9 +49,14 @@ export function deleteUser(id: number) {
|
|||
return requestClient.delete(`/system/user/delete?id=${id}`);
|
||||
}
|
||||
|
||||
/** 批量删除用户 */
|
||||
export function deleteUserList(ids: number[]) {
|
||||
return requestClient.delete(`/system/user/delete-list?ids=${ids.join(',')}`);
|
||||
}
|
||||
|
||||
/** 导出用户 */
|
||||
export function exportUser(params: any) {
|
||||
return requestClient.download('/system/user/export', params);
|
||||
return requestClient.download('/system/user/export-excel', params);
|
||||
}
|
||||
|
||||
/** 下载用户导入模板 */
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
<script setup lang="ts">
|
||||
import type { Item } from './ui/typing';
|
||||
|
||||
import { onMounted, onUnmounted, ref } from 'vue';
|
||||
|
||||
import { Tinyflow as TinyflowNative } from './ui/index';
|
||||
|
||||
import './ui/index.css';
|
||||
|
||||
const props = defineProps<{
|
||||
className?: string;
|
||||
data?: Record<string, any>;
|
||||
provider?: {
|
||||
internal?: () => Item[] | Promise<Item[]>;
|
||||
knowledge?: () => Item[] | Promise<Item[]>;
|
||||
llm?: () => Item[] | Promise<Item[]>;
|
||||
};
|
||||
style?: Record<string, string>;
|
||||
}>();
|
||||
|
||||
const divRef = ref<HTMLDivElement | null>(null);
|
||||
let tinyflow: null | TinyflowNative = null;
|
||||
// 定义默认的 provider 方法
|
||||
const defaultProvider = {
|
||||
llm: () => [] as Item[],
|
||||
knowledge: () => [] as Item[],
|
||||
internal: () => [] as Item[],
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
if (divRef.value) {
|
||||
// 合并默认 provider 和传入的 props.provider
|
||||
const mergedProvider = {
|
||||
...defaultProvider,
|
||||
...props.provider,
|
||||
};
|
||||
tinyflow = new TinyflowNative({
|
||||
element: divRef.value as Element,
|
||||
data: props.data || {},
|
||||
provider: mergedProvider,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (tinyflow) {
|
||||
tinyflow.destroy();
|
||||
tinyflow = null;
|
||||
}
|
||||
});
|
||||
|
||||
const getData = () => {
|
||||
if (tinyflow) {
|
||||
return tinyflow.getData();
|
||||
}
|
||||
console.warn('Tinyflow instance is not initialized');
|
||||
return null;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
getData,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
ref="divRef"
|
||||
class="tinyflow"
|
||||
:class="[className]"
|
||||
:style="style"
|
||||
style="height: 100%"
|
||||
></div>
|
||||
</template>
|
|
@ -0,0 +1,2 @@
|
|||
export { default as Tinyflow } from './tinyflow.vue';
|
||||
export * from './ui/typing';
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,68 @@
|
|||
export interface Item {
|
||||
children?: Item[];
|
||||
label: string;
|
||||
value: number | string;
|
||||
}
|
||||
|
||||
export interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface Viewport {
|
||||
x: number;
|
||||
y: number;
|
||||
zoom: number;
|
||||
}
|
||||
|
||||
export interface Node {
|
||||
data?: Record<string, any>;
|
||||
draggable?: boolean;
|
||||
height?: number;
|
||||
id: string;
|
||||
position: Position;
|
||||
selected?: boolean;
|
||||
type?: string;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export interface Edge {
|
||||
animated?: boolean;
|
||||
id: string;
|
||||
label?: string;
|
||||
source: string;
|
||||
target: string;
|
||||
type?: string;
|
||||
}
|
||||
export type TinyflowData = Partial<{
|
||||
edges: Edge[];
|
||||
nodes: Node[];
|
||||
viewport: Viewport;
|
||||
}>;
|
||||
|
||||
export interface TinyflowOptions {
|
||||
data?: TinyflowData;
|
||||
element: Element | string;
|
||||
provider?: {
|
||||
internal?: () => Item[] | Promise<Item[]>;
|
||||
knowledge?: () => Item[] | Promise<Item[]>;
|
||||
llm?: () => Item[] | Promise<Item[]>;
|
||||
};
|
||||
}
|
||||
|
||||
export declare class Tinyflow {
|
||||
private _init;
|
||||
private _setOptions;
|
||||
private options;
|
||||
private rootEl;
|
||||
private svelteFlowInstance;
|
||||
constructor(options: TinyflowOptions);
|
||||
destroy(): void;
|
||||
getData(): {
|
||||
edges: Edge[];
|
||||
nodes: Node[];
|
||||
viewport: Viewport;
|
||||
};
|
||||
getOptions(): TinyflowOptions;
|
||||
setData(data: TinyflowData): void;
|
||||
}
|
|
@ -29,14 +29,14 @@ withDefaults(
|
|||
<Card :body-style="bodyStyle" :title="title" class="mb-4">
|
||||
<template v-if="title" #title>
|
||||
<div class="flex items-center">
|
||||
<span class="text-4 font-[700]">{{ title }}</span>
|
||||
<span class="text-base font-bold">{{ title }}</span>
|
||||
<Tooltip placement="right">
|
||||
<template #title>
|
||||
<div class="max-w-[200px]">{{ message }}</div>
|
||||
</template>
|
||||
<ShieldQuestion :size="14" class="ml-5px" />
|
||||
<ShieldQuestion :size="14" class="ml-1" />
|
||||
</Tooltip>
|
||||
<div class="pl-20px flex flex-grow">
|
||||
<div class="flex flex-grow pl-5">
|
||||
<slot name="header"></slot>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -6,6 +6,7 @@ import type { CropperAvatarProps } from './typing';
|
|||
import { computed, ref, unref, watch, watchEffect } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
|
||||
import { Button, message } from 'ant-design-vue';
|
||||
|
@ -80,15 +81,16 @@ defineExpose({
|
|||
@click="openModal"
|
||||
>
|
||||
<div :class="`${prefixCls}-image-mask`" :style="getImageWrapperStyle">
|
||||
<span
|
||||
<IconifyIcon
|
||||
icon="lucide:cloud-upload"
|
||||
class="text-gray-400"
|
||||
:style="{
|
||||
...getImageWrapperStyle,
|
||||
width: `${getIconWidth}`,
|
||||
height: `${getIconWidth}`,
|
||||
lineHeight: `${getIconWidth}`,
|
||||
}"
|
||||
class="icon-[ant-design--cloud-upload-outlined] text-[#d6d6d6]"
|
||||
></span>
|
||||
/>
|
||||
</div>
|
||||
<img v-if="sourceValue" :src="sourceValue" alt="avatar" />
|
||||
</div>
|
||||
|
|
|
@ -4,6 +4,7 @@ import type { CropendResult, CropperModalProps, CropperType } from './typing';
|
|||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
import { dataURLtoBlob, isFunction } from '@vben/utils';
|
||||
|
||||
|
@ -118,7 +119,7 @@ async function handleOk() {
|
|||
:confirm-text="$t('ui.cropper.okText')"
|
||||
:fullscreen-button="false"
|
||||
:title="$t('ui.cropper.modalTitle')"
|
||||
class="w-[800px]"
|
||||
class="w-2/3"
|
||||
>
|
||||
<div :class="prefixCls">
|
||||
<div :class="`${prefixCls}-left`" class="w-full">
|
||||
|
@ -143,7 +144,7 @@ async function handleOk() {
|
|||
<Button size="small" type="primary">
|
||||
<template #icon>
|
||||
<div class="flex items-center justify-center">
|
||||
<span class="icon-[ant-design--upload-outlined]"></span>
|
||||
<IconifyIcon icon="lucide:upload" />
|
||||
</div>
|
||||
</template>
|
||||
</Button>
|
||||
|
@ -159,7 +160,7 @@ async function handleOk() {
|
|||
>
|
||||
<template #icon>
|
||||
<div class="flex items-center justify-center">
|
||||
<span class="icon-[ant-design--reload-outlined]"></span>
|
||||
<IconifyIcon icon="lucide:rotate-ccw" />
|
||||
</div>
|
||||
</template>
|
||||
</Button>
|
||||
|
@ -176,9 +177,7 @@ async function handleOk() {
|
|||
>
|
||||
<template #icon>
|
||||
<div class="flex items-center justify-center">
|
||||
<span
|
||||
class="icon-[ant-design--rotate-left-outlined]"
|
||||
></span>
|
||||
<IconifyIcon icon="ant-design:rotate-left-outlined" />
|
||||
</div>
|
||||
</template>
|
||||
</Button>
|
||||
|
@ -189,16 +188,13 @@ async function handleOk() {
|
|||
>
|
||||
<Button
|
||||
:disabled="!src"
|
||||
pre-icon="ant-design:rotate-right-outlined"
|
||||
size="small"
|
||||
type="primary"
|
||||
@click="handlerToolbar('rotate', 45)"
|
||||
>
|
||||
<template #icon>
|
||||
<div class="flex items-center justify-center">
|
||||
<span
|
||||
class="icon-[ant-design--rotate-right-outlined]"
|
||||
></span>
|
||||
<IconifyIcon icon="ant-design:rotate-right-outlined" />
|
||||
</div>
|
||||
</template>
|
||||
</Button>
|
||||
|
@ -212,7 +208,7 @@ async function handleOk() {
|
|||
>
|
||||
<template #icon>
|
||||
<div class="flex items-center justify-center">
|
||||
<span class="icon-[vaadin--arrows-long-h]"></span>
|
||||
<IconifyIcon icon="vaadin--arrows-long-h" />
|
||||
</div>
|
||||
</template>
|
||||
</Button>
|
||||
|
@ -226,7 +222,7 @@ async function handleOk() {
|
|||
>
|
||||
<template #icon>
|
||||
<div class="flex items-center justify-center">
|
||||
<span class="icon-[vaadin--arrows-long-v]"></span>
|
||||
<IconifyIcon icon="vaadin:arrows-long-v" />
|
||||
</div>
|
||||
</template>
|
||||
</Button>
|
||||
|
@ -240,7 +236,7 @@ async function handleOk() {
|
|||
>
|
||||
<template #icon>
|
||||
<div class="flex items-center justify-center">
|
||||
<span class="icon-[ant-design--zoom-in-outlined]"></span>
|
||||
<IconifyIcon icon="lucide:zoom-in" />
|
||||
</div>
|
||||
</template>
|
||||
</Button>
|
||||
|
@ -254,7 +250,7 @@ async function handleOk() {
|
|||
>
|
||||
<template #icon>
|
||||
<div class="flex items-center justify-center">
|
||||
<span class="icon-[ant-design--zoom-out-outlined]"></span>
|
||||
<IconifyIcon icon="lucide:zoom-out" />
|
||||
</div>
|
||||
</template>
|
||||
</Button>
|
||||
|
|
|
@ -45,7 +45,7 @@ const getDictOptions = computed(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Select v-if="selectType === 'select'" class="w-1/1" v-bind="attrs">
|
||||
<Select v-if="selectType === 'select'" class="w-full" v-bind="attrs">
|
||||
<SelectOption
|
||||
v-for="(dict, index) in getDictOptions"
|
||||
:key="index"
|
||||
|
@ -54,7 +54,7 @@ const getDictOptions = computed(() => {
|
|||
{{ dict.label }}
|
||||
</SelectOption>
|
||||
</Select>
|
||||
<RadioGroup v-if="selectType === 'radio'" class="w-1/1" v-bind="attrs">
|
||||
<RadioGroup v-if="selectType === 'radio'" class="w-full" v-bind="attrs">
|
||||
<Radio
|
||||
v-for="(dict, index) in getDictOptions"
|
||||
:key="index"
|
||||
|
@ -63,7 +63,7 @@ const getDictOptions = computed(() => {
|
|||
{{ dict.label }}
|
||||
</Radio>
|
||||
</RadioGroup>
|
||||
<CheckboxGroup v-if="selectType === 'checkbox'" class="w-1/1" v-bind="attrs">
|
||||
<CheckboxGroup v-if="selectType === 'checkbox'" class="w-full" v-bind="attrs">
|
||||
<Checkbox
|
||||
v-for="(dict, index) in getDictOptions"
|
||||
:key="index"
|
||||
|
|
|
@ -190,7 +190,7 @@ export const useApiSelect = (option: ApiSelectProps) => {
|
|||
// fix:多写此步是为了解决 multiple 属性问题
|
||||
return (
|
||||
<Select
|
||||
class="w-1/1"
|
||||
class="w-full"
|
||||
loading={loading.value}
|
||||
mode="multiple"
|
||||
{...attrs}
|
||||
|
@ -210,7 +210,7 @@ export const useApiSelect = (option: ApiSelectProps) => {
|
|||
}
|
||||
return (
|
||||
<Select
|
||||
class="w-1/1"
|
||||
class="w-full"
|
||||
loading={loading.value}
|
||||
{...attrs}
|
||||
// TODO: @dhb52 remote 对等实现, 还是说没作用
|
||||
|
@ -235,7 +235,7 @@ export const useApiSelect = (option: ApiSelectProps) => {
|
|||
];
|
||||
}
|
||||
return (
|
||||
<CheckboxGroup class="w-1/1" {...attrs}>
|
||||
<CheckboxGroup class="w-full" {...attrs}>
|
||||
{options.value.map(
|
||||
(item: { label: any; value: any }, index: any) => (
|
||||
<Checkbox key={index} value={item.value}>
|
||||
|
@ -254,7 +254,7 @@ export const useApiSelect = (option: ApiSelectProps) => {
|
|||
];
|
||||
}
|
||||
return (
|
||||
<RadioGroup class="w-1/1" {...attrs}>
|
||||
<RadioGroup class="w-full" {...attrs}>
|
||||
{options.value.map(
|
||||
(item: { label: any; value: any }, index: any) => (
|
||||
<Radio key={index} value={item.value}>
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
export { default as MarkdownView } from './markdown-view.vue';
|
||||
|
||||
export * from './typing';
|
|
@ -0,0 +1,206 @@
|
|||
<script setup lang="ts">
|
||||
import type { MarkdownViewProps } from './typing';
|
||||
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
|
||||
import { MarkdownIt } from '@vben/plugins/markmap';
|
||||
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import { message } from 'ant-design-vue';
|
||||
import hljs from 'highlight.js';
|
||||
|
||||
import 'highlight.js/styles/vs2015.min.css';
|
||||
|
||||
// 定义组件属性
|
||||
const props = defineProps<MarkdownViewProps>();
|
||||
|
||||
const { copy } = useClipboard(); // 初始化 copy 到粘贴板
|
||||
const contentRef = ref<HTMLElement | null>(null);
|
||||
|
||||
const md = new MarkdownIt({
|
||||
highlight(str, lang) {
|
||||
if (lang && hljs.getLanguage(lang)) {
|
||||
try {
|
||||
const copyHtml = `<div id="copy" data-copy='${str}' style="position: absolute; right: 10px; top: 5px; color: #fff;cursor: pointer;">复制</div>`;
|
||||
return `<pre style="position: relative;">${copyHtml}<code class="hljs">${hljs.highlight(lang, str, true).value}</code></pre>`;
|
||||
} catch {}
|
||||
}
|
||||
return ``;
|
||||
},
|
||||
});
|
||||
|
||||
/** 渲染 markdown */
|
||||
const renderedMarkdown = computed(() => {
|
||||
return md.render(props.content);
|
||||
});
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
// 添加 copy 监听
|
||||
contentRef.value?.addEventListener('click', (e: any) => {
|
||||
if (e.target.id === 'copy') {
|
||||
copy(e.target?.dataset?.copy);
|
||||
message.success('复制成功!');
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="contentRef" class="markdown-view" v-html="renderedMarkdown"></div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
.markdown-view {
|
||||
max-width: 100%;
|
||||
font-family: 'PingFang SC';
|
||||
font-size: 0.95rem;
|
||||
font-weight: 400;
|
||||
line-height: 1.6rem;
|
||||
color: #3b3e55;
|
||||
text-align: left;
|
||||
letter-spacing: 0;
|
||||
|
||||
pre {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
pre code.hljs {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
code.hljs {
|
||||
width: auto;
|
||||
padding-top: 20px;
|
||||
border-radius: 6px;
|
||||
|
||||
@media screen and (min-width: 1536px) {
|
||||
width: 960px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1536px) and (min-width: 1024px) {
|
||||
width: calc(100vw - 400px - 64px - 32px * 2);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1024px) and (min-width: 768px) {
|
||||
width: calc(100vw - 32px * 2);
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
width: calc(100vw - 16px * 2);
|
||||
}
|
||||
}
|
||||
|
||||
p,
|
||||
code.hljs {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
p {
|
||||
//margin-bottom: 1rem !important;
|
||||
margin: 0;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
/* 标题通用格式 */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
margin: 24px 0 8px;
|
||||
font-weight: 600;
|
||||
color: #3b3e55;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 22px;
|
||||
line-height: 32px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 20px;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 18px;
|
||||
line-height: 28px;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 16px;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
/* 列表(有序,无序) */
|
||||
ul,
|
||||
ol {
|
||||
padding: 0;
|
||||
margin: 0 0 8px;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
color: #3b3e55; // var(--color-CG600);
|
||||
}
|
||||
|
||||
li {
|
||||
margin: 4px 0 0 20px;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
ol > li {
|
||||
margin-bottom: 1rem;
|
||||
list-style-type: decimal;
|
||||
// 表达式,修复有序列表序号展示不全的问题
|
||||
// &:nth-child(n + 10) {
|
||||
// margin-left: 30px;
|
||||
// }
|
||||
|
||||
// &:nth-child(n + 100) {
|
||||
// margin-left: 30px;
|
||||
// }
|
||||
}
|
||||
|
||||
ul > li {
|
||||
margin-right: 11px;
|
||||
margin-bottom: 1rem;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
color: #3b3e55; // var(--color-G900);
|
||||
list-style-type: disc;
|
||||
}
|
||||
|
||||
ol ul,
|
||||
ol ul > li,
|
||||
ul ul,
|
||||
ul ul li {
|
||||
margin-bottom: 1rem;
|
||||
margin-left: 6px;
|
||||
// list-style: circle;
|
||||
font-size: 16px;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
ul ul ul,
|
||||
ul ul ul li,
|
||||
ol ol,
|
||||
ol ol > li,
|
||||
ol ul ul,
|
||||
ol ul ul > li,
|
||||
ul ol,
|
||||
ul ol > li {
|
||||
list-style: square;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,3 @@
|
|||
export type MarkdownViewProps = {
|
||||
content: string;
|
||||
};
|
|
@ -43,7 +43,7 @@ function getUserTypeColor(userType: number) {
|
|||
<template #dot>
|
||||
<p
|
||||
:style="{ backgroundColor: getUserTypeColor(log.userType) }"
|
||||
class="absolute left-[-5px] flex h-5 w-5 items-center justify-center rounded-full text-xs text-white"
|
||||
class="absolute left--1 flex h-5 w-5 items-center justify-center rounded-full text-xs text-white"
|
||||
>
|
||||
{{ getDictLabel(DICT_TYPE.USER_TYPE, log.userType)[0] }}
|
||||
</p>
|
||||
|
|
|
@ -122,7 +122,7 @@ function handleCheck() {
|
|||
}
|
||||
</script>
|
||||
<template>
|
||||
<Modal :title="title" key="dept-select-modal" class="w-[40%]">
|
||||
<Modal :title="title" key="dept-select-modal" class="w-2/5">
|
||||
<Row class="h-full">
|
||||
<Col :span="24">
|
||||
<Card class="h-full">
|
||||
|
|
|
@ -408,7 +408,7 @@ function processDeptNode(node: any): DeptTreeNode {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-[40%]" key="user-select-modal" :title="title">
|
||||
<Modal class="w-2/5" key="user-select-modal" :title="title">
|
||||
<Row :gutter="[16, 16]">
|
||||
<Col :span="6">
|
||||
<div class="h-[500px] overflow-auto rounded border">
|
||||
|
|
|
@ -158,7 +158,7 @@ function changeNodeName() {
|
|||
defineExpose({ open }); // 提供 open 方法,用于打开弹窗
|
||||
</script>
|
||||
<template>
|
||||
<Drawer class="w-[580px]">
|
||||
<Drawer class="w-1/3">
|
||||
<template #title>
|
||||
<div class="flex items-center">
|
||||
<Input
|
||||
|
|
|
@ -207,14 +207,14 @@ onMounted(() => {
|
|||
defineExpose({ showCopyTaskNodeConfig }); // 暴露方法给父组件
|
||||
</script>
|
||||
<template>
|
||||
<Drawer class="w-[580px]">
|
||||
<Drawer class="w-1/3">
|
||||
<template #title>
|
||||
<div class="config-header">
|
||||
<Input
|
||||
v-if="showInput"
|
||||
ref="inputRef"
|
||||
type="text"
|
||||
class="config-editable-input"
|
||||
class="focus:border-blue-500 focus:shadow-[0_0_0_2px_rgba(24,144,255,0.2)] focus:outline-none"
|
||||
@blur="changeNodeName()"
|
||||
@press-enter="changeNodeName()"
|
||||
v-model:value="nodeName"
|
||||
|
@ -446,7 +446,7 @@ defineExpose({ showCopyTaskNodeConfig }); // 暴露方法给父组件
|
|||
v-if="formType === BpmModelFormType.NORMAL"
|
||||
>
|
||||
<div class="p-1">
|
||||
<div class="mb-4 text-[16px] font-bold">字段权限</div>
|
||||
<div class="mb-4 text-base font-bold">字段权限</div>
|
||||
|
||||
<!-- 表头 -->
|
||||
<Row class="border border-gray-200 px-4 py-3">
|
||||
|
@ -522,12 +522,3 @@ defineExpose({ showCopyTaskNodeConfig }); // 暴露方法给父组件
|
|||
</Tabs>
|
||||
</Drawer>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.config-editable-input {
|
||||
&:focus {
|
||||
outline: 0;
|
||||
border-color: #40a9ff;
|
||||
box-shadow: 0 0 0 2px rgb(24 144 255 / 20%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -152,7 +152,7 @@ function openDrawer(node: SimpleFlowNode) {
|
|||
defineExpose({ openDrawer }); // 暴露方法给父组件
|
||||
</script>
|
||||
<template>
|
||||
<Drawer class="w-[480px]">
|
||||
<Drawer class="w-1/3">
|
||||
<template #title>
|
||||
<div class="flex items-center">
|
||||
<Input
|
||||
|
|
|
@ -178,7 +178,7 @@ defineExpose({ validate });
|
|||
{{ condition.conditionGroups.and ? '且' : '或' }}
|
||||
</template>
|
||||
<Card
|
||||
class="group relative w-full hover:border-[#1890ff]"
|
||||
class="group relative w-full hover:border-blue-500"
|
||||
v-for="(equation, cIdx) in condition.conditionGroups.conditions"
|
||||
:key="cIdx"
|
||||
>
|
||||
|
@ -187,7 +187,7 @@ defineExpose({ validate });
|
|||
v-if="condition.conditionGroups.conditions.length > 1"
|
||||
>
|
||||
<IconifyIcon
|
||||
color="#0089ff"
|
||||
color="blue"
|
||||
icon="lucide:circle-x"
|
||||
class="size-4"
|
||||
@click="
|
||||
|
@ -290,7 +290,7 @@ defineExpose({ validate });
|
|||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="3">
|
||||
<div class="flex h-[32px] items-center">
|
||||
<div class="flex h-8 items-center">
|
||||
<Trash2
|
||||
v-if="equation.rules.length > 1"
|
||||
class="mr-2 size-4 cursor-pointer text-red-500"
|
||||
|
@ -307,7 +307,7 @@ defineExpose({ validate });
|
|||
</Space>
|
||||
<div title="添加条件组" class="mt-4 cursor-pointer">
|
||||
<Plus
|
||||
class="size-[24px] text-blue-500"
|
||||
class="size-6 text-blue-500"
|
||||
@click="addConditionGroup(condition.conditionGroups?.conditions)"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import type { HttpRequestParam } from '../../../consts';
|
||||
|
||||
import { Plus, Trash2 } from '@vben/icons';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import {
|
||||
Button,
|
||||
|
@ -121,9 +121,10 @@ function deleteHttpRequestParam(arr: HttpRequestParam[], index: number) {
|
|||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="2">
|
||||
<div class="flex h-[32px] items-center">
|
||||
<Trash2
|
||||
<div class="flex h-8 items-center">
|
||||
<IconifyIcon
|
||||
class="size-4 cursor-pointer text-red-500"
|
||||
icon="lucide:trash-2"
|
||||
@click="deleteHttpRequestParam(props.header, index)"
|
||||
/>
|
||||
</div>
|
||||
|
@ -135,7 +136,7 @@ function deleteHttpRequestParam(arr: HttpRequestParam[], index: number) {
|
|||
class="flex items-center"
|
||||
>
|
||||
<template #icon>
|
||||
<Plus class="size-[18px]" />
|
||||
<IconifyIcon class="size-4" icon="lucide:plus" />
|
||||
</template>
|
||||
添加一行
|
||||
</Button>
|
||||
|
@ -205,9 +206,10 @@ function deleteHttpRequestParam(arr: HttpRequestParam[], index: number) {
|
|||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="2">
|
||||
<div class="flex h-[32px] items-center">
|
||||
<Trash2
|
||||
<div class="flex h-8 items-center">
|
||||
<IconifyIcon
|
||||
class="size-4 cursor-pointer text-red-500"
|
||||
icon="lucide:trash-2"
|
||||
@click="deleteHttpRequestParam(props.body, index)"
|
||||
/>
|
||||
</div>
|
||||
|
@ -219,7 +221,7 @@ function deleteHttpRequestParam(arr: HttpRequestParam[], index: number) {
|
|||
class="flex items-center"
|
||||
>
|
||||
<template #icon>
|
||||
<Plus class="size-[18px]" />
|
||||
<IconifyIcon class="size-4" icon="lucide:plus" />
|
||||
</template>
|
||||
添加一行
|
||||
</Button>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { toRefs, watch } from 'vue';
|
||||
|
||||
import { Plus, Trash2 } from '@vben/icons';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import {
|
||||
Alert,
|
||||
|
@ -153,9 +153,10 @@ function deleteHttpResponseSetting(
|
|||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="2">
|
||||
<div class="flex h-[32px] items-center">
|
||||
<Trash2
|
||||
<div class="flex h-8 items-center">
|
||||
<IconifyIcon
|
||||
class="size-4 cursor-pointer text-red-500"
|
||||
icon="lucide:trash-2"
|
||||
@click="deleteHttpResponseSetting(setting.response!, index)"
|
||||
/>
|
||||
</div>
|
||||
|
@ -167,7 +168,7 @@ function deleteHttpResponseSetting(
|
|||
class="flex items-center"
|
||||
>
|
||||
<template #icon>
|
||||
<Plus class="size-[18px]" />
|
||||
<IconifyIcon class="size-4" icon="lucide:plus" />
|
||||
</template>
|
||||
添加一行
|
||||
</Button>
|
||||
|
|
|
@ -200,7 +200,7 @@ function getRouterNode(node: any) {
|
|||
defineExpose({ openDrawer }); // 暴露方法给父组件
|
||||
</script>
|
||||
<template>
|
||||
<Drawer class="w-[40%]">
|
||||
<Drawer class="w-2/5">
|
||||
<template #title>
|
||||
<div class="flex items-center">
|
||||
<Input
|
||||
|
@ -236,7 +236,7 @@ defineExpose({ openDrawer }); // 暴露方法给父组件
|
|||
<div class="flex items-center font-normal">
|
||||
<span class="font-medium">路由{{ index + 1 }}</span>
|
||||
<FormItem
|
||||
class="mb-0 ml-4 inline-block w-[180px]"
|
||||
class="mb-0 ml-4 inline-block w-48"
|
||||
:name="['routerGroups', index, 'nodeId']"
|
||||
:rules="{
|
||||
required: true,
|
||||
|
|
|
@ -148,7 +148,7 @@ defineExpose({ showStartUserNodeConfig });
|
|||
ref="inputRef"
|
||||
v-if="showInput"
|
||||
type="text"
|
||||
class="config-editable-input"
|
||||
class="focus:border-blue-500 focus:shadow-[0_0_0_2px_rgba(24,144,255,0.2)] focus:outline-none"
|
||||
@blur="changeNodeName()"
|
||||
@press-enter="changeNodeName()"
|
||||
v-model:value="nodeName"
|
||||
|
@ -214,7 +214,7 @@ defineExpose({ showStartUserNodeConfig });
|
|||
v-if="formType === BpmModelFormType.NORMAL"
|
||||
>
|
||||
<div class="p-1">
|
||||
<div class="mb-4 text-[16px] font-bold">字段权限</div>
|
||||
<div class="mb-4 text-base font-bold">字段权限</div>
|
||||
|
||||
<!-- 表头 -->
|
||||
<Row class="border border-gray-200 px-4 py-3">
|
||||
|
|
|
@ -11,7 +11,7 @@ import type {
|
|||
import { computed, getCurrentInstance, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import { useVbenDrawer } from '@vben/common-ui';
|
||||
import { IconifyIcon, Trash2 } from '@vben/icons';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { cloneDeep } from '@vben/utils';
|
||||
|
||||
import {
|
||||
|
@ -384,14 +384,14 @@ onMounted(() => {
|
|||
});
|
||||
</script>
|
||||
<template>
|
||||
<Drawer class="w-[580px]">
|
||||
<Drawer class="w-1/3">
|
||||
<template #title>
|
||||
<div class="config-header">
|
||||
<Input
|
||||
ref="inputRef"
|
||||
v-if="showInput"
|
||||
type="text"
|
||||
class="config-editable-input"
|
||||
class="focus:border-blue-500 focus:shadow-[0_0_0_2px_rgba(24,144,255,0.2)] focus:outline-none"
|
||||
@blur="changeNodeName()"
|
||||
@press-enter="changeNodeName()"
|
||||
v-model:value="nodeName"
|
||||
|
@ -543,9 +543,10 @@ onMounted(() => {
|
|||
</FormItem>
|
||||
</Col>
|
||||
<Col :span="2">
|
||||
<div class="flex h-[32px] items-center">
|
||||
<Trash2
|
||||
<div class="flex h-8 items-center">
|
||||
<IconifyIcon
|
||||
class="size-4 cursor-pointer text-red-500"
|
||||
icon="lucide:trash-2"
|
||||
@click="deleteFormFieldSetting(formSetting, key)"
|
||||
/>
|
||||
</div>
|
||||
|
@ -684,12 +685,3 @@ onMounted(() => {
|
|||
</div>
|
||||
</Drawer>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.config-editable-input {
|
||||
&:focus {
|
||||
outline: 0;
|
||||
border-color: #40a9ff;
|
||||
box-shadow: 0 0 0 2px rgb(24 144 255 / 20%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -580,14 +580,14 @@ onMounted(() => {
|
|||
});
|
||||
</script>
|
||||
<template>
|
||||
<Drawer class="w-[580px]">
|
||||
<Drawer class="w-1/3">
|
||||
<template #title>
|
||||
<div class="config-header">
|
||||
<Input
|
||||
v-if="showInput"
|
||||
ref="inputRef"
|
||||
type="text"
|
||||
class="config-editable-input"
|
||||
class="focus:border-blue-500 focus:shadow-[0_0_0_2px_rgba(24,144,255,0.2)] focus:outline-none"
|
||||
@blur="changeNodeName()"
|
||||
@press-enter="changeNodeName()"
|
||||
v-model:value="nodeName"
|
||||
|
@ -603,7 +603,7 @@ onMounted(() => {
|
|||
v-if="currentNode.type === BpmNodeTypeEnum.USER_TASK_NODE"
|
||||
class="mb-3 flex items-center"
|
||||
>
|
||||
<span class="mr-3 text-[16px]">审批类型 :</span>
|
||||
<span class="mr-3 text-base">审批类型 :</span>
|
||||
<RadioGroup v-model:value="approveType">
|
||||
<RadioButton
|
||||
v-for="(item, index) in APPROVE_TYPE"
|
||||
|
@ -949,7 +949,7 @@ onMounted(() => {
|
|||
label="超时时间设置"
|
||||
v-if="configForm.timeoutHandlerEnable"
|
||||
label-align="left"
|
||||
class="h-[32px]"
|
||||
class="h-8"
|
||||
:label-col="{ span: 6 }"
|
||||
:wrapper-col="{ span: 18 }"
|
||||
>
|
||||
|
@ -1105,7 +1105,7 @@ onMounted(() => {
|
|||
key="buttons"
|
||||
>
|
||||
<div class="p-1">
|
||||
<div class="mb-4 text-[16px] font-bold">操作按钮</div>
|
||||
<div class="mb-4 text-base font-bold">操作按钮</div>
|
||||
|
||||
<!-- 表头 -->
|
||||
<Row class="border border-gray-200 px-4 py-3">
|
||||
|
@ -1127,7 +1127,7 @@ onMounted(() => {
|
|||
<Input
|
||||
v-if="btnDisplayNameEdit[index]"
|
||||
type="text"
|
||||
class="input-edit max-w-[130px]"
|
||||
class="max-w-32 focus:border-blue-500 focus:shadow-[0_0_0_2px_rgba(24,144,255,0.2)] focus:outline-none"
|
||||
@blur="btnDisplayNameBlurEvent(index)"
|
||||
v-model:value="item.displayName"
|
||||
:placeholder="item.displayName"
|
||||
|
@ -1152,7 +1152,7 @@ onMounted(() => {
|
|||
v-if="formType === BpmModelFormType.NORMAL"
|
||||
>
|
||||
<div class="p-1">
|
||||
<div class="mb-4 text-[16px] font-bold">字段权限</div>
|
||||
<div class="mb-4 text-base font-bold">字段权限</div>
|
||||
|
||||
<!-- 表头 -->
|
||||
<Row class="border border-gray-200 px-4 py-3">
|
||||
|
@ -1234,12 +1234,3 @@ onMounted(() => {
|
|||
</Tabs>
|
||||
</Drawer>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.input-edit {
|
||||
&:focus {
|
||||
outline: 0;
|
||||
border-color: #40a9ff;
|
||||
box-shadow: 0 0 0 2px rgb(24 144 255 / 20%);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -235,7 +235,7 @@ defineExpose({ validate });
|
|||
:readonly="false"
|
||||
@save="saveSimpleFlowModel"
|
||||
/>
|
||||
<ErrorModal title="流程设计校验不通过" class="w-[40%]">
|
||||
<ErrorModal title="流程设计校验不通过" class="w-2/5">
|
||||
<div class="mb-2 text-base">以下节点配置不完善,请修改相关配置</div>
|
||||
<div
|
||||
class="mb-3 rounded-md bg-gray-100 p-2 text-sm"
|
||||
|
|
|
@ -201,7 +201,7 @@ onMounted(() => {
|
|||
</script>
|
||||
<template>
|
||||
<div class="simple-process-model-container">
|
||||
<div class="absolute right-[0px] top-[0px] bg-[#fff]">
|
||||
<div class="absolute right-0 top-0 bg-white">
|
||||
<Row type="flex" justify="end">
|
||||
<ButtonGroup key="scale-control">
|
||||
<Button v-if="!readonly" @click="exportJson">
|
||||
|
@ -216,7 +216,7 @@ onMounted(() => {
|
|||
type="file"
|
||||
id="files"
|
||||
ref="refFile"
|
||||
style="display: none"
|
||||
class="hidden"
|
||||
accept=".json"
|
||||
@change="importLocalFile"
|
||||
/>
|
||||
|
@ -226,7 +226,7 @@ onMounted(() => {
|
|||
<Button :plain="true" @click="zoomOut()">
|
||||
<IconifyIcon icon="lucide:zoom-out" />
|
||||
</Button>
|
||||
<Button class="w-80px"> {{ scaleValue }}% </Button>
|
||||
<Button class="w-20"> {{ scaleValue }}% </Button>
|
||||
<Button :plain="true" @click="zoomIn()">
|
||||
<IconifyIcon icon="lucide:zoom-in" />
|
||||
</Button>
|
||||
|
@ -258,7 +258,7 @@ onMounted(() => {
|
|||
>
|
||||
<div class="mb-2">以下节点内容不完善,请修改后保存</div>
|
||||
<div
|
||||
class="b-rounded-1 line-height-normal mb-3 bg-gray-100 p-2"
|
||||
class="line-height-normal mb-3 rounded bg-gray-100 p-2"
|
||||
v-for="(item, index) in errorNodes"
|
||||
:key="index"
|
||||
>
|
||||
|
|
|
@ -16,14 +16,14 @@ defineProps<SummaryCardProps>();
|
|||
class="flex flex-row items-center gap-3 rounded bg-[var(--el-bg-color-overlay)] p-4"
|
||||
>
|
||||
<div
|
||||
class="rounded-1 flex h-12 w-12 flex-shrink-0 items-center justify-center"
|
||||
class="flex h-12 w-12 flex-shrink-0 items-center justify-center rounded"
|
||||
:class="`${iconColor} ${iconBgColor}`"
|
||||
>
|
||||
<IconifyIcon v-if="icon" :icon="icon" class="!text-6" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-1">
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="text-3.5">{{ title }}</span>
|
||||
<span class="text-base">{{ title }}</span>
|
||||
<Tooltip :content="tooltip" placement="topLeft" v-if="tooltip">
|
||||
<IconifyIcon
|
||||
icon="lucide:circle-alert"
|
||||
|
@ -32,7 +32,7 @@ defineProps<SummaryCardProps>();
|
|||
</Tooltip>
|
||||
</div>
|
||||
<div class="flex flex-row items-baseline gap-2">
|
||||
<div class="text-7">
|
||||
<div class="text-lg">
|
||||
<CountTo
|
||||
:prefix="prefix"
|
||||
:end-val="value ?? 0"
|
||||
|
@ -48,7 +48,7 @@ defineProps<SummaryCardProps>();
|
|||
:icon="
|
||||
Number(percent) > 0 ? 'lucide:chevron-up' : 'lucide:chevron-down'
|
||||
"
|
||||
class="!text-3 ml-0.5"
|
||||
class="ml-0.5 !text-sm"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -3,7 +3,7 @@ export const ACTION_ICON = {
|
|||
UPLOAD: 'lucide:upload',
|
||||
ADD: 'lucide:plus',
|
||||
EDIT: 'lucide:edit',
|
||||
DELETE: 'lucide:trash',
|
||||
DELETE: 'lucide:trash-2',
|
||||
REFRESH: 'lucide:refresh-cw',
|
||||
SEARCH: 'lucide:search',
|
||||
FILTER: 'lucide:filter',
|
||||
|
|
|
@ -5,7 +5,7 @@ import type { VxeToolbarInstance } from '#/adapter/vxe-table';
|
|||
import { ref } from 'vue';
|
||||
|
||||
import { useContentMaximize, useRefresh } from '@vben/hooks';
|
||||
import { Expand, MsRefresh, Search, TMinimize } from '@vben/icons';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { Button, Tooltip } from 'ant-design-vue';
|
||||
|
||||
|
@ -41,37 +41,39 @@ defineExpose({
|
|||
<slot></slot>
|
||||
<Tooltip placement="bottom">
|
||||
<template #title>
|
||||
<div class="max-w-[200px]">搜索</div>
|
||||
<div class="max-w-52">搜索</div>
|
||||
</template>
|
||||
<Button
|
||||
class="ml-2 font-[8px]"
|
||||
class="ml-2 font-normal"
|
||||
shape="circle"
|
||||
@click="onHiddenSearchBar"
|
||||
>
|
||||
<Search :size="15" />
|
||||
<IconifyIcon icon="lucide:search" :size="15" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip placement="bottom">
|
||||
<template #title>
|
||||
<div class="max-w-[200px]">刷新</div>
|
||||
<div class="max-w-52">刷新</div>
|
||||
</template>
|
||||
<Button class="ml-2 font-[8px]" shape="circle" @click="refresh">
|
||||
<MsRefresh :size="15" />
|
||||
<Button class="ml-2 font-medium" shape="circle" @click="refresh">
|
||||
<IconifyIcon icon="lucide:refresh-cw" :size="15" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip placement="bottom">
|
||||
<template #title>
|
||||
<div class="max-w-[200px]">
|
||||
<div class="max-w-52">
|
||||
{{ contentIsMaximize ? '还原' : '全屏' }}
|
||||
</div>
|
||||
</template>
|
||||
<Button
|
||||
class="ml-2 font-[8px]"
|
||||
class="ml-2 font-medium"
|
||||
shape="circle"
|
||||
@click="toggleMaximizeAndTabbarHidden"
|
||||
>
|
||||
<Expand v-if="!contentIsMaximize" :size="15" />
|
||||
<TMinimize v-else :size="15" />
|
||||
<IconifyIcon
|
||||
:icon="contentIsMaximize ? 'lucide:minimize' : 'lucide:maximize'"
|
||||
:size="15"
|
||||
/>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</template>
|
||||
|
|
|
@ -8,7 +8,7 @@ import type { AxiosProgressEvent } from '#/api/infra/file';
|
|||
|
||||
import { ref, toRefs, watch } from 'vue';
|
||||
|
||||
import { CloudUpload } from '@vben/icons';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
import { $t } from '@vben/locales';
|
||||
import { isFunction, isObject, isString } from '@vben/utils';
|
||||
|
||||
|
@ -214,13 +214,13 @@ function getValue() {
|
|||
v-if="fileList && fileList.length < maxNumber"
|
||||
class="flex flex-col items-center justify-center"
|
||||
>
|
||||
<CloudUpload />
|
||||
<IconifyIcon icon="lucide:cloud-upload" />
|
||||
<div class="mt-2">{{ $t('ui.upload.imgUpload') }}</div>
|
||||
</div>
|
||||
</Upload>
|
||||
<div
|
||||
v-if="showDescription"
|
||||
class="mt-2 flex flex-wrap items-center text-[14px]"
|
||||
class="mt-2 flex flex-wrap items-center text-sm"
|
||||
>
|
||||
请上传不超过
|
||||
<div class="text-primary mx-1 font-bold">{{ maxSize }}MB</div>
|
||||
|
|
|
@ -17,7 +17,7 @@ const [Modal, modalApi] = useVbenModal({
|
|||
});
|
||||
</script>
|
||||
<template>
|
||||
<Modal class="w-[40%]" :title="$t('ui.widgets.qa')">
|
||||
<Modal class="w-2/5" :title="$t('ui.widgets.qa')">
|
||||
<div class="mt-2 flex flex-col">
|
||||
<div class="mt-2 flex flex-row">
|
||||
<VbenButtonGroup class="basis-1/3" :gap="2" border size="large">
|
||||
|
|
|
@ -21,10 +21,11 @@ const accessStore = useAccessStore();
|
|||
|
||||
const tenantEnable = isTenantEnable();
|
||||
|
||||
const value = ref<number>(accessStore.visitTenantId ?? undefined); // 当前访问的租户 ID
|
||||
const tenants = ref<SystemTenantApi.Tenant[]>([]); // 租户列表
|
||||
// 当前访问的租户 ID
|
||||
const value = ref<number | undefined>(accessStore.visitTenantId ?? undefined);
|
||||
// 租户列表
|
||||
const tenants = ref<SystemTenantApi.Tenant[]>([]);
|
||||
|
||||
// TODO @xingyu:这个有可能 3 端复用么?
|
||||
async function handleChange(id: SelectValue) {
|
||||
// 设置访问租户 ID
|
||||
accessStore.setVisitTenantId(id as number);
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
path: '/ai',
|
||||
name: 'Ai',
|
||||
meta: {
|
||||
title: 'Ai',
|
||||
hideInMenu: true,
|
||||
},
|
||||
children: [
|
||||
{
|
||||
path: 'image/square',
|
||||
component: () => import('#/views/ai/image/square/index.vue'),
|
||||
name: 'AiImageSquare',
|
||||
meta: {
|
||||
noCache: true,
|
||||
hidden: true,
|
||||
canTo: true,
|
||||
title: '绘图作品',
|
||||
activePath: '/ai/image',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'knowledge/document',
|
||||
component: () => import('#/views/ai/knowledge/document/index.vue'),
|
||||
name: 'AiKnowledgeDocument',
|
||||
meta: {
|
||||
noCache: true,
|
||||
hidden: true,
|
||||
canTo: true,
|
||||
title: '知识库文档',
|
||||
activePath: '/ai/knowledge',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'knowledge/document/create',
|
||||
component: () => import('#/views/ai/knowledge/document/form/index.vue'),
|
||||
name: 'AiKnowledgeDocumentCreate',
|
||||
meta: {
|
||||
noCache: true,
|
||||
hidden: true,
|
||||
canTo: true,
|
||||
title: '创建文档',
|
||||
activePath: '/ai/knowledge',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'knowledge/document/update',
|
||||
component: () => import('#/views/ai/knowledge/document/form/index.vue'),
|
||||
name: 'AiKnowledgeDocumentUpdate',
|
||||
meta: {
|
||||
noCache: true,
|
||||
hidden: true,
|
||||
canTo: true,
|
||||
title: '修改文档',
|
||||
activePath: '/ai/knowledge',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'knowledge/retrieval',
|
||||
component: () =>
|
||||
import('#/views/ai/knowledge/knowledge/retrieval/index.vue'),
|
||||
name: 'AiKnowledgeRetrieval',
|
||||
meta: {
|
||||
noCache: true,
|
||||
hidden: true,
|
||||
canTo: true,
|
||||
title: '文档召回测试',
|
||||
activePath: '/ai/knowledge',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'knowledge/segment',
|
||||
component: () => import('#/views/ai/knowledge/segment/index.vue'),
|
||||
name: 'AiKnowledgeSegment',
|
||||
meta: {
|
||||
noCache: true,
|
||||
hidden: true,
|
||||
canTo: true,
|
||||
title: '知识库分段',
|
||||
activePath: '/ai/knowledge',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'console/workflow/create',
|
||||
component: () => import('#/views/ai/workflow/form/index.vue'),
|
||||
name: 'AiWorkflowCreate',
|
||||
meta: {
|
||||
noCache: true,
|
||||
hidden: true,
|
||||
canTo: true,
|
||||
title: '设计 AI 工作流',
|
||||
activePath: '/ai/workflow',
|
||||
},
|
||||
},
|
||||
{
|
||||
path: 'console/workflow/:type/:id',
|
||||
component: () => import('#/views/ai/workflow/form/index.vue'),
|
||||
name: 'AiWorkflowUpdate',
|
||||
meta: {
|
||||
noCache: true,
|
||||
hidden: true,
|
||||
canTo: true,
|
||||
title: '设计 AI 工作流',
|
||||
activePath: '/ai/workflow',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
|
@ -5,6 +5,244 @@
|
|||
* 枚举类
|
||||
*/
|
||||
|
||||
/**
|
||||
* AI 平台的枚举
|
||||
*/
|
||||
export const AiPlatformEnum = {
|
||||
TONG_YI: 'TongYi', // 阿里
|
||||
YI_YAN: 'YiYan', // 百度
|
||||
DEEP_SEEK: 'DeepSeek', // DeepSeek
|
||||
ZHI_PU: 'ZhiPu', // 智谱 AI
|
||||
XING_HUO: 'XingHuo', // 讯飞
|
||||
SiliconFlow: 'SiliconFlow', // 硅基流动
|
||||
OPENAI: 'OpenAI',
|
||||
Ollama: 'Ollama',
|
||||
STABLE_DIFFUSION: 'StableDiffusion', // Stability AI
|
||||
MIDJOURNEY: 'Midjourney', // Midjourney
|
||||
SUNO: 'Suno', // Suno AI
|
||||
};
|
||||
|
||||
export const AiModelTypeEnum = {
|
||||
CHAT: 1, // 聊天
|
||||
IMAGE: 2, // 图像
|
||||
VOICE: 3, // 音频
|
||||
VIDEO: 4, // 视频
|
||||
EMBEDDING: 5, // 向量
|
||||
RERANK: 6, // 重排
|
||||
};
|
||||
export interface ImageModelVO {
|
||||
key: string;
|
||||
name: string;
|
||||
image?: string;
|
||||
}
|
||||
export const OtherPlatformEnum: ImageModelVO[] = [
|
||||
{
|
||||
key: AiPlatformEnum.TONG_YI,
|
||||
name: '通义万相',
|
||||
},
|
||||
{
|
||||
key: AiPlatformEnum.YI_YAN,
|
||||
name: '百度千帆',
|
||||
},
|
||||
{
|
||||
key: AiPlatformEnum.ZHI_PU,
|
||||
name: '智谱 AI',
|
||||
},
|
||||
{
|
||||
key: AiPlatformEnum.SiliconFlow,
|
||||
name: '硅基流动',
|
||||
},
|
||||
];
|
||||
/**
|
||||
* 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, // 回复
|
||||
}
|
||||
|
||||
// ========== 【图片 UI】相关的枚举 ==========
|
||||
|
||||
export const ImageHotWords = [
|
||||
'中国旗袍',
|
||||
'古装美女',
|
||||
'卡通头像',
|
||||
'机甲战士',
|
||||
'童话小屋',
|
||||
'中国长城',
|
||||
]; // 图片热词
|
||||
|
||||
export const ImageHotEnglishWords = [
|
||||
'Chinese Cheongsam',
|
||||
'Ancient Beauty',
|
||||
'Cartoon Avatar',
|
||||
'Mech Warrior',
|
||||
'Fairy Tale Cottage',
|
||||
'The Great Wall of China',
|
||||
]; // 图片热词(英文)
|
||||
|
||||
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 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',
|
||||
},
|
||||
];
|
||||
// ========== COMMON 模块 ==========
|
||||
// 全局通用状态枚举
|
||||
export const CommonStatusEnum = {
|
||||
|
@ -92,7 +330,136 @@ export const InfraApiErrorLogProcessStatusEnum = {
|
|||
DONE: 1, // 已处理
|
||||
IGNORE: 2, // 已忽略
|
||||
};
|
||||
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 Dall3Models: ImageModelVO[] = [
|
||||
{
|
||||
key: 'dall-e-3',
|
||||
name: 'DALL·E 3',
|
||||
image: `/static/imgs/ai/dall2.jpg`,
|
||||
},
|
||||
{
|
||||
key: 'dall-e-2',
|
||||
name: 'DALL·E 2',
|
||||
image: `/static/imgs/ai/dall3.jpg`,
|
||||
},
|
||||
];
|
||||
|
||||
export const Dall3StyleList: ImageModelVO[] = [
|
||||
{
|
||||
key: 'vivid',
|
||||
name: '清晰',
|
||||
image: `/static/imgs/ai/qingxi.jpg`,
|
||||
},
|
||||
{
|
||||
key: 'natural',
|
||||
name: '自然',
|
||||
image: `/static/imgs/ai/ziran.jpg`,
|
||||
},
|
||||
];
|
||||
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 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',
|
||||
},
|
||||
];
|
||||
|
||||
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;',
|
||||
},
|
||||
];
|
||||
// ========== PAY 模块 ==========
|
||||
/**
|
||||
* 支付渠道枚举
|
||||
|
@ -743,3 +1110,81 @@ export enum ProcessVariableEnum {
|
|||
*/
|
||||
START_USER_ID = 'PROCESS_START_USER_ID',
|
||||
}
|
||||
// ========== 【写作 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`;
|
||||
|
|
|
@ -143,10 +143,10 @@ export const getBoolDictOptions = (dictType: string) => {
|
|||
enum DICT_TYPE {
|
||||
AI_GENERATE_MODE = 'ai_generate_mode', // AI 生成模式
|
||||
AI_IMAGE_STATUS = 'ai_image_status', // AI 图片状态
|
||||
AI_MODEL_TYPE = 'ai_model_type', // AI 模型类型
|
||||
AI_MUSIC_STATUS = 'ai_music_status', // AI 音乐状态
|
||||
// ========== AI - 人工智能模块 ==========
|
||||
AI_PLATFORM = 'ai_platform', // AI 平台
|
||||
|
||||
AI_WRITE_FORMAT = 'ai_write_format', // AI 写作格式
|
||||
AI_WRITE_LANGUAGE = 'ai_write_language', // AI 写作语言
|
||||
AI_WRITE_LENGTH = 'ai_write_length', // AI 写作长度
|
||||
|
|
|
@ -1,215 +0,0 @@
|
|||
/**
|
||||
* 下载工具模块
|
||||
* 提供多种文件格式的下载功能
|
||||
*/
|
||||
// TODO @ziye:请使用 @vben/utils/download 代替 packages/@core/base/shared/src/utils/download.ts
|
||||
|
||||
/**
|
||||
* 图片下载配置接口
|
||||
*/
|
||||
interface ImageDownloadOptions {
|
||||
/** 图片 URL */
|
||||
url: string;
|
||||
/** 指定画布宽度 */
|
||||
canvasWidth?: number;
|
||||
/** 指定画布高度 */
|
||||
canvasHeight?: number;
|
||||
/** 将图片绘制在画布上时带上图片的宽高值,默认为 true */
|
||||
drawWithImageSize?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 基础文件下载函数
|
||||
* @param data - 文件数据 Blob
|
||||
* @param fileName - 文件名
|
||||
* @param mimeType - MIME 类型
|
||||
*/
|
||||
export const download0 = (data: Blob, fileName: string, mimeType: string) => {
|
||||
try {
|
||||
// 创建 blob
|
||||
const blob = new Blob([data], { type: mimeType });
|
||||
// 创建 href 超链接,点击进行下载
|
||||
window.URL = window.URL || window.webkitURL;
|
||||
const href = URL.createObjectURL(blob);
|
||||
const downA = document.createElement('a');
|
||||
downA.href = href;
|
||||
downA.download = fileName;
|
||||
downA.click();
|
||||
// 销毁超链接
|
||||
window.URL.revokeObjectURL(href);
|
||||
} catch (error) {
|
||||
console.error('文件下载失败:', error);
|
||||
throw new Error(
|
||||
`文件下载失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 触发文件下载的通用方法
|
||||
* @param url - 下载链接
|
||||
* @param fileName - 文件名
|
||||
*/
|
||||
const triggerDownload = (url: string, fileName: string) => {
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = fileName;
|
||||
a.click();
|
||||
};
|
||||
|
||||
export const download = {
|
||||
/**
|
||||
* 下载 Excel 文件
|
||||
* @param data - 文件数据 Blob
|
||||
* @param fileName - 文件名
|
||||
*/
|
||||
excel: (data: Blob, fileName: string) => {
|
||||
download0(data, fileName, 'application/vnd.ms-excel');
|
||||
},
|
||||
|
||||
/**
|
||||
* 下载 Word 文件
|
||||
* @param data - 文件数据 Blob
|
||||
* @param fileName - 文件名
|
||||
*/
|
||||
word: (data: Blob, fileName: string) => {
|
||||
download0(data, fileName, 'application/msword');
|
||||
},
|
||||
|
||||
/**
|
||||
* 下载 Zip 文件
|
||||
* @param data - 文件数据 Blob
|
||||
* @param fileName - 文件名
|
||||
*/
|
||||
zip: (data: Blob, fileName: string) => {
|
||||
download0(data, fileName, 'application/zip');
|
||||
},
|
||||
|
||||
/**
|
||||
* 下载 HTML 文件
|
||||
* @param data - 文件数据 Blob
|
||||
* @param fileName - 文件名
|
||||
*/
|
||||
html: (data: Blob, fileName: string) => {
|
||||
download0(data, fileName, 'text/html');
|
||||
},
|
||||
|
||||
/**
|
||||
* 下载 Markdown 文件
|
||||
* @param data - 文件数据 Blob
|
||||
* @param fileName - 文件名
|
||||
*/
|
||||
markdown: (data: Blob, fileName: string) => {
|
||||
download0(data, fileName, 'text/markdown');
|
||||
},
|
||||
|
||||
/**
|
||||
* 下载 JSON 文件
|
||||
* @param data - 文件数据 Blob
|
||||
* @param fileName - 文件名
|
||||
*/
|
||||
json: (data: Blob, fileName: string) => {
|
||||
download0(data, fileName, 'application/json');
|
||||
},
|
||||
|
||||
/**
|
||||
* 下载图片(允许跨域)
|
||||
* @param options - 图片下载配置
|
||||
*/
|
||||
image: (options: ImageDownloadOptions) => {
|
||||
const {
|
||||
url,
|
||||
canvasWidth,
|
||||
canvasHeight,
|
||||
drawWithImageSize = true,
|
||||
} = options;
|
||||
|
||||
const image = new Image();
|
||||
// image.setAttribute('crossOrigin', 'anonymous')
|
||||
image.src = url;
|
||||
image.addEventListener('load', () => {
|
||||
try {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = canvasWidth || image.width;
|
||||
canvas.height = canvasHeight || image.height;
|
||||
const ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
|
||||
ctx?.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
if (drawWithImageSize) {
|
||||
ctx.drawImage(image, 0, 0, image.width, image.height);
|
||||
} else {
|
||||
ctx.drawImage(image, 0, 0);
|
||||
}
|
||||
|
||||
const dataUrl = canvas.toDataURL('image/png');
|
||||
triggerDownload(dataUrl, 'image.png');
|
||||
} catch (error) {
|
||||
console.error('图片下载失败:', error);
|
||||
throw new Error(
|
||||
`图片下载失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
image.addEventListener('error', () => {
|
||||
throw new Error('图片加载失败');
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 将 Base64 字符串转换为文件对象
|
||||
* @param base64 - Base64 字符串
|
||||
* @param fileName - 文件名
|
||||
* @returns File 对象
|
||||
*/
|
||||
base64ToFile: (base64: string, fileName: string): File => {
|
||||
// 输入验证
|
||||
if (!base64 || typeof base64 !== 'string') {
|
||||
throw new Error('base64 参数必须是非空字符串');
|
||||
}
|
||||
|
||||
// 将 base64 按照逗号进行分割,将前缀与后续内容分隔开
|
||||
const data = base64.split(',');
|
||||
if (data.length !== 2 || !data[0] || !data[1]) {
|
||||
throw new Error('无效的 base64 格式');
|
||||
}
|
||||
|
||||
// 利用正则表达式从前缀中获取类型信息(image/png、image/jpeg、image/webp等)
|
||||
const typeMatch = data[0].match(/:(.*?);/);
|
||||
if (!typeMatch || !typeMatch[1]) {
|
||||
throw new Error('无法解析 base64 类型信息');
|
||||
}
|
||||
const type = typeMatch[1];
|
||||
|
||||
// 从类型信息中获取具体的文件格式后缀(png、jpeg、webp)
|
||||
const typeParts = type.split('/');
|
||||
if (typeParts.length !== 2 || !typeParts[1]) {
|
||||
throw new Error('无效的 MIME 类型格式');
|
||||
}
|
||||
const suffix = typeParts[1];
|
||||
|
||||
try {
|
||||
// 使用 atob() 对 base64 数据进行解码,结果是一个文件数据流以字符串的格式输出
|
||||
const bstr = window.atob(data[1]);
|
||||
|
||||
// 获取解码结果字符串的长度
|
||||
const n = bstr.length;
|
||||
// 根据解码结果字符串的长度创建一个等长的整型数字数组
|
||||
const u8arr = new Uint8Array(n);
|
||||
|
||||
// 优化的 Uint8Array 填充逻辑
|
||||
for (let i = 0; i < n; i++) {
|
||||
// 使用 charCodeAt() 获取字符对应的字节值(Base64 解码后的字符串是字节级别的)
|
||||
// eslint-disable-next-line unicorn/prefer-code-point
|
||||
u8arr[i] = bstr.charCodeAt(i);
|
||||
}
|
||||
|
||||
// 返回 File 文件对象
|
||||
return new File([u8arr], `${fileName}.${suffix}`, { type });
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Base64 解码失败: ${error instanceof Error ? error.message : '未知错误'}`,
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
|
@ -1,6 +1,5 @@
|
|||
export * from './constants';
|
||||
export * from './dict';
|
||||
export * from './download';
|
||||
export * from './formCreate';
|
||||
export * from './rangePickerProps';
|
||||
export * from './routerHelper';
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { defineAsyncComponent } from 'vue';
|
||||
|
||||
const modules = import.meta.glob('../views/**/*.{vue,tsx}');
|
||||
// TODO @xingyu:这个要不要融合到哪个 router util 里? utils 里面没有引入 vue 使用不了 defineAsyncComponent
|
||||
|
||||
/**
|
||||
* 注册一个异步组件
|
||||
* @param componentPath 例:/bpm/oa/leave/detail
|
||||
|
|
|
@ -101,7 +101,7 @@ watch(
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mt-16px md:w-full lg:w-1/2 2xl:w-2/5">
|
||||
<div class="mt-4 md:w-full lg:w-1/2 2xl:w-2/5">
|
||||
<Form />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -88,7 +88,7 @@ async function handleSubmit(values: Recordable<any>) {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="mt-[16px] md:w-full lg:w-1/2 2xl:w-2/5">
|
||||
<div class="mt-4 md:w-full lg:w-1/2 2xl:w-2/5">
|
||||
<Form />
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -181,9 +181,7 @@ onMounted(() => {
|
|||
/>
|
||||
<div class="flex flex-1 items-center justify-between">
|
||||
<div class="flex flex-col">
|
||||
<h4
|
||||
class="mb-[4px] text-[14px] text-black/85 dark:text-white/85"
|
||||
>
|
||||
<h4 class="mb-1 text-sm text-black/85 dark:text-white/85">
|
||||
{{ getDictLabel(DICT_TYPE.SYSTEM_SOCIAL_TYPE, item.type) }}
|
||||
</h4>
|
||||
<span class="text-black/45 dark:text-white/45">
|
||||
|
@ -191,9 +189,9 @@ onMounted(() => {
|
|||
{{ item.socialUser?.nickname || item.socialUser?.openid }}
|
||||
</template>
|
||||
<template v-else>
|
||||
绑定{{
|
||||
getDictLabel(DICT_TYPE.SYSTEM_SOCIAL_TYPE, item.type)
|
||||
}}账号
|
||||
绑定
|
||||
{{ getDictLabel(DICT_TYPE.SYSTEM_SOCIAL_TYPE, item.type) }}
|
||||
账号
|
||||
</template>
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,445 @@
|
|||
<script setup lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
import type { AiChatConversationApi } from '#/api/ai/chat/conversation';
|
||||
|
||||
import { h, onMounted, ref, toRefs, watch } from 'vue';
|
||||
|
||||
import { confirm, prompt, useVbenDrawer } from '@vben/common-ui';
|
||||
import { IconifyIcon, SvgGptIcon } from '@vben/icons';
|
||||
|
||||
import { Avatar, Button, Empty, Input, Layout, message } from 'ant-design-vue';
|
||||
|
||||
import {
|
||||
createChatConversationMy,
|
||||
deleteChatConversationMy,
|
||||
deleteChatConversationMyByUnpinned,
|
||||
getChatConversationMyList,
|
||||
updateChatConversationMy,
|
||||
} from '#/api/ai/chat/conversation';
|
||||
|
||||
import RoleRepository from '../role/RoleRepository.vue';
|
||||
|
||||
// 定义组件 props
|
||||
const props = defineProps({
|
||||
activeId: {
|
||||
type: [Number, null] as PropType<null | number>,
|
||||
default: null,
|
||||
},
|
||||
});
|
||||
|
||||
// 定义钩子
|
||||
const emits = defineEmits([
|
||||
'onConversationCreate',
|
||||
'onConversationClick',
|
||||
'onConversationClear',
|
||||
'onConversationDelete',
|
||||
]);
|
||||
|
||||
const [Drawer, drawerApi] = useVbenDrawer({
|
||||
connectedComponent: RoleRepository,
|
||||
});
|
||||
|
||||
// 定义属性
|
||||
const searchName = ref<string>(''); // 对话搜索
|
||||
const activeConversationId = ref<null | number>(null); // 选中的对话,默认为 null
|
||||
const hoverConversationId = ref<null | number>(null); // 悬浮上去的对话
|
||||
const conversationList = ref([] as AiChatConversationApi.ChatConversationVO[]); // 对话列表
|
||||
const conversationMap = ref<any>({}); // 对话分组 (置顶、今天、三天前、一星期前、一个月前)
|
||||
const loading = ref<boolean>(false); // 加载中
|
||||
const loadingTime = ref<any>();
|
||||
|
||||
/** 搜索对话 */
|
||||
async function searchConversation() {
|
||||
// 恢复数据
|
||||
if (searchName.value.trim().length === 0) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/** 点击对话 */
|
||||
async function handleConversationClick(id: number) {
|
||||
// 过滤出选中的对话
|
||||
const filterConversation = conversationList.value.find((item) => {
|
||||
return item.id === id;
|
||||
});
|
||||
// 回调 onConversationClick
|
||||
// noinspection JSVoidFunctionReturnValueUsed
|
||||
const success = emits('onConversationClick', filterConversation) as any;
|
||||
// 切换对话
|
||||
if (success) {
|
||||
activeConversationId.value = id;
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取对话列表 */
|
||||
async function getChatConversationList() {
|
||||
try {
|
||||
// 加载中
|
||||
loadingTime.value = setTimeout(() => {
|
||||
loading.value = true;
|
||||
}, 50);
|
||||
|
||||
// 1.1 获取 对话数据
|
||||
conversationList.value = await getChatConversationMyList();
|
||||
// 1.2 排序
|
||||
conversationList.value.sort((a, b) => {
|
||||
return Number(b.createTime) - Number(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 创建时间,进行分组 */
|
||||
async function getConversationGroupByCreateTime(
|
||||
list: AiChatConversationApi.ChatConversationVO[],
|
||||
) {
|
||||
// 排序、指定、时间分组(今天、一天前、三天前、七天前、30天前)
|
||||
// noinspection NonAsciiCharacters
|
||||
const groupMap: any = {
|
||||
置顶: [],
|
||||
今天: [],
|
||||
一天前: [],
|
||||
三天前: [],
|
||||
七天前: [],
|
||||
三十天前: [],
|
||||
};
|
||||
// 当前时间的时间戳
|
||||
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 - Number(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;
|
||||
}
|
||||
|
||||
async function createConversation() {
|
||||
// 1. 新建对话
|
||||
const conversationId = await createChatConversationMy(
|
||||
{} as unknown as AiChatConversationApi.ChatConversationVO,
|
||||
);
|
||||
// 2. 获取对话内容
|
||||
await getChatConversationList();
|
||||
// 3. 选中对话
|
||||
await handleConversationClick(conversationId);
|
||||
// 4. 回调
|
||||
emits('onConversationCreate');
|
||||
}
|
||||
|
||||
/** 修改对话的标题 */
|
||||
async function updateConversationTitle(
|
||||
conversation: AiChatConversationApi.ChatConversationVO,
|
||||
) {
|
||||
// 1. 二次确认
|
||||
prompt({
|
||||
async beforeClose(scope) {
|
||||
if (scope.isConfirm) {
|
||||
if (scope.value) {
|
||||
try {
|
||||
// 2. 发起修改
|
||||
await updateChatConversationMy({
|
||||
id: conversation.id,
|
||||
title: scope.value,
|
||||
} as AiChatConversationApi.ChatConversationVO);
|
||||
message.success('重命名成功');
|
||||
// 3. 刷新列表
|
||||
await getChatConversationList();
|
||||
// 4. 过滤当前切换的
|
||||
const filterConversationList = conversationList.value.filter(
|
||||
(item) => {
|
||||
return item.id === conversation.id;
|
||||
},
|
||||
);
|
||||
if (
|
||||
filterConversationList.length > 0 &&
|
||||
filterConversationList[0] && // tip:避免切换对话
|
||||
activeConversationId.value === filterConversationList[0].id
|
||||
) {
|
||||
emits('onConversationClick', filterConversationList[0]);
|
||||
}
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
message.error('请输入标题');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
},
|
||||
component: () => {
|
||||
return h(Input, {
|
||||
placeholder: '请输入标题',
|
||||
allowClear: true,
|
||||
defaultValue: conversation.title,
|
||||
rules: [{ required: true, message: '请输入标题' }],
|
||||
});
|
||||
},
|
||||
content: '请输入标题',
|
||||
title: '修改标题',
|
||||
modelPropName: 'value',
|
||||
});
|
||||
}
|
||||
|
||||
/** 删除聊天对话 */
|
||||
async function deleteChatConversation(
|
||||
conversation: AiChatConversationApi.ChatConversationVO,
|
||||
) {
|
||||
try {
|
||||
// 删除的二次确认
|
||||
await confirm(`是否确认删除对话 - ${conversation.title}?`);
|
||||
// 发起删除
|
||||
await deleteChatConversationMy(conversation.id);
|
||||
message.success('对话已删除');
|
||||
// 刷新列表
|
||||
await getChatConversationList();
|
||||
// 回调
|
||||
emits('onConversationDelete', conversation);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
async function handleClearConversation() {
|
||||
try {
|
||||
await confirm('确认后对话会全部清空,置顶的对话除外。');
|
||||
await deleteChatConversationMyByUnpinned();
|
||||
message.success('操作成功!');
|
||||
// 清空 对话 和 对话内容
|
||||
activeConversationId.value = null;
|
||||
// 获取 对话列表
|
||||
await getChatConversationList();
|
||||
// 回调 方法
|
||||
emits('onConversationClear');
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 对话置顶 */
|
||||
async function handleTop(
|
||||
conversation: AiChatConversationApi.ChatConversationVO,
|
||||
) {
|
||||
// 更新对话置顶
|
||||
conversation.pinned = !conversation.pinned;
|
||||
await updateChatConversationMy(conversation);
|
||||
// 刷新对话
|
||||
await getChatConversationList();
|
||||
}
|
||||
|
||||
// ============ 角色仓库 ============
|
||||
|
||||
/** 角色仓库抽屉 */
|
||||
const handleRoleRepository = async () => {
|
||||
drawerApi.open();
|
||||
};
|
||||
|
||||
/** 监听选中的对话 */
|
||||
const { activeId } = toRefs(props);
|
||||
watch(activeId, async (newValue) => {
|
||||
activeConversationId.value = newValue;
|
||||
});
|
||||
|
||||
// 定义 public 方法
|
||||
defineExpose({ createConversation });
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
// 获取 对话列表
|
||||
await getChatConversationList();
|
||||
// 默认选中
|
||||
if (props.activeId) {
|
||||
activeConversationId.value = props.activeId;
|
||||
} else {
|
||||
// 首次默认选中第一个
|
||||
if (conversationList.value.length > 0 && conversationList.value[0]) {
|
||||
activeConversationId.value = conversationList.value[0].id;
|
||||
// 回调 onConversationClick
|
||||
await emits('onConversationClick', conversationList.value[0]);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Layout.Sider
|
||||
width="260px"
|
||||
class="!bg-primary-foreground conversation-container relative flex h-full flex-col justify-between overflow-hidden py-2.5 pb-0 pt-2.5"
|
||||
>
|
||||
<Drawer />
|
||||
<!-- 左顶部:对话 -->
|
||||
<div class="flex h-full flex-col">
|
||||
<Button
|
||||
class="btn-new-conversation h-9 w-full"
|
||||
type="primary"
|
||||
@click="createConversation"
|
||||
>
|
||||
<IconifyIcon icon="lucide:plus" class="mr-1" />
|
||||
新建对话
|
||||
</Button>
|
||||
|
||||
<Input
|
||||
v-model:value="searchName"
|
||||
size="large"
|
||||
class="search-input mt-5"
|
||||
placeholder="搜索历史记录"
|
||||
@keyup="searchConversation"
|
||||
>
|
||||
<template #prefix>
|
||||
<IconifyIcon icon="lucide:search" />
|
||||
</template>
|
||||
</Input>
|
||||
|
||||
<!-- 左中间:对话列表 -->
|
||||
<div class="conversation-list mt-2.5 flex-1 overflow-auto">
|
||||
<!-- 情况一:加载中 -->
|
||||
<Empty v-if="loading" description="." v-loading="loading" />
|
||||
|
||||
<!-- 情况二:按照 group 分组 -->
|
||||
<div
|
||||
v-for="conversationKey in Object.keys(conversationMap)"
|
||||
:key="conversationKey"
|
||||
class=""
|
||||
>
|
||||
<div
|
||||
v-if="conversationMap[conversationKey].length > 0"
|
||||
class="conversation-item classify-title pt-2.5"
|
||||
>
|
||||
<b class="mx-1">
|
||||
{{ conversationKey }}
|
||||
</b>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="conversation in conversationMap[conversationKey]"
|
||||
:key="conversation.id"
|
||||
@click="handleConversationClick(conversation.id)"
|
||||
@mouseover="hoverConversationId = conversation.id"
|
||||
@mouseout="hoverConversationId = null"
|
||||
class="conversation-item mt-1"
|
||||
>
|
||||
<div
|
||||
class="conversation flex cursor-pointer flex-row items-center justify-between rounded-lg px-2.5 leading-10"
|
||||
:class="[
|
||||
conversation.id === activeConversationId ? 'bg-gray-100' : '',
|
||||
]"
|
||||
>
|
||||
<div class="title-wrapper flex items-center">
|
||||
<Avatar
|
||||
v-if="conversation.roleAvatar"
|
||||
:src="conversation.roleAvatar"
|
||||
/>
|
||||
<SvgGptIcon v-else class="size-8" />
|
||||
<span
|
||||
class="max-w-36 overflow-hidden text-ellipsis whitespace-nowrap px-2.5 py-1 text-sm font-normal text-gray-600"
|
||||
>
|
||||
{{ conversation.title }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-show="hoverConversationId === conversation.id"
|
||||
class="button-wrapper relative right-0.5 flex items-center text-gray-400"
|
||||
>
|
||||
<Button
|
||||
class="mr-0 px-1"
|
||||
type="link"
|
||||
@click.stop="handleTop(conversation)"
|
||||
>
|
||||
<IconifyIcon
|
||||
v-if="!conversation.pinned"
|
||||
icon="lucide:arrow-up-to-line"
|
||||
/>
|
||||
<IconifyIcon
|
||||
v-if="conversation.pinned"
|
||||
icon="lucide:arrow-down-from-line"
|
||||
/>
|
||||
</Button>
|
||||
<Button
|
||||
class="mr-0 px-1"
|
||||
type="link"
|
||||
@click.stop="updateConversationTitle(conversation)"
|
||||
>
|
||||
<IconifyIcon icon="lucide:edit" />
|
||||
</Button>
|
||||
<Button
|
||||
class="mr-0 px-1"
|
||||
type="link"
|
||||
@click.stop="deleteChatConversation(conversation)"
|
||||
>
|
||||
<IconifyIcon icon="lucide:trash-2" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部占位 -->
|
||||
<div class="h-12 w-full"></div>
|
||||
</div>
|
||||
|
||||
<!-- 左底部:工具栏 -->
|
||||
<div
|
||||
class="tool-box absolute bottom-0 left-0 right-0 flex items-center justify-between bg-gray-50 px-5 leading-9 text-gray-400 shadow-sm"
|
||||
>
|
||||
<div
|
||||
class="flex cursor-pointer items-center text-gray-400"
|
||||
@click="handleRoleRepository"
|
||||
>
|
||||
<IconifyIcon icon="lucide:user" />
|
||||
<span class="ml-1">角色仓库</span>
|
||||
</div>
|
||||
<div
|
||||
class="flex cursor-pointer items-center text-gray-400"
|
||||
@click="handleClearConversation"
|
||||
>
|
||||
<IconifyIcon icon="lucide:trash" />
|
||||
<span class="ml-1">清空未置顶对话</span>
|
||||
</div>
|
||||
</div>
|
||||
</Layout.Sider>
|
||||
</template>
|
|
@ -0,0 +1,82 @@
|
|||
<script lang="ts" setup>
|
||||
import type { AiChatConversationApi } from '#/api/ai/chat/conversation';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { useVbenModal } from '@vben/common-ui';
|
||||
|
||||
import { message } from 'ant-design-vue';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
import {
|
||||
getChatConversationMy,
|
||||
updateChatConversationMy,
|
||||
} from '#/api/ai/chat/conversation';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
import { useFormSchema } from '../../data';
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
const formData = ref<AiChatConversationApi.ChatConversationVO>();
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
formItemClass: 'col-span-2',
|
||||
labelWidth: 140,
|
||||
},
|
||||
layout: 'horizontal',
|
||||
schema: useFormSchema(),
|
||||
showDefaultActions: false,
|
||||
});
|
||||
|
||||
const [Modal, modalApi] = useVbenModal({
|
||||
async onConfirm() {
|
||||
const { valid } = await formApi.validate();
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
// 提交表单
|
||||
const data =
|
||||
(await formApi.getValues()) as AiChatConversationApi.ChatConversationVO;
|
||||
try {
|
||||
await updateChatConversationMy(data);
|
||||
|
||||
// 关闭并提示
|
||||
await modalApi.close();
|
||||
emit('success');
|
||||
message.success($t('ui.actionMessage.operationSuccess'));
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
async onOpenChange(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
formData.value = undefined;
|
||||
return;
|
||||
}
|
||||
// 加载数据
|
||||
const data = modalApi.getData<AiChatConversationApi.ChatConversationVO>();
|
||||
if (!data || !data.id) {
|
||||
return;
|
||||
}
|
||||
modalApi.lock();
|
||||
try {
|
||||
formData.value = await getChatConversationMy(data.id as number);
|
||||
// 设置到 values
|
||||
await formApi.setValues(formData.value);
|
||||
} finally {
|
||||
modalApi.unlock();
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Modal class="w-2/5" title="设定">
|
||||
<Form class="mx-4" />
|
||||
</Modal>
|
||||
</template>
|
|
@ -0,0 +1,103 @@
|
|||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { Tooltip } from 'ant-design-vue';
|
||||
|
||||
const props = defineProps<{
|
||||
segments: {
|
||||
content: string;
|
||||
documentId: number;
|
||||
documentName: string;
|
||||
id: number;
|
||||
}[];
|
||||
}>();
|
||||
|
||||
const document = ref<null | {
|
||||
id: number;
|
||||
segments: {
|
||||
content: string;
|
||||
id: number;
|
||||
}[];
|
||||
title: string;
|
||||
}>(null); // 知识库文档列表
|
||||
const dialogVisible = ref(false); // 知识引用详情弹窗
|
||||
const documentRef = ref<HTMLElement>(); // 知识引用详情弹窗 Ref
|
||||
|
||||
/** 按照 document 聚合 segments */
|
||||
const documentList = computed(() => {
|
||||
if (!props.segments) return [];
|
||||
|
||||
const docMap = new Map();
|
||||
props.segments.forEach((segment) => {
|
||||
if (!docMap.has(segment.documentId)) {
|
||||
docMap.set(segment.documentId, {
|
||||
id: segment.documentId,
|
||||
title: segment.documentName,
|
||||
segments: [],
|
||||
});
|
||||
}
|
||||
docMap.get(segment.documentId).segments.push({
|
||||
id: segment.id,
|
||||
content: segment.content,
|
||||
});
|
||||
});
|
||||
return [...docMap.values()];
|
||||
});
|
||||
|
||||
/** 点击 document 处理 */
|
||||
function handleClick(doc: any) {
|
||||
document.value = doc;
|
||||
dialogVisible.value = true;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 知识引用列表 -->
|
||||
<div
|
||||
v-if="segments && segments.length > 0"
|
||||
class="mt-2 rounded-lg bg-gray-50 p-2"
|
||||
>
|
||||
<div class="mb-2 flex items-center text-sm text-gray-400">
|
||||
<IconifyIcon icon="lucide:file-text" class="mr-1" /> 知识引用
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<div
|
||||
v-for="(doc, index) in documentList"
|
||||
:key="index"
|
||||
class="cursor-pointer rounded-lg bg-white p-2 px-3 transition-all hover:bg-blue-50"
|
||||
@click="handleClick(doc)"
|
||||
>
|
||||
<div class="mb-1 text-sm text-gray-600">
|
||||
{{ doc.title }}
|
||||
<span class="ml-1 text-xs text-gray-300">
|
||||
({{ doc.segments.length }} 条)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Tooltip placement="topLeft" trigger="click">
|
||||
<div ref="documentRef"></div>
|
||||
<template #title>
|
||||
<div class="mb-3 text-base font-bold">{{ document?.title }}</div>
|
||||
<div class="max-h-[60vh] overflow-y-auto">
|
||||
<div
|
||||
v-for="(segment, index) in document?.segments"
|
||||
:key="index"
|
||||
class="border-b-solid border-b-gray-200 p-3 last:border-b-0"
|
||||
>
|
||||
<div
|
||||
class="mb-2 block w-fit rounded-sm bg-gray-50 px-2 py-1 text-xs text-gray-400"
|
||||
>
|
||||
分段 {{ segment.id }}
|
||||
</div>
|
||||
<div class="mt-2 text-sm leading-[1.6] text-gray-600">
|
||||
{{ segment.content }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Tooltip>
|
||||
</template>
|
|
@ -0,0 +1,222 @@
|
|||
<script setup lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
import type { AiChatConversationApi } from '#/api/ai/chat/conversation';
|
||||
import type { AiChatMessageApi } from '#/api/ai/chat/message';
|
||||
|
||||
import { computed, nextTick, onMounted, ref, toRefs } from 'vue';
|
||||
|
||||
import { IconifyIcon, SvgGptIcon } from '@vben/icons';
|
||||
import { preferences } from '@vben/preferences';
|
||||
import { useUserStore } from '@vben/stores';
|
||||
import { formatDate } from '@vben/utils';
|
||||
|
||||
import { useClipboard } from '@vueuse/core';
|
||||
import { Avatar, Button, message } from 'ant-design-vue';
|
||||
|
||||
import { deleteChatMessage } from '#/api/ai/chat/message';
|
||||
import { MarkdownView } from '#/components/markdown-view';
|
||||
|
||||
import MessageKnowledge from './MessageKnowledge.vue';
|
||||
// 定义 props
|
||||
const props = defineProps({
|
||||
conversation: {
|
||||
type: Object as PropType<AiChatConversationApi.ChatConversationVO>,
|
||||
required: true,
|
||||
},
|
||||
list: {
|
||||
type: Array as PropType<AiChatMessageApi.ChatMessageVO[]>,
|
||||
required: true,
|
||||
},
|
||||
});
|
||||
// 消息列表
|
||||
|
||||
const emits = defineEmits(['onDeleteSuccess', 'onRefresh', 'onEdit']);
|
||||
const { copy } = useClipboard(); // 初始化 copy 到粘贴板
|
||||
const userStore = useUserStore();
|
||||
|
||||
// 判断“消息列表”滚动的位置(用于判断是否需要滚动到消息最下方)
|
||||
const messageContainer: any = ref(null);
|
||||
const isScrolling = ref(false); // 用于判断用户是否在滚动
|
||||
|
||||
const userAvatar = computed(
|
||||
() => userStore.userInfo?.avatar || preferences.app.defaultAvatar,
|
||||
);
|
||||
|
||||
const { list } = toRefs(props); // 定义 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;
|
||||
isScrolling.value = scrollTop + offsetHeight < scrollHeight - 100;
|
||||
}
|
||||
|
||||
/** 回到底部 */
|
||||
async function handleGoBottom() {
|
||||
const scrollContainer = messageContainer.value;
|
||||
scrollContainer.scrollTop = scrollContainer.scrollHeight;
|
||||
}
|
||||
|
||||
/** 回到顶部 */
|
||||
async function handlerGoTop() {
|
||||
const scrollContainer = messageContainer.value;
|
||||
scrollContainer.scrollTop = 0;
|
||||
}
|
||||
|
||||
defineExpose({ scrollToBottom, handlerGoTop }); // 提供方法给 parent 调用
|
||||
|
||||
// ============ 处理消息操作 ==============
|
||||
|
||||
/** 复制 */
|
||||
async function copyContent(content: string) {
|
||||
await copy(content);
|
||||
message.success('复制成功!');
|
||||
}
|
||||
/** 删除 */
|
||||
async function onDelete(id: number) {
|
||||
// 删除 message
|
||||
await deleteChatMessage(id);
|
||||
message.success('删除成功!');
|
||||
// 回调
|
||||
emits('onDeleteSuccess');
|
||||
}
|
||||
|
||||
/** 刷新 */
|
||||
async function onRefresh(message: AiChatMessageApi.ChatMessageVO) {
|
||||
emits('onRefresh', message);
|
||||
}
|
||||
|
||||
/** 编辑 */
|
||||
async function onEdit(message: AiChatMessageApi.ChatMessageVO) {
|
||||
emits('onEdit', message);
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
messageContainer.value.addEventListener('scroll', handleScroll);
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div ref="messageContainer" class="relative h-full overflow-y-auto">
|
||||
<div
|
||||
v-for="(item, index) in list"
|
||||
:key="index"
|
||||
class="mt-12 flex flex-col overflow-y-hidden px-5"
|
||||
>
|
||||
<!-- 左侧消息:system、assistant -->
|
||||
<div v-if="item.type !== 'user'" class="flex flex-row">
|
||||
<div class="avatar">
|
||||
<Avatar
|
||||
v-if="conversation.roleAvatar"
|
||||
:src="conversation.roleAvatar"
|
||||
/>
|
||||
<SvgGptIcon v-else class="size-8" />
|
||||
</div>
|
||||
<div class="mx-4 flex flex-col text-left">
|
||||
<div class="text-left leading-10">
|
||||
{{ formatDate(item.createTime) }}
|
||||
</div>
|
||||
<div
|
||||
class="relative flex flex-col break-words rounded-lg bg-gray-100 p-2.5 pb-1 pt-2.5 shadow-sm"
|
||||
>
|
||||
<MarkdownView
|
||||
class="text-sm text-gray-600"
|
||||
:content="item.content"
|
||||
/>
|
||||
<MessageKnowledge v-if="item.segments" :segments="item.segments" />
|
||||
</div>
|
||||
<div class="mt-2 flex flex-row">
|
||||
<Button
|
||||
class="flex items-center bg-transparent px-1.5 hover:bg-gray-100"
|
||||
type="text"
|
||||
@click="copyContent(item.content)"
|
||||
>
|
||||
<IconifyIcon icon="lucide:copy" />
|
||||
</Button>
|
||||
<Button
|
||||
v-if="item.id > 0"
|
||||
class="flex items-center bg-transparent px-1.5 hover:bg-gray-100"
|
||||
type="text"
|
||||
@click="onDelete(item.id)"
|
||||
>
|
||||
<IconifyIcon icon="lucide:trash" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 右侧消息:user -->
|
||||
<div v-else class="flex flex-row-reverse justify-start">
|
||||
<div class="avatar">
|
||||
<Avatar :src="userAvatar" />
|
||||
</div>
|
||||
<div class="mx-4 flex flex-col text-left">
|
||||
<div class="text-left leading-8">
|
||||
{{ formatDate(item.createTime) }}
|
||||
</div>
|
||||
<div class="flex flex-row-reverse">
|
||||
<div
|
||||
class="inline w-auto whitespace-pre-wrap break-words rounded-lg bg-blue-500 p-2.5 text-sm text-white shadow-sm"
|
||||
>
|
||||
{{ item.content }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-2 flex flex-row-reverse">
|
||||
<Button
|
||||
class="flex items-center bg-transparent px-1.5 hover:bg-gray-100"
|
||||
type="text"
|
||||
@click="copyContent(item.content)"
|
||||
>
|
||||
<IconifyIcon icon="lucide:copy" />
|
||||
</Button>
|
||||
<Button
|
||||
class="flex items-center bg-transparent px-1.5 hover:bg-gray-100"
|
||||
type="text"
|
||||
@click="onDelete(item.id)"
|
||||
>
|
||||
<IconifyIcon icon="lucide:trash" />
|
||||
</Button>
|
||||
<Button
|
||||
class="flex items-center bg-transparent px-1.5 hover:bg-gray-100"
|
||||
type="text"
|
||||
@click="onRefresh(item)"
|
||||
>
|
||||
<IconifyIcon icon="lucide:refresh-cw" />
|
||||
</Button>
|
||||
<Button
|
||||
class="flex items-center bg-transparent px-1.5 hover:bg-gray-100"
|
||||
type="text"
|
||||
@click="onEdit(item)"
|
||||
>
|
||||
<IconifyIcon icon="lucide:edit" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 回到底部按钮 -->
|
||||
<div
|
||||
v-if="isScrolling"
|
||||
class="z-1000 absolute bottom-0 right-1/2"
|
||||
@click="handleGoBottom"
|
||||
>
|
||||
<Button shape="circle">
|
||||
<IconifyIcon icon="lucide:chevron-down" />
|
||||
</Button>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,38 @@
|
|||
<!-- 消息列表为空时,展示 prompt 列表 -->
|
||||
<script setup lang="ts">
|
||||
// prompt 列表
|
||||
|
||||
const emits = defineEmits(['onPrompt']);
|
||||
const promptList = [
|
||||
{
|
||||
prompt: '今天气怎么样?',
|
||||
},
|
||||
{
|
||||
prompt: '写一首好听的诗歌?',
|
||||
},
|
||||
]; /** 选中 prompt 点击 */
|
||||
async function handlerPromptClick(prompt: any) {
|
||||
emits('onPrompt', prompt.prompt);
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="relative flex h-full w-full flex-row justify-center">
|
||||
<!-- center-container -->
|
||||
<div class="flex flex-col justify-center">
|
||||
<!-- title -->
|
||||
<div class="text-center text-3xl font-bold">芋道 AI</div>
|
||||
|
||||
<!-- role-list -->
|
||||
<div class="mt-5 flex w-96 flex-wrap items-center justify-center">
|
||||
<div
|
||||
v-for="prompt in promptList"
|
||||
:key="prompt.prompt"
|
||||
@click="handlerPromptClick(prompt)"
|
||||
class="m-2.5 flex w-44 cursor-pointer justify-center rounded-lg border border-gray-200 leading-10 hover:bg-gray-100"
|
||||
>
|
||||
{{ prompt.prompt }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,9 @@
|
|||
<script setup lang="ts">
|
||||
import { Skeleton } from 'ant-design-vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="p-8">
|
||||
<Skeleton active />
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,23 @@
|
|||
<script setup lang="ts">
|
||||
import { Button } from 'ant-design-vue';
|
||||
|
||||
const emits = defineEmits(['onNewConversation']);
|
||||
|
||||
/** 新建 conversation 聊天对话 */
|
||||
function handlerNewChat() {
|
||||
emits('onNewConversation');
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full w-full flex-row justify-center">
|
||||
<div class="flex flex-col justify-center">
|
||||
<div class="text-center text-sm text-gray-400">
|
||||
点击下方按钮,开始你的对话吧
|
||||
</div>
|
||||
<div class="mt-5 flex flex-row justify-center">
|
||||
<Button type="primary" round @click="handlerNewChat">新建对话</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,44 @@
|
|||
<script setup lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
// 定义属性
|
||||
defineProps({
|
||||
categoryList: {
|
||||
type: Array as PropType<string[]>,
|
||||
required: true,
|
||||
},
|
||||
active: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '全部',
|
||||
},
|
||||
});
|
||||
|
||||
// 定义回调
|
||||
const emits = defineEmits(['onCategoryClick']);
|
||||
|
||||
/** 处理分类点击事件 */
|
||||
async function handleCategoryClick(category: string) {
|
||||
emits('onCategoryClick', category);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-wrap items-center">
|
||||
<div
|
||||
class="mr-2 flex flex-row"
|
||||
v-for="category in categoryList"
|
||||
:key="category"
|
||||
>
|
||||
<Button
|
||||
size="small"
|
||||
shape="round"
|
||||
:type="category === active ? 'primary' : 'default'"
|
||||
@click="handleCategoryClick(category)"
|
||||
>
|
||||
{{ category }}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,129 @@
|
|||
<script setup lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
|
||||
import type { AiModelChatRoleApi } from '#/api/ai/model/chatRole';
|
||||
|
||||
import { ref } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { Avatar, Button, Card, Dropdown, Menu } from 'ant-design-vue';
|
||||
|
||||
// tabs ref
|
||||
|
||||
// 定义属性
|
||||
const props = defineProps({
|
||||
loading: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
roleList: {
|
||||
type: Array as PropType<AiModelChatRoleApi.ChatRoleVO[]>,
|
||||
required: true,
|
||||
},
|
||||
showMore: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
});
|
||||
|
||||
// 定义钩子
|
||||
const emits = defineEmits(['onDelete', 'onEdit', 'onUse', 'onPage']);
|
||||
const tabsRef = ref<any>();
|
||||
|
||||
/** 操作:编辑、删除 */
|
||||
async function handleMoreClick(data: any) {
|
||||
const type = data[0];
|
||||
const role = data[1];
|
||||
if (type === 'delete') {
|
||||
emits('onDelete', role);
|
||||
} else {
|
||||
emits('onEdit', role);
|
||||
}
|
||||
}
|
||||
|
||||
/** 选中 */
|
||||
function handleUseClick(role: any) {
|
||||
emits('onUse', role);
|
||||
}
|
||||
|
||||
/** 滚动 */
|
||||
async function handleTabsScroll() {
|
||||
if (tabsRef.value) {
|
||||
const { scrollTop, scrollHeight, clientHeight } = tabsRef.value;
|
||||
if (scrollTop + clientHeight >= scrollHeight - 20 && !props.loading) {
|
||||
await emits('onPage');
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="relative flex h-full flex-wrap content-start items-start overflow-auto px-6 pb-36"
|
||||
ref="tabsRef"
|
||||
@scroll="handleTabsScroll"
|
||||
>
|
||||
<div class="mb-5 mr-5 inline-block" v-for="role in roleList" :key="role.id">
|
||||
<Card
|
||||
class="relative rounded-lg"
|
||||
:body-style="{
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'row',
|
||||
justifyContent: 'flex-start',
|
||||
width: '240px',
|
||||
maxWidth: '240px',
|
||||
padding: '15px 15px 10px',
|
||||
}"
|
||||
>
|
||||
<!-- 更多操作 -->
|
||||
<div v-if="showMore" class="absolute right-3 top-0">
|
||||
<Dropdown>
|
||||
<Button type="link">
|
||||
<IconifyIcon icon="lucide:ellipsis-vertical" />
|
||||
</Button>
|
||||
<template #overlay>
|
||||
<Menu>
|
||||
<Menu.Item @click="handleMoreClick(['edit', role])">
|
||||
<div class="flex items-center">
|
||||
<IconifyIcon icon="lucide:edit" color="#787878" />
|
||||
<span>编辑</span>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
<Menu.Item @click="handleMoreClick(['delete', role])">
|
||||
<div class="flex items-center">
|
||||
<IconifyIcon icon="lucide:trash" color="red" />
|
||||
<span class="text-red-500">删除</span>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
</Menu>
|
||||
</template>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
<!-- 角色信息 -->
|
||||
<div>
|
||||
<Avatar :src="role.avatar" class="h-10 w-10 overflow-hidden" />
|
||||
</div>
|
||||
|
||||
<div class="ml-2 w-full">
|
||||
<div class="h-20">
|
||||
<div class="max-w-36 text-lg font-bold text-gray-600">
|
||||
{{ role.name }}
|
||||
</div>
|
||||
<div class="mt-2 text-sm text-gray-400">
|
||||
{{ role.description }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-1 flex flex-row-reverse">
|
||||
<Button type="primary" size="small" @click="handleUseClick(role)">
|
||||
使用
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,250 @@
|
|||
<script setup lang="ts">
|
||||
import type { AiChatConversationApi } from '#/api/ai/chat/conversation';
|
||||
import type { AiModelChatRoleApi } from '#/api/ai/model/chatRole';
|
||||
|
||||
import { onMounted, reactive, ref } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
|
||||
import { useVbenDrawer, useVbenModal } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { Button, Input, Layout, Tabs } from 'ant-design-vue';
|
||||
|
||||
import { createChatConversationMy } from '#/api/ai/chat/conversation';
|
||||
import { deleteMy, getCategoryList, getMyPage } from '#/api/ai/model/chatRole';
|
||||
|
||||
import Form from '../../../../model/chatRole/modules/form.vue';
|
||||
import RoleCategoryList from './RoleCategoryList.vue';
|
||||
import RoleList from './RoleList.vue';
|
||||
|
||||
const router = useRouter(); // 路由对象
|
||||
const [Drawer] = useVbenDrawer({
|
||||
title: '角色管理',
|
||||
footer: false,
|
||||
class: 'w-2/5',
|
||||
});
|
||||
|
||||
const [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: Form,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
// 属性定义
|
||||
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<AiModelChatRoleApi.ChatRoleVO[]>([]); // my 分页大小
|
||||
const publicRoleParams = reactive({
|
||||
pageNo: 1,
|
||||
pageSize: 50,
|
||||
});
|
||||
const publicRoleList = ref<AiModelChatRoleApi.ChatRoleVO[]>([]); // public 分页大小
|
||||
const activeCategory = ref<string>('全部'); // 选择中的分类
|
||||
const categoryList = ref<string[]>([]); // 角色分类类别
|
||||
|
||||
/** tabs 点击 */
|
||||
async function handleTabsClick(tab: any) {
|
||||
// 设置切换状态
|
||||
activeTab.value = tab;
|
||||
// 切换的时候重新加载数据
|
||||
await getActiveTabsRole();
|
||||
}
|
||||
|
||||
/** 获取 my role 我的角色 */
|
||||
async function getMyRole(append?: boolean) {
|
||||
const params: AiModelChatRoleApi.ChatRolePageReqVO = {
|
||||
...myRoleParams,
|
||||
name: search.value,
|
||||
publicStatus: false,
|
||||
};
|
||||
const { list } = await getMyPage(params);
|
||||
if (append) {
|
||||
myRoleList.value.push(...list);
|
||||
} else {
|
||||
myRoleList.value = list;
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取 public role 公共角色 */
|
||||
async function getPublicRole(append?: boolean) {
|
||||
const params: AiModelChatRoleApi.ChatRolePageReqVO = {
|
||||
...publicRoleParams,
|
||||
category: activeCategory.value === '全部' ? '' : activeCategory.value,
|
||||
name: search.value,
|
||||
publicStatus: true,
|
||||
};
|
||||
const { list } = await getMyPage(params);
|
||||
if (append) {
|
||||
publicRoleList.value.push(...list);
|
||||
} else {
|
||||
publicRoleList.value = list;
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取选中的 tabs 角色 */
|
||||
async function getActiveTabsRole() {
|
||||
if (activeTab.value === 'my-role') {
|
||||
myRoleParams.pageNo = 1;
|
||||
await getMyRole();
|
||||
} else {
|
||||
publicRoleParams.pageNo = 1;
|
||||
await getPublicRole();
|
||||
}
|
||||
}
|
||||
|
||||
/** 获取角色分类列表 */
|
||||
async function getRoleCategoryList() {
|
||||
categoryList.value = ['全部', ...(await getCategoryList())];
|
||||
}
|
||||
|
||||
/** 处理分类点击 */
|
||||
async function handlerCategoryClick(category: string) {
|
||||
// 切换选择的分类
|
||||
activeCategory.value = category;
|
||||
// 筛选
|
||||
await getActiveTabsRole();
|
||||
}
|
||||
|
||||
async function handlerAddRole() {
|
||||
formModalApi.setData({ formType: 'my-create' }).open();
|
||||
}
|
||||
/** 编辑角色 */
|
||||
async function handlerCardEdit(role: any) {
|
||||
formModalApi.setData({ formType: 'my-update', id: role.id }).open();
|
||||
}
|
||||
|
||||
/** 添加角色成功 */
|
||||
async function handlerAddRoleSuccess() {
|
||||
// 刷新数据
|
||||
await getActiveTabsRole();
|
||||
}
|
||||
|
||||
/** 删除角色 */
|
||||
async function handlerCardDelete(role: any) {
|
||||
await deleteMy(role.id);
|
||||
// 刷新数据
|
||||
await getActiveTabsRole();
|
||||
}
|
||||
|
||||
/** 角色分页:获取下一页 */
|
||||
async function handlerCardPage(type: string) {
|
||||
try {
|
||||
loading.value = true;
|
||||
if (type === 'public') {
|
||||
publicRoleParams.pageNo++;
|
||||
await getPublicRole(true);
|
||||
} else {
|
||||
myRoleParams.pageNo++;
|
||||
await getMyRole(true);
|
||||
}
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
/** 选择 card 角色:新建聊天对话 */
|
||||
async function handlerCardUse(role: any) {
|
||||
// 1. 创建对话
|
||||
const data: AiChatConversationApi.ChatConversationVO = {
|
||||
roleId: role.id,
|
||||
} as unknown as AiChatConversationApi.ChatConversationVO;
|
||||
const conversationId = await createChatConversationMy(data);
|
||||
|
||||
// 2. 跳转页面
|
||||
await router.push({
|
||||
path: '/ai/chat',
|
||||
query: {
|
||||
conversationId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** 初始化 */
|
||||
onMounted(async () => {
|
||||
// 获取分类
|
||||
await getRoleCategoryList();
|
||||
// 获取 role 数据
|
||||
await getActiveTabsRole();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Drawer>
|
||||
<Layout
|
||||
class="absolute inset-0 flex h-full w-full flex-col overflow-hidden bg-white"
|
||||
>
|
||||
<FormModal @success="handlerAddRoleSuccess" />
|
||||
|
||||
<Layout.Content class="relative m-0 flex-1 overflow-hidden p-0">
|
||||
<div class="z-100 absolute right-0 top--1 mr-5 mt-5">
|
||||
<!-- 搜索输入框 -->
|
||||
<Input.Search
|
||||
:loading="loading"
|
||||
v-model:value="search"
|
||||
class="w-60"
|
||||
placeholder="请输入搜索的内容"
|
||||
@search="getActiveTabsRole"
|
||||
/>
|
||||
<Button
|
||||
v-if="activeTab === 'my-role'"
|
||||
type="primary"
|
||||
@click="handlerAddRole"
|
||||
class="ml-5"
|
||||
>
|
||||
<IconifyIcon icon="lucide:user" class="mr-1.5" />
|
||||
添加角色
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- 标签页内容 -->
|
||||
<Tabs
|
||||
v-model:value="activeTab"
|
||||
class="relative h-full p-4"
|
||||
@tab-click="handleTabsClick"
|
||||
>
|
||||
<Tabs.TabPane
|
||||
key="my-role"
|
||||
class="flex h-full flex-col overflow-y-auto"
|
||||
tab="我的角色"
|
||||
>
|
||||
<RoleList
|
||||
:loading="loading"
|
||||
:role-list="myRoleList"
|
||||
:show-more="true"
|
||||
@on-delete="handlerCardDelete"
|
||||
@on-edit="handlerCardEdit"
|
||||
@on-use="handlerCardUse"
|
||||
@on-page="handlerCardPage('my')"
|
||||
class="mt-5"
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane
|
||||
key="public-role"
|
||||
class="flex h-full flex-col overflow-y-auto"
|
||||
tab="公共角色"
|
||||
>
|
||||
<RoleCategoryList
|
||||
:category-list="categoryList"
|
||||
:active="activeCategory"
|
||||
@on-category-click="handlerCategoryClick"
|
||||
class="mx-6"
|
||||
/>
|
||||
<RoleList
|
||||
:role-list="publicRoleList"
|
||||
@on-delete="handlerCardDelete"
|
||||
@on-edit="handlerCardEdit"
|
||||
@on-use="handlerCardUse"
|
||||
@on-page="handlerCardPage('public')"
|
||||
class="mt-5"
|
||||
loading
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Layout.Content>
|
||||
</Layout>
|
||||
</Drawer>
|
||||
</template>
|
|
@ -0,0 +1,79 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
|
||||
import { getModelSimpleList } from '#/api/ai/model/model';
|
||||
import { AiModelTypeEnum } from '#/utils';
|
||||
|
||||
export function useFormSchema(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'id',
|
||||
dependencies: {
|
||||
triggerFields: [''],
|
||||
show: () => false,
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'systemMessage',
|
||||
label: '角色设定',
|
||||
component: 'Textarea',
|
||||
componentProps: {
|
||||
rows: 4,
|
||||
placeholder: '请输入角色设定',
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'ApiSelect',
|
||||
fieldName: 'modelId',
|
||||
label: '模型',
|
||||
componentProps: {
|
||||
api: () => getModelSimpleList(AiModelTypeEnum.CHAT),
|
||||
labelField: 'name',
|
||||
valueField: 'id',
|
||||
allowClear: true,
|
||||
placeholder: '请选择模型',
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'temperature',
|
||||
label: '温度参数',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
controlsPosition: 'right',
|
||||
placeholder: '请输入温度参数',
|
||||
class: 'w-full',
|
||||
precision: 2,
|
||||
min: 0,
|
||||
max: 2,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'maxTokens',
|
||||
label: '回复数 Token 数',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
controlsPosition: 'right',
|
||||
placeholder: '请输入回复数 Token 数',
|
||||
class: 'w-full',
|
||||
min: 0,
|
||||
max: 8192,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
{
|
||||
fieldName: 'maxContexts',
|
||||
label: '上下文数量',
|
||||
component: 'InputNumber',
|
||||
componentProps: {
|
||||
controlsPosition: 'right',
|
||||
placeholder: '请输入上下文数量',
|
||||
class: 'w-full',
|
||||
min: 0,
|
||||
max: 20,
|
||||
},
|
||||
rules: 'required',
|
||||
},
|
||||
];
|
||||
}
|
|
@ -1,28 +1,619 @@
|
|||
<script lang="ts" setup>
|
||||
import { Page } from '@vben/common-ui';
|
||||
import type { AiChatConversationApi } from '#/api/ai/chat/conversation';
|
||||
import type { AiChatMessageApi } from '#/api/ai/chat/message';
|
||||
|
||||
import { Button } from 'ant-design-vue';
|
||||
import { computed, nextTick, onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { alert, confirm, Page, useVbenModal } from '@vben/common-ui';
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import { Button, Layout, message, Switch } from 'ant-design-vue';
|
||||
|
||||
import { getChatConversationMy } from '#/api/ai/chat/conversation';
|
||||
import {
|
||||
deleteByConversationId,
|
||||
getChatMessageListByConversationId,
|
||||
sendChatMessageStream,
|
||||
} from '#/api/ai/chat/message';
|
||||
|
||||
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 [FormModal, formModalApi] = useVbenModal({
|
||||
connectedComponent: ConversationUpdateForm,
|
||||
destroyOnClose: true,
|
||||
});
|
||||
// 聊天对话
|
||||
const conversationListRef = ref();
|
||||
const activeConversationId = ref<null | number>(null); // 选中的对话编号
|
||||
const activeConversation = ref<AiChatConversationApi.ChatConversationVO | null>(
|
||||
null,
|
||||
); // 选中的 Conversation
|
||||
const conversationInProgress = ref(false); // 对话是否正在进行中。目前只有【发送】消息时,会更新为 true,避免切换对话、删除对话等操作
|
||||
|
||||
// 消息列表
|
||||
const messageRef = ref();
|
||||
const activeMessageList = ref<AiChatMessageApi.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('');
|
||||
|
||||
// =========== 【聊天对话】相关 ===========
|
||||
|
||||
/** 获取对话信息 */
|
||||
async function getConversation(id: null | number) {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
const conversation: AiChatConversationApi.ChatConversationVO =
|
||||
await getChatConversationMy(id);
|
||||
if (!conversation) {
|
||||
return;
|
||||
}
|
||||
activeConversation.value = conversation;
|
||||
activeConversationId.value = conversation.id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 点击某个对话
|
||||
*
|
||||
* @param conversation 选中的对话
|
||||
* @return 是否切换成功
|
||||
*/
|
||||
async function handleConversationClick(
|
||||
conversation: AiChatConversationApi.ChatConversationVO,
|
||||
) {
|
||||
// 对话进行中,不允许切换
|
||||
if (conversationInProgress.value) {
|
||||
alert('对话中,不允许切换!');
|
||||
return false;
|
||||
}
|
||||
|
||||
// 更新选中的对话 id
|
||||
activeConversationId.value = conversation.id;
|
||||
activeConversation.value = conversation;
|
||||
// 刷新 message 列表
|
||||
await getMessageList();
|
||||
// 滚动底部
|
||||
scrollToBottom(true);
|
||||
// 清空输入框
|
||||
prompt.value = '';
|
||||
return true;
|
||||
}
|
||||
|
||||
/** 删除某个对话*/
|
||||
async function handlerConversationDelete(
|
||||
delConversation: AiChatConversationApi.ChatConversationVO,
|
||||
) {
|
||||
// 删除的对话如果是当前选中的,那么就重置
|
||||
if (activeConversationId.value === delConversation.id) {
|
||||
await handleConversationClear();
|
||||
}
|
||||
}
|
||||
|
||||
/** 清空选中的对话 */
|
||||
async function handleConversationClear() {
|
||||
// 对话进行中,不允许切换
|
||||
if (conversationInProgress.value) {
|
||||
alert('对话中,不允许切换!');
|
||||
return false;
|
||||
}
|
||||
activeConversationId.value = null;
|
||||
activeConversation.value = null;
|
||||
activeMessageList.value = [];
|
||||
}
|
||||
|
||||
async function openChatConversationUpdateForm() {
|
||||
formModalApi.setData({ id: activeConversationId.value }).open();
|
||||
}
|
||||
async function handleConversationUpdateSuccess() {
|
||||
// 对话更新成功,刷新最新信息
|
||||
await getConversation(activeConversationId.value);
|
||||
}
|
||||
|
||||
/** 处理聊天对话的创建成功 */
|
||||
async function handleConversationCreate() {
|
||||
// 创建对话
|
||||
await conversationListRef.value.createConversation();
|
||||
}
|
||||
/** 处理聊天对话的创建成功 */
|
||||
async function handleConversationCreateSuccess() {
|
||||
// 创建新的对话,清空输入框
|
||||
prompt.value = '';
|
||||
}
|
||||
|
||||
// =========== 【消息列表】相关 ===========
|
||||
|
||||
/** 获取消息 message 列表 */
|
||||
async function getMessageList() {
|
||||
try {
|
||||
if (activeConversationId.value === null) {
|
||||
return;
|
||||
}
|
||||
// Timer 定时器,如果加载速度很快,就不进入加载中
|
||||
activeMessageListLoadingTimer.value = setTimeout(() => {
|
||||
activeMessageListLoading.value = true;
|
||||
}, 60);
|
||||
|
||||
// 获取消息列表
|
||||
activeMessageList.value = await 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 消息 */
|
||||
function handleMessageDelete() {
|
||||
if (conversationInProgress.value) {
|
||||
alert('回答中,不能删除!');
|
||||
return;
|
||||
}
|
||||
// 刷新 message 列表
|
||||
getMessageList();
|
||||
}
|
||||
|
||||
/** 处理 message 清空 */
|
||||
async function handlerMessageClear() {
|
||||
if (!activeConversationId.value) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
// 确认提示
|
||||
await confirm('确认清空对话消息?');
|
||||
// 清空对话
|
||||
await deleteByConversationId(activeConversationId.value);
|
||||
// 刷新 message 列表
|
||||
activeMessageList.value = [];
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 回到 message 列表的顶部 */
|
||||
function handleGoTopMessage() {
|
||||
messageRef.value.handlerGoTop();
|
||||
}
|
||||
|
||||
// =========== 【发送消息】相关 ===========
|
||||
/** 处理来自 keydown 的发送消息 */
|
||||
async function handleSendByKeydown(event: any) {
|
||||
// 判断用户是否在输入
|
||||
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(); // 防止默认的提交行为
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** 处理来自【发送】按钮的发送消息 */
|
||||
function handleSendByButton() {
|
||||
doSendMessage(prompt.value?.trim() as string);
|
||||
}
|
||||
|
||||
/** 处理 prompt 输入变化 */
|
||||
function handlePromptInput(event: any) {
|
||||
// 非输入法 输入设置为 true
|
||||
if (!isComposing.value) {
|
||||
// 回车 event data 是 null
|
||||
if (event.data === null || event.data === 'null') {
|
||||
return;
|
||||
}
|
||||
isComposing.value = true;
|
||||
}
|
||||
// 清理定时器
|
||||
if (inputTimeout.value) {
|
||||
clearTimeout(inputTimeout.value);
|
||||
}
|
||||
// 重置定时器
|
||||
inputTimeout.value = setTimeout(() => {
|
||||
isComposing.value = false;
|
||||
}, 400);
|
||||
}
|
||||
|
||||
function onCompositionstart() {
|
||||
isComposing.value = true;
|
||||
}
|
||||
|
||||
function onCompositionend() {
|
||||
// console.log('输入结束...')
|
||||
setTimeout(() => {
|
||||
isComposing.value = false;
|
||||
}, 200);
|
||||
}
|
||||
|
||||
/** 真正执行【发送】消息操作 */
|
||||
async function doSendMessage(content: string) {
|
||||
// 校验
|
||||
if (content.length === 0) {
|
||||
message.error('发送失败,原因:内容为空!');
|
||||
return;
|
||||
}
|
||||
if (activeConversationId.value === null) {
|
||||
message.error('还没创建对话,不能发送!');
|
||||
return;
|
||||
}
|
||||
// 清空输入框
|
||||
prompt.value = '';
|
||||
// 执行发送
|
||||
await doSendMessageStream({
|
||||
conversationId: activeConversationId.value,
|
||||
content,
|
||||
} as AiChatMessageApi.ChatMessageVO);
|
||||
}
|
||||
|
||||
/** 真正执行【发送】消息操作 */
|
||||
async function doSendMessageStream(
|
||||
userMessage: AiChatMessageApi.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 AiChatMessageApi.ChatMessageVO,
|
||||
{
|
||||
id: -2,
|
||||
conversationId: activeConversationId.value,
|
||||
type: 'assistant',
|
||||
content: '思考中...',
|
||||
createTime: new Date(),
|
||||
} as AiChatMessageApi.ChatMessageVO,
|
||||
);
|
||||
// 1.2 滚动到最下面
|
||||
await nextTick();
|
||||
await scrollToBottom(); // 底部
|
||||
// 1.3 开始滚动
|
||||
textRoll();
|
||||
|
||||
// 2. 发送 event stream
|
||||
let isFirstChunk = true; // 是否是第一个 chunk 消息段
|
||||
await sendChatMessageStream(
|
||||
userMessage.conversationId,
|
||||
userMessage.content,
|
||||
conversationInAbortController.value,
|
||||
enableContext.value,
|
||||
async (res: any) => {
|
||||
const { code, data, msg } = JSON.parse(res.data);
|
||||
if (code !== 0) {
|
||||
alert(`对话异常! ${msg}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果内容为空,就不处理。
|
||||
if (data.receive.content === '') {
|
||||
return;
|
||||
}
|
||||
// 首次返回需要添加一个 message 到页面,后面的都是更新
|
||||
if (isFirstChunk) {
|
||||
isFirstChunk = false;
|
||||
// 弹出两个假数据
|
||||
activeMessageList.value.pop();
|
||||
activeMessageList.value.pop();
|
||||
// 更新返回的数据
|
||||
activeMessageList.value.push(data.send, data.receive);
|
||||
}
|
||||
// debugger
|
||||
receiveMessageFullText.value =
|
||||
receiveMessageFullText.value + data.receive.content;
|
||||
// 滚动到最下面
|
||||
await scrollToBottom();
|
||||
},
|
||||
(error: any) => {
|
||||
alert(`对话异常! ${error}`);
|
||||
stopStream();
|
||||
// 需要抛出异常,禁止重试
|
||||
throw error;
|
||||
},
|
||||
() => {
|
||||
stopStream();
|
||||
},
|
||||
);
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/** 停止 stream 流式调用 */
|
||||
async function stopStream() {
|
||||
// tip:如果 stream 进行中的 message,就需要调用 controller 结束
|
||||
if (conversationInAbortController.value) {
|
||||
conversationInAbortController.value.abort();
|
||||
}
|
||||
// 设置为 false
|
||||
conversationInProgress.value = false;
|
||||
}
|
||||
|
||||
/** 编辑 message:设置为 prompt,可以再次编辑 */
|
||||
function handleMessageEdit(message: AiChatMessageApi.ChatMessageVO) {
|
||||
prompt.value = message.content;
|
||||
}
|
||||
|
||||
/** 刷新 message:基于指定消息,再次发起对话 */
|
||||
function handleMessageRefresh(message: AiChatMessageApi.ChatMessageVO) {
|
||||
doSendMessage(message.content);
|
||||
}
|
||||
|
||||
// ============== 【消息滚动】相关 =============
|
||||
|
||||
/** 滚动到 message 底部 */
|
||||
async function scrollToBottom(isIgnore?: boolean) {
|
||||
await nextTick();
|
||||
if (messageRef.value) {
|
||||
messageRef.value.scrollToBottom(isIgnore);
|
||||
}
|
||||
}
|
||||
|
||||
/** 自提滚动效果 */
|
||||
async function textRoll() {
|
||||
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];
|
||||
if (lastMessage)
|
||||
lastMessage.content = receiveMessageDisplayedText.value;
|
||||
// 滚动到住下面
|
||||
await scrollToBottom();
|
||||
// 重新设置任务
|
||||
timer = setTimeout(task, textSpeed.value);
|
||||
} else {
|
||||
// 不是对话中可以结束
|
||||
if (conversationInProgress.value) {
|
||||
// 重新设置任务
|
||||
timer = setTimeout(task, textSpeed.value);
|
||||
} else {
|
||||
textRoleRunning.value = false;
|
||||
clearTimeout(timer);
|
||||
}
|
||||
}
|
||||
};
|
||||
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>
|
||||
|
||||
<template>
|
||||
<Page>
|
||||
<Button
|
||||
danger
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3"
|
||||
>
|
||||
该功能支持 Vue3 + element-plus 版本!
|
||||
</Button>
|
||||
<br />
|
||||
<Button
|
||||
type="link"
|
||||
target="_blank"
|
||||
href="https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/ai/chat/index/index.vue"
|
||||
>
|
||||
可参考
|
||||
https://github.com/yudaocode/yudao-ui-admin-vue3/blob/master/src/views/ai/chat/index/index.vue
|
||||
代码,pull request 贡献给我们!
|
||||
</Button>
|
||||
<Page auto-content-height>
|
||||
<Layout class="absolute left-0 top-0 m-4 h-full w-full flex-1">
|
||||
<!-- 左侧:对话列表 -->
|
||||
<ConversationList
|
||||
:active-id="activeConversationId as any"
|
||||
ref="conversationListRef"
|
||||
@on-conversation-create="handleConversationCreateSuccess"
|
||||
@on-conversation-click="handleConversationClick"
|
||||
@on-conversation-clear="handleConversationClear"
|
||||
@on-conversation-delete="handlerConversationDelete"
|
||||
/>
|
||||
|
||||
<!-- 右侧:详情部分 -->
|
||||
<Layout class="ml-4 bg-white">
|
||||
<Layout.Header
|
||||
class="flex items-center justify-between !bg-gray-50 shadow-none"
|
||||
>
|
||||
<div class="text-lg font-bold">
|
||||
{{ activeConversation?.title ? activeConversation?.title : '对话' }}
|
||||
<span v-if="activeMessageList.length > 0">
|
||||
({{ activeMessageList.length }})
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="flex w-72 justify-end" v-if="activeConversation">
|
||||
<Button
|
||||
type="primary"
|
||||
ghost
|
||||
class="mr-2 px-2"
|
||||
size="small"
|
||||
@click="openChatConversationUpdateForm"
|
||||
>
|
||||
<span v-html="activeConversation?.modelName"></span>
|
||||
<IconifyIcon icon="lucide:settings" class="ml-2 size-4" />
|
||||
</Button>
|
||||
<Button size="small" class="mr-2 px-2" @click="handlerMessageClear">
|
||||
<IconifyIcon icon="lucide:trash-2" color="#787878" />
|
||||
</Button>
|
||||
<Button size="small" class="mr-2 px-2">
|
||||
<IconifyIcon icon="lucide:download" color="#787878" />
|
||||
</Button>
|
||||
<Button size="small" class="mr-2 px-2" @click="handleGoTopMessage">
|
||||
<IconifyIcon icon="lucide:arrow-up" color="#787878" />
|
||||
</Button>
|
||||
</div>
|
||||
</Layout.Header>
|
||||
|
||||
<Layout.Content class="relative m-0 h-full w-full p-0">
|
||||
<div class="absolute inset-0 m-0 overflow-y-hidden p-0">
|
||||
<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 as any"
|
||||
:list="messageList as any"
|
||||
@on-delete-success="handleMessageDelete"
|
||||
@on-edit="handleMessageEdit"
|
||||
@on-refresh="handleMessageRefresh"
|
||||
/>
|
||||
</div>
|
||||
</Layout.Content>
|
||||
|
||||
<Layout.Footer class="m-0 flex flex-col !bg-white p-0">
|
||||
<form
|
||||
class="my-5 mb-5 mt-2 flex flex-col rounded-xl border border-gray-200 px-2 py-2.5"
|
||||
>
|
||||
<textarea
|
||||
class="box-border h-20 resize-none overflow-auto border-none px-0 py-0.5 focus:outline-none"
|
||||
v-model="prompt"
|
||||
@keydown="handleSendByKeydown"
|
||||
@input="handlePromptInput"
|
||||
@compositionstart="onCompositionstart"
|
||||
@compositionend="onCompositionend"
|
||||
placeholder="问我任何问题...(Shift+Enter 换行,按下 Enter 发送)"
|
||||
></textarea>
|
||||
<div class="flex justify-between pb-0 pt-1">
|
||||
<div class="flex items-center">
|
||||
<Switch v-model:checked="enableContext" />
|
||||
<span class="ml-1 text-sm text-gray-400">上下文</span>
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
@click="handleSendByButton"
|
||||
:loading="conversationInProgress"
|
||||
v-if="conversationInProgress === false"
|
||||
>
|
||||
<IconifyIcon
|
||||
:icon="
|
||||
conversationInProgress
|
||||
? 'lucide:loader'
|
||||
: 'lucide:send-horizontal'
|
||||
"
|
||||
/>
|
||||
{{ conversationInProgress ? '进行中' : '发送' }}
|
||||
</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
danger
|
||||
@click="stopStream()"
|
||||
v-if="conversationInProgress === true"
|
||||
>
|
||||
<IconifyIcon icon="lucide:circle-stop" />
|
||||
停止
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Layout.Footer>
|
||||
</Layout>
|
||||
</Layout>
|
||||
<FormModal @success="handleConversationUpdateSuccess" />
|
||||
</Page>
|
||||
</template>
|
||||
|
|
|
@ -0,0 +1,194 @@
|
|||
import type { VbenFormSchema } from '#/adapter/form';
|
||||
import type { VxeTableGridOptions } from '#/adapter/vxe-table';
|
||||
|
||||
import { getSimpleUserList } from '#/api/system/user';
|
||||
import { DICT_TYPE, getRangePickerDefaultProps } from '#/utils';
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchemaConversation(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'userId',
|
||||
label: '用户编号',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'title',
|
||||
label: '聊天标题',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumnsConversation(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '对话编号',
|
||||
fixed: 'left',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'title',
|
||||
title: '对话标题',
|
||||
minWidth: 180,
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
title: '用户',
|
||||
width: 180,
|
||||
slots: { default: 'userId' },
|
||||
},
|
||||
{
|
||||
field: 'roleName',
|
||||
title: '角色',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'model',
|
||||
title: '模型标识',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'messageCount',
|
||||
title: '消息数',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'temperature',
|
||||
title: '温度参数',
|
||||
minWidth: 80,
|
||||
},
|
||||
{
|
||||
title: '回复数 Token 数',
|
||||
field: 'maxTokens',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
title: '上下文数量',
|
||||
field: 'maxContexts',
|
||||
minWidth: 120,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 130,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的搜索表单 */
|
||||
export function useGridFormSchemaMessage(): VbenFormSchema[] {
|
||||
return [
|
||||
{
|
||||
fieldName: 'conversationId',
|
||||
label: '对话编号',
|
||||
component: 'Input',
|
||||
},
|
||||
{
|
||||
fieldName: 'userId',
|
||||
label: '用户编号',
|
||||
component: 'ApiSelect',
|
||||
componentProps: {
|
||||
api: getSimpleUserList,
|
||||
labelField: 'nickname',
|
||||
valueField: 'id',
|
||||
},
|
||||
},
|
||||
{
|
||||
fieldName: 'createTime',
|
||||
label: '创建时间',
|
||||
component: 'RangePicker',
|
||||
componentProps: {
|
||||
...getRangePickerDefaultProps(),
|
||||
allowClear: true,
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
/** 列表的字段 */
|
||||
export function useGridColumnsMessage(): VxeTableGridOptions['columns'] {
|
||||
return [
|
||||
{
|
||||
field: 'id',
|
||||
title: '消息编号',
|
||||
fixed: 'left',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'conversationId',
|
||||
title: '对话编号',
|
||||
minWidth: 180,
|
||||
fixed: 'left',
|
||||
},
|
||||
{
|
||||
title: '用户',
|
||||
width: 180,
|
||||
slots: { default: 'userId' },
|
||||
},
|
||||
{
|
||||
field: 'roleName',
|
||||
title: '角色',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'type',
|
||||
title: '消息类型',
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
field: 'model',
|
||||
title: '模型标识',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
field: 'content',
|
||||
title: '消息内容',
|
||||
minWidth: 300,
|
||||
},
|
||||
{
|
||||
field: 'createTime',
|
||||
title: '创建时间',
|
||||
minWidth: 180,
|
||||
formatter: 'formatDateTime',
|
||||
},
|
||||
{
|
||||
field: 'replyId',
|
||||
title: '回复消息编号',
|
||||
minWidth: 180,
|
||||
},
|
||||
{
|
||||
title: '携带上下文',
|
||||
field: 'useContext',
|
||||
cellRender: {
|
||||
name: 'CellDict',
|
||||
props: { type: DICT_TYPE.INFRA_BOOLEAN_STRING },
|
||||
},
|
||||
minWidth: 100,
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 130,
|
||||
fixed: 'right',
|
||||
slots: { default: 'actions' },
|
||||
},
|
||||
];
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue