mirror of
https://github.com/1Panel-dev/MaxKB.git
synced 2025-12-26 09:43:10 +00:00
feat: resource authorization
This commit is contained in:
parent
3f565602a4
commit
af8172a7b5
|
|
@ -4,6 +4,15 @@ import type { pageRequest } from '@/api/type/common'
|
|||
import type { Ref } from 'vue'
|
||||
|
||||
const prefix = '/workspace'
|
||||
|
||||
/**
|
||||
* 获取成员列表
|
||||
* @query 参数
|
||||
*/
|
||||
const getUserList: (workspace_id: String) => Promise<Result<any>> = (workspace_id) => {
|
||||
return get(`${prefix}/${workspace_id}/user_list`)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取资源权限
|
||||
* @query 参数
|
||||
|
|
@ -40,4 +49,5 @@ const putResourceAuthorization: (workspace_id: String, body: any) => Promise<Res
|
|||
export default {
|
||||
getResourceAuthorization,
|
||||
putResourceAuthorization,
|
||||
getUserList
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export enum AuthorizationEnum {
|
||||
MANAGE = 'MANAGE',
|
||||
USE = 'USE',
|
||||
DATASET = 'DATASET',
|
||||
KNOWLEDGE = 'KNOWLEDGE',
|
||||
APPLICATION = 'APPLICATION'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,97 @@
|
|||
export default {
|
||||
noHistory: '暂无历史记录',
|
||||
createChat: '新建对话',
|
||||
history: '历史记录',
|
||||
only20history: '仅显示最近 20 条对话',
|
||||
question_count: '条提问',
|
||||
exportRecords: '导出聊天记录',
|
||||
chatId: '对话 ID',
|
||||
userInput: '用户输入',
|
||||
quote: '引用',
|
||||
download: '点击下载文件',
|
||||
transcribing: '转文字中',
|
||||
passwordValidator: {
|
||||
title: '请输入密码打开链接',
|
||||
errorMessage1: '密码不能为空',
|
||||
errorMessage2: '密码错误'
|
||||
},
|
||||
operation: {
|
||||
play: '点击播放',
|
||||
pause: '停止',
|
||||
regeneration: '换个答案',
|
||||
like: '赞同',
|
||||
cancelLike: '取消赞同',
|
||||
oppose: '反对',
|
||||
cancelOppose: '取消反对',
|
||||
continue: '继续',
|
||||
stopChat: '停止回答',
|
||||
startChat: '开始对话'
|
||||
},
|
||||
tip: {
|
||||
error500Message: '抱歉,当前正在维护,无法提供服务,请稍后再试!',
|
||||
errorIdentifyMessage: '无法识别用户身份',
|
||||
errorLimitMessage: '抱歉,您的提问已达到最大限制,请明天再来吧!',
|
||||
answerMessage: '抱歉,没有查找到相关内容,请重新描述您的问题或提供更多信息。',
|
||||
stopAnswer: '已停止回答',
|
||||
answerLoading: '回答中',
|
||||
recorderTip: `<p>该功能需要使用麦克风,浏览器禁止不安全页面录音,解决方案如下:<br/>
|
||||
1、可开启 https 解决;<br/>
|
||||
2、若无 https 配置则需要修改浏览器安全配置,Chrome 设置如下:<br/>
|
||||
(1) 地址栏输入chrome://flags/#unsafely-treat-insecure-origin-as-secure;<br/>
|
||||
(2) 将 http 站点配置在文本框中,例如: http://127.0.0.1:8080。</p>`,
|
||||
recorderError: '录音失败',
|
||||
confirm: '我知道了',
|
||||
requiredMessage: '请填写所有必填字段',
|
||||
inputParamMessage1: '请在URL中填写参数',
|
||||
inputParamMessage2: '的值',
|
||||
prologueMessage: '抱歉,当前正在维护,无法提供服务,请稍后再试!'
|
||||
},
|
||||
inputPlaceholder: {
|
||||
speaking: '说话中',
|
||||
recorderLoading: '转文字中',
|
||||
default: '请输入问题'
|
||||
},
|
||||
uploadFile: {
|
||||
label: '上传文件',
|
||||
most: '最多',
|
||||
limit: '个,每个文件限制',
|
||||
fileType: '文件类型',
|
||||
tipMessage: '请在文件上传配置中选择文件类型',
|
||||
limitMessage1: '最多上传',
|
||||
limitMessage2: '个文件',
|
||||
sizeLimit: '单个文件大小不能超过',
|
||||
imageMessage: '请解析图片内容',
|
||||
errorMessage: '上传失败'
|
||||
},
|
||||
executionDetails: {
|
||||
title: '执行详情',
|
||||
paramOutputTooltip: '每个文档仅支持预览500字',
|
||||
audioFile: '语音文件',
|
||||
searchContent: '检索内容',
|
||||
searchResult: '检索结果',
|
||||
conditionResult: '判断结果',
|
||||
currentChat: '本次对话',
|
||||
answer: 'AI 回答',
|
||||
replyContent: '回复内容',
|
||||
textContent: '文本内容',
|
||||
input: '输入',
|
||||
output: '输出',
|
||||
rerankerContent: '重排内容',
|
||||
rerankerResult: '重排结果',
|
||||
paragraph: '分段',
|
||||
noSubmit: '用户未提交',
|
||||
errMessage: '错误日志'
|
||||
},
|
||||
KnowledgeSource: {
|
||||
title: '知识来源',
|
||||
referenceParagraph: '引用分段',
|
||||
consume: '消耗tokens',
|
||||
consumeTime: '耗时'
|
||||
},
|
||||
paragraphSource: {
|
||||
title: '知识库引用',
|
||||
question: '用户问题',
|
||||
optimizationQuestion: '优化后问题'
|
||||
},
|
||||
editTitle: '编辑标题'
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
export default {
|
||||
quickCreatePlaceholder: '快速创建空白文档',
|
||||
quickCreateName: '文档名称',
|
||||
noData: '无匹配数据',
|
||||
loading: '加载中',
|
||||
noMore: '到底啦!',
|
||||
selectParagraph: {
|
||||
title: '选择分段',
|
||||
error: '仅执行未成功分段',
|
||||
all: '全部分段'
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
import zhCn from 'element-plus/es/locale/lang/zh-cn'
|
||||
// import components from './components'
|
||||
import components from './components'
|
||||
import views from './views'
|
||||
import theme from './theme'
|
||||
import layout from './layout'
|
||||
import dynamicsForm from './dynamics-form'
|
||||
import common from './common'
|
||||
// import chat from './ai-chat'
|
||||
import chat from './ai-chat'
|
||||
export default {
|
||||
lang: '简体中文',
|
||||
zhCn,
|
||||
|
|
@ -14,4 +14,6 @@ export default {
|
|||
layout,
|
||||
dynamicsForm,
|
||||
common,
|
||||
chat,
|
||||
components,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,113 @@
|
|||
export default {
|
||||
title: '概览',
|
||||
appInfo: {
|
||||
header: '应用信息',
|
||||
publicAccessLink: '公开访问链接',
|
||||
openText: '开',
|
||||
closeText: '关',
|
||||
copyLinkText: '复制链接',
|
||||
refreshLinkText: '刷新链接',
|
||||
demo: '演示',
|
||||
embedInWebsite: '嵌入第三方',
|
||||
accessControl: '访问限制',
|
||||
displaySetting: '显示设置',
|
||||
apiAccessCredentials: 'API 访问凭据',
|
||||
apiKey: 'API Key',
|
||||
refreshToken: {
|
||||
msgConfirm1: '是否重新生成公开访问链接?',
|
||||
msgConfirm2:
|
||||
'重新生成公开访问链接会影响嵌入第三方脚本变更,需要将新脚本重新嵌入第三方,请谨慎操作!',
|
||||
refreshSuccess: '刷新成功'
|
||||
},
|
||||
|
||||
APIKeyDialog: {
|
||||
saveSettings: '保存设置',
|
||||
msgConfirm1: '是否删除API Key',
|
||||
msgConfirm2: '删除API Key后将无法恢复,请确认是否删除?',
|
||||
enabledSuccess: '已启用',
|
||||
disabledSuccess: '已禁用'
|
||||
},
|
||||
EditAvatarDialog: {
|
||||
title: '应用头像',
|
||||
customizeUpload: '自定义上传',
|
||||
upload: '上传',
|
||||
default: '默认logo',
|
||||
custom: '自定义',
|
||||
sizeTip: '建议尺寸 32*32,支持 JPG、PNG、GIF,大小不超过 10 MB',
|
||||
fileSizeExceeded: '文件大小超过 10 MB',
|
||||
uploadImagePrompt: '请上传一张图片'
|
||||
},
|
||||
EmbedDialog: {
|
||||
fullscreenModeTitle: '全屏模式',
|
||||
copyInstructions: '复制以下代码进行嵌入',
|
||||
floatingModeTitle: '浮窗模式',
|
||||
mobileModeTitle: '移动端模式'
|
||||
},
|
||||
LimitDialog: {
|
||||
showSourceLabel: '显示知识来源',
|
||||
clientQueryLimitLabel: '每个客户端提问限制',
|
||||
timesDays: '次/天',
|
||||
authentication: '身份验证',
|
||||
authenticationValue: '验证密码',
|
||||
whitelistLabel: '白名单',
|
||||
whitelistPlaceholder:
|
||||
'请输入允许嵌入第三方的源地址,一行一个,如:\nhttp://127.0.0.1:5678\nhttps://dataease.io'
|
||||
},
|
||||
SettingAPIKeyDialog: {
|
||||
dialogTitle: '设置',
|
||||
allowCrossDomainLabel: '允许跨域地址',
|
||||
crossDomainPlaceholder:
|
||||
'请输入允许的跨域地址,开启后不输入跨域地址则不限制。\n跨域地址一行一个,如:\nhttp://127.0.0.1:5678 \nhttps://dataease.io'
|
||||
},
|
||||
SettingDisplayDialog: {
|
||||
dialogTitle: '显示设置',
|
||||
languageLabel: '语言',
|
||||
showSourceLabel: '显示知识来源',
|
||||
showExecutionDetail: '显示执行详情',
|
||||
restoreDefault: '恢复默认',
|
||||
customThemeColor: '自定义主题色',
|
||||
headerTitleFontColor: '头部标题字体颜色',
|
||||
default: '默认',
|
||||
askUserAvatar: '提问用户头像',
|
||||
replace: '替换',
|
||||
imageMessage: '建议尺寸 32*32,支持 JPG、PNG、GIF,大小不超过 10 MB',
|
||||
AIAvatar: 'AI 回复头像',
|
||||
display: '显示',
|
||||
floatIcon: '浮窗入口图标',
|
||||
iconDefaultPosition: '图标默认位置',
|
||||
iconPosition: {
|
||||
left: '左',
|
||||
right: '右',
|
||||
bottom: '下',
|
||||
top: '上'
|
||||
},
|
||||
draggablePosition: '可拖拽位置',
|
||||
showHistory: '显示历史记录',
|
||||
displayGuide: '显示引导图(浮窗模式)',
|
||||
disclaimer: '免责声明',
|
||||
disclaimerValue: '「以上内容均由 AI 生成,仅供参考和借鉴」'
|
||||
}
|
||||
},
|
||||
monitor: {
|
||||
monitoringStatistics: '监控统计',
|
||||
customRange: '自定义范围',
|
||||
startDatePlaceholder: '开始时间',
|
||||
endDatePlaceholder: '结束时间',
|
||||
pastDayOptions: {
|
||||
past7Days: '过去7天',
|
||||
past30Days: '过去30天',
|
||||
past90Days: '过去90天',
|
||||
past183Days: '过去半年',
|
||||
other: '自定义'
|
||||
},
|
||||
charts: {
|
||||
customerTotal: '用户总数',
|
||||
customerNew: '用户新增数',
|
||||
queryCount: '提问次数',
|
||||
tokensTotal: 'Tokens 总数',
|
||||
userSatisfaction: '用户满意度',
|
||||
approval: '赞同',
|
||||
disapproval: '反对'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,302 @@
|
|||
export default {
|
||||
node: '节点',
|
||||
nodeName: '节点名称',
|
||||
baseComponent: '基础组件',
|
||||
nodeSetting: '节点设置',
|
||||
workflow: '工作流',
|
||||
searchBar: {
|
||||
placeholder: '按名称搜索'
|
||||
},
|
||||
info: {
|
||||
previewVersion: '预览版本:',
|
||||
saveTime: '保存时间:'
|
||||
},
|
||||
setting: {
|
||||
restoreVersion: '恢复版本',
|
||||
restoreCurrentVersion: '恢复此版本',
|
||||
addComponent: '添加组件',
|
||||
public: '发布',
|
||||
releaseHistory: '发布历史',
|
||||
autoSave: '自动保存',
|
||||
latestRelease: '最近发布',
|
||||
copyParam: '复制参数',
|
||||
debug: '调试',
|
||||
exit: '直接退出',
|
||||
exitSave: '保存并退出'
|
||||
},
|
||||
tip: {
|
||||
publicSuccess: '发布成功',
|
||||
noData: '没有找到相关结果',
|
||||
nameMessage: '名字不能为空!',
|
||||
onlyRight: '只允许从右边的锚点连出',
|
||||
notRecyclable: '不可循环连线',
|
||||
onlyLeft: '只允许连接左边的锚点',
|
||||
applicationNodeError: '该应用不可用',
|
||||
functionNodeError: '该函数不可用',
|
||||
repeatedNodeError: '节点名称已存在!',
|
||||
cannotCopy: '不能被复制',
|
||||
copyError: '已复制节点',
|
||||
paramErrorMessage: '参数已存在: ',
|
||||
saveMessage: '当前的更改尚未保存,是否保存后退出?'
|
||||
},
|
||||
delete: {
|
||||
confirmTitle: '确定删除该节点?',
|
||||
deleteMessage: '节点不允许删除'
|
||||
},
|
||||
control: {
|
||||
zoomOut: '缩小',
|
||||
zoomIn: '放大',
|
||||
fitView: '适应',
|
||||
retract: '收起全部节点',
|
||||
extend: '展开全部节点',
|
||||
beautify: '一键美化'
|
||||
},
|
||||
variable: {
|
||||
label: '变量',
|
||||
global: '全局变量',
|
||||
Referencing: '引用变量',
|
||||
ReferencingRequired: '引用变量必填',
|
||||
ReferencingError: '引用变量错误',
|
||||
NoReferencing: '不存在的引用变量',
|
||||
placeholder: '请选择变量'
|
||||
},
|
||||
condition: {
|
||||
title: '执行条件',
|
||||
front: '前置',
|
||||
AND: '所有',
|
||||
OR: '任一',
|
||||
text: '连线节点执行完,执行当前节点'
|
||||
},
|
||||
validate: {
|
||||
startNodeRequired: '开始节点必填',
|
||||
startNodeOnly: '开始节点只能有一个',
|
||||
baseNodeRequired: '基本信息节点必填',
|
||||
baseNodeOnly: '基本信息节点只能有一个',
|
||||
notInWorkFlowNode: '未在流程中的节点',
|
||||
noNextNode: '不存在的下一个节点',
|
||||
nodeUnavailable: '节点不可用',
|
||||
needConnect1: '节点的',
|
||||
needConnect2: '分支需要连接',
|
||||
cannotEndNode: '节点不能当做结束节点'
|
||||
},
|
||||
nodes: {
|
||||
startNode: {
|
||||
label: '开始',
|
||||
question: '用户问题',
|
||||
currentTime: '当前时间'
|
||||
},
|
||||
baseNode: {
|
||||
label: '基本信息',
|
||||
appName: {
|
||||
label: '应用名称'
|
||||
},
|
||||
appDescription: {
|
||||
label: '应用描述'
|
||||
},
|
||||
fileUpload: {
|
||||
label: '文件上传',
|
||||
tooltip: '开启后,问答页面会显示上传文件的按钮。'
|
||||
},
|
||||
FileUploadSetting: {
|
||||
title: '文件上传设置',
|
||||
maxFiles: '单次上传最多文件数',
|
||||
fileLimit: '每个文件最大(MB)',
|
||||
fileUploadType: {
|
||||
label: '上传的文件类型',
|
||||
documentText: '需要使用“文档内容提取”节点解析文档内容',
|
||||
imageText: '需要使用“视觉模型”节点解析图片内容',
|
||||
audioText: '需要使用“语音转文本”节点解析音频内容',
|
||||
otherText: '需要自行解析该类型文件'
|
||||
},
|
||||
|
||||
}
|
||||
},
|
||||
aiChatNode: {
|
||||
label: 'AI 对话',
|
||||
text: '与 AI 大模型进行对话',
|
||||
answer: 'AI 回答内容',
|
||||
returnContent: {
|
||||
label: '返回内容',
|
||||
tooltip: `关闭后该节点的内容则不输出给用户。
|
||||
如果你想让用户看到该节点的输出内容,请打开开关。`
|
||||
},
|
||||
defaultPrompt: '已知信息',
|
||||
think: '思考过程'
|
||||
},
|
||||
searchDatasetNode: {
|
||||
label: '知识库检索',
|
||||
text: '关联知识库,查找与问题相关的分段',
|
||||
paragraph_list: '检索结果的分段列表',
|
||||
is_hit_handling_method_list: '满足直接回答的分段列表',
|
||||
result: '检索结果',
|
||||
directly_return: '满足直接回答的分段内容',
|
||||
searchParam: '检索参数',
|
||||
searchQuestion: {
|
||||
label: '检索问题',
|
||||
placeholder: '请选择检索问题',
|
||||
requiredMessage: '请选择检索问题'
|
||||
}
|
||||
},
|
||||
questionNode: {
|
||||
label: '问题优化',
|
||||
text: '根据历史聊天记录优化完善当前问题,更利于匹配知识库分段',
|
||||
result: '问题优化结果',
|
||||
defaultPrompt1: `根据上下文优化和完善用户问题`,
|
||||
defaultPrompt2: `请输出一个优化后的问题。`,
|
||||
systemDefault: '你是一个问题优化大师'
|
||||
},
|
||||
conditionNode: {
|
||||
label: '判断器',
|
||||
text: '根据不同条件执行不同的节点',
|
||||
branch_name: '分支名称',
|
||||
conditions: {
|
||||
label: '条件',
|
||||
info: '符合以下',
|
||||
requiredMessage: '请选择条件'
|
||||
},
|
||||
valueMessage: '请输入值',
|
||||
addCondition: '添加条件',
|
||||
addBranch: '添加分支'
|
||||
},
|
||||
replyNode: {
|
||||
label: '指定回复',
|
||||
text: '指定回复内容,引用变量会转换为字符串进行输出',
|
||||
content: '内容',
|
||||
replyContent: {
|
||||
label: '回复内容',
|
||||
custom: '自定义',
|
||||
reference: '引用变量'
|
||||
}
|
||||
},
|
||||
rerankerNode: {
|
||||
label: '多路召回',
|
||||
text: '使用重排模型对多个知识库的检索结果进行二次召回',
|
||||
result_list: '重排结果列表',
|
||||
result: '重排结果',
|
||||
rerankerContent: {
|
||||
label: '重排内容',
|
||||
requiredMessage: '请选择重排内容'
|
||||
},
|
||||
higher: '高于',
|
||||
ScoreTooltip: 'Score越高相关性越强。',
|
||||
max_paragraph_char_number: '最大引用字符数',
|
||||
reranker_model: {
|
||||
label: '重排模型',
|
||||
placeholder: '请选择重排模型'
|
||||
}
|
||||
},
|
||||
formNode: {
|
||||
label: '表单收集',
|
||||
text: '在问答过程中用于收集用户信息,可以根据收集到表单数据执行后续流程',
|
||||
form_content_format1: '你好,请先填写下面表单内容:',
|
||||
form_content_format2: '填写后请点击【提交】按钮进行提交。',
|
||||
form_data: '表单全部内容',
|
||||
formContent: {
|
||||
label: '表单输出内容',
|
||||
requiredMessage: '请表单输出内容',
|
||||
tooltip: '设置执行该节点输出的内容,{ form } 为表单的占位符。'
|
||||
},
|
||||
formAllContent: '表单全部内容',
|
||||
formSetting: '表单配置'
|
||||
},
|
||||
documentExtractNode: {
|
||||
label: '文档内容提取',
|
||||
text: '提取文档中的内容',
|
||||
content: '文档内容'
|
||||
},
|
||||
imageUnderstandNode: {
|
||||
label: '图片理解',
|
||||
text: '识别出图片中的对象、场景等信息回答用户问题',
|
||||
answer: 'AI 回答内容',
|
||||
model: {
|
||||
label: '视觉模型',
|
||||
requiredMessage: '请选择视觉模型'
|
||||
},
|
||||
image: {
|
||||
label: '选择图片',
|
||||
requiredMessage: '请选择图片'
|
||||
}
|
||||
},
|
||||
variableAssignNode: {
|
||||
label: '变量赋值',
|
||||
text: '更新全局变量的值',
|
||||
assign: '赋值'
|
||||
},
|
||||
mcpNode: {
|
||||
label: 'MCP 调用',
|
||||
text: '通过SSE/Streamable HTTP方式执行MCP服务中的工具',
|
||||
getToolsSuccess: '获取工具成功',
|
||||
getTool: '获取工具',
|
||||
tool: '工具',
|
||||
toolParam: '工具参数',
|
||||
mcpServerTip: '请输入JSON格式的MCP服务器配置',
|
||||
mcpToolTip: '请选择工具',
|
||||
configLabel: 'MCP Server Config (仅支持SSE/Streamable HTTP调用方式)'
|
||||
},
|
||||
imageGenerateNode: {
|
||||
label: '图片生成',
|
||||
text: '根据提供的文本内容生成图片',
|
||||
answer: 'AI 回答内容',
|
||||
model: {
|
||||
label: '图片生成模型',
|
||||
requiredMessage: '请选择图片生成模型'
|
||||
},
|
||||
prompt: {
|
||||
label: '提示词(正向)',
|
||||
tooltip: '正向提示词,用来描述生成图像中期望包含的元素和视觉特点'
|
||||
},
|
||||
negative_prompt: {
|
||||
label: '提示词(负向)',
|
||||
tooltip: '反向提示词,用来描述不希望在画面中看到的内容,可以对画面进行限制。',
|
||||
placeholder: '请描述不想生成的图片内容,比如:颜色、血腥内容'
|
||||
}
|
||||
},
|
||||
speechToTextNode: {
|
||||
label: '语音转文本',
|
||||
text: '将音频通过语音识别模型转换为文本',
|
||||
stt_model: {
|
||||
label: '语音识别模型'
|
||||
},
|
||||
audio: {
|
||||
label: '选择语音文件',
|
||||
placeholder: '请选择语音文件'
|
||||
}
|
||||
},
|
||||
textToSpeechNode: {
|
||||
label: '文本转语音',
|
||||
text: '将文本通过语音合成模型转换为音频',
|
||||
tts_model: {
|
||||
label: '语音识别模型'
|
||||
},
|
||||
content: {
|
||||
label: '选择文本内容'
|
||||
}
|
||||
},
|
||||
functionNode: {
|
||||
label: '自定义函数',
|
||||
text: '通过执行自定义脚本,实现数据处理'
|
||||
},
|
||||
applicationNode: {
|
||||
label: '应用节点'
|
||||
}
|
||||
},
|
||||
compare: {
|
||||
is_null: '为空',
|
||||
is_not_null: '不为空',
|
||||
contain: '包含',
|
||||
not_contain: '不包含',
|
||||
eq: '等于',
|
||||
ge: '大于等于',
|
||||
gt: '大于',
|
||||
le: '小于等于',
|
||||
lt: '小于',
|
||||
len_eq: '长度等于',
|
||||
len_ge: '长度大于等于',
|
||||
len_gt: '长度大于',
|
||||
len_le: '长度小于等于',
|
||||
len_lt: '长度小于',
|
||||
is_true: '为真',
|
||||
is_not_true: '不为真'
|
||||
},
|
||||
FileUploadSetting: {}
|
||||
}
|
||||
|
|
@ -8,17 +8,13 @@ import userManage from './user-manage'
|
|||
import resourceAuthorization from './resource-authorization'
|
||||
import application from './application'
|
||||
import problem from './problem'
|
||||
import applicationOverview from './application-overview'
|
||||
import applicationWorkflow from './application-workflow'
|
||||
// import notFound from './404'
|
||||
|
||||
// import applicationOverview from './application-overview'
|
||||
|
||||
// import user from './user'
|
||||
// import team from './team'
|
||||
|
||||
// import paragraph from './paragraph'
|
||||
|
||||
// import log from './log'
|
||||
// import applicationWorkflow from './application-workflow'
|
||||
|
||||
// import operateLog from './operate-log'
|
||||
export default {
|
||||
|
|
@ -32,16 +28,12 @@ export default {
|
|||
resourceAuthorization,
|
||||
application,
|
||||
problem,
|
||||
applicationOverview,
|
||||
applicationWorkflow,
|
||||
// notFound,
|
||||
|
||||
// applicationOverview,
|
||||
|
||||
// user,
|
||||
// team,
|
||||
// paragraph,
|
||||
|
||||
// log,
|
||||
// applicationWorkflow,
|
||||
|
||||
// operateLog
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,94 @@
|
|||
// import Layout from '@/layout/layout-template/DetailLayout.vue'
|
||||
import { ComplexPermission } from '@/utils/permission/type'
|
||||
|
||||
const applicationRouter = {
|
||||
path: '/application',
|
||||
name: 'application',
|
||||
meta: { title: 'views.application.title', permission: 'APPLICATION:READ' },
|
||||
redirect: '/application',
|
||||
component: () => import('@/layout/layout-template/SimpleLayout.vue'),
|
||||
children: [
|
||||
{
|
||||
path: '/application',
|
||||
name: 'application-index',
|
||||
meta: { title: '应用主页', activeMenu: '/application' },
|
||||
component: () => import('@/views/application/index.vue')
|
||||
},
|
||||
// {
|
||||
// path: '/application/:id/:type',
|
||||
// name: 'ApplicationDetail',
|
||||
// meta: { title: '应用详情', activeMenu: '/application' },
|
||||
// component: Layout,
|
||||
// hidden: true,
|
||||
// children: [
|
||||
// {
|
||||
// path: 'overview',
|
||||
// name: 'AppOverview',
|
||||
// meta: {
|
||||
// icon: 'app-all-menu',
|
||||
// iconActive: 'app-all-menu-active',
|
||||
// title: 'views.applicationOverview.title',
|
||||
// active: 'overview',
|
||||
// parentPath: '/application/:id/:type',
|
||||
// parentName: 'ApplicationDetail'
|
||||
// },
|
||||
// component: () => import('@/views/application-overview/index.vue')
|
||||
// },
|
||||
// {
|
||||
// path: 'setting',
|
||||
// name: 'AppSetting',
|
||||
// meta: {
|
||||
// icon: 'app-setting',
|
||||
// iconActive: 'app-setting-active',
|
||||
// title: 'common.setting',
|
||||
// active: 'setting',
|
||||
// parentPath: '/application/:id/:type',
|
||||
// parentName: 'ApplicationDetail'
|
||||
// },
|
||||
// component: () => import('@/views/application/ApplicationSetting.vue')
|
||||
// },
|
||||
// {
|
||||
// path: 'access',
|
||||
// name: 'AppAccess',
|
||||
// meta: {
|
||||
// icon: 'app-access',
|
||||
// iconActive: 'app-access-active',
|
||||
// title: 'views.application.applicationAccess.title',
|
||||
// active: 'access',
|
||||
// parentPath: '/application/:id/:type',
|
||||
// parentName: 'ApplicationDetail',
|
||||
// permission: new ComplexPermission([], ['x-pack'], 'OR')
|
||||
// },
|
||||
// component: () => import('@/views/application/ApplicationAccess.vue')
|
||||
// },
|
||||
// {
|
||||
// path: 'hit-test',
|
||||
// name: 'AppHitTest',
|
||||
// meta: {
|
||||
// icon: 'app-hit-test',
|
||||
// title: 'views.application.hitTest.title',
|
||||
// active: 'hit-test',
|
||||
// parentPath: '/application/:id/:type',
|
||||
// parentName: 'ApplicationDetail'
|
||||
// },
|
||||
// component: () => import('@/views/hit-test/index.vue')
|
||||
// },
|
||||
// // {
|
||||
// // path: 'log',
|
||||
// // name: 'Log',
|
||||
// // meta: {
|
||||
// // icon: 'app-document',
|
||||
// // iconActive: 'app-document-active',
|
||||
// // title: 'views.log.title',
|
||||
// // active: 'log',
|
||||
// // parentPath: '/application/:id/:type',
|
||||
// // parentName: 'ApplicationDetail'
|
||||
// // },
|
||||
// // component: () => import('@/views/log/index.vue')
|
||||
// // }
|
||||
// ]
|
||||
// }
|
||||
]
|
||||
}
|
||||
|
||||
export default applicationRouter
|
||||
|
|
@ -10,6 +10,20 @@ export const routes: Array<RouteRecordRaw> = [
|
|||
children: [...rolesRoutes],
|
||||
},
|
||||
|
||||
// 高级编排
|
||||
{
|
||||
path: '/application/:id/workflow',
|
||||
name: 'ApplicationWorkflow',
|
||||
meta: { activeMenu: '/application' },
|
||||
component: () => import('@/views/application-workflow/index.vue'),
|
||||
},
|
||||
// 对话
|
||||
{
|
||||
path: '/chat/:accessToken',
|
||||
name: 'Chat',
|
||||
component: () => import('@/views/chat/index.vue'),
|
||||
},
|
||||
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
|
|
|
|||
|
|
@ -409,3 +409,11 @@ h5 {
|
|||
background: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
内容部分 自适应高度
|
||||
*/
|
||||
.main-calc-height {
|
||||
height: var(--app-main-height);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,153 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
title="API Key"
|
||||
v-model="dialogVisible"
|
||||
width="800"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
align-center
|
||||
>
|
||||
<el-button type="primary" class="mb-16" @click="createApiKey">
|
||||
{{ $t('common.create') }}
|
||||
</el-button>
|
||||
<el-table :data="apiKey" class="mb-16" :loading="loading" height="420">
|
||||
<el-table-column prop="secret_key" label="API Key">
|
||||
<template #default="{ row }">
|
||||
<span class="vertical-middle lighter break-all">
|
||||
{{ row.secret_key }}
|
||||
</span>
|
||||
<el-button type="primary" text @click="copyClick(row.secret_key)">
|
||||
<AppIcon iconName="app-copy"></AppIcon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('common.status.label')" width="70">
|
||||
<template #default="{ row }">
|
||||
<div @click.stop>
|
||||
<el-switch
|
||||
size="small"
|
||||
v-model="row.is_active"
|
||||
:before-change="() => changeState(row)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="name" :label="$t('common.createDate')" width="170">
|
||||
<template #default="{ row }">
|
||||
{{ datetimeFormat(row.create_time) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('common.operation')" align="left" width="90">
|
||||
<template #default="{ row }">
|
||||
<span class="mr-4">
|
||||
<el-tooltip effect="dark" :content="$t('common.setting')" placement="top">
|
||||
<el-button type="primary" text @click.stop="settingApiKey(row)">
|
||||
<el-icon><Setting /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<el-tooltip effect="dark" :content="$t('common.delete')" placement="top">
|
||||
<el-button type="primary" text @click="deleteApiKey(row)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
<SettingAPIKeyDialog ref="SettingAPIKeyDialogRef" @refresh="refresh" />
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { copyClick } from '@/utils/clipboard'
|
||||
import overviewApi from '@/api/application-overview'
|
||||
import SettingAPIKeyDialog from './SettingAPIKeyDialog.vue'
|
||||
import { datetimeFormat } from '@/utils/time'
|
||||
import { MsgSuccess, MsgConfirm } from '@/utils/message'
|
||||
import { t } from '@/locales'
|
||||
const route = useRoute()
|
||||
const {
|
||||
params: { id }
|
||||
} = route
|
||||
|
||||
const emit = defineEmits(['addData'])
|
||||
|
||||
const SettingAPIKeyDialogRef = ref()
|
||||
const dialogVisible = ref<boolean>(false)
|
||||
const loading = ref(false)
|
||||
const apiKey = ref<any>(null)
|
||||
|
||||
watch(dialogVisible, (bool) => {
|
||||
if (!bool) {
|
||||
apiKey.value = null
|
||||
}
|
||||
})
|
||||
|
||||
function settingApiKey(row: any) {
|
||||
SettingAPIKeyDialogRef.value.open(row, 'APPLICATION')
|
||||
}
|
||||
|
||||
function deleteApiKey(row: any) {
|
||||
MsgConfirm(
|
||||
// @ts-ignore
|
||||
`${t('views.applicationOverview.appInfo.APIKeyDialog.msgConfirm1')}: ${row.secret_key}?`,
|
||||
t('views.applicationOverview.appInfo.APIKeyDialog.msgConfirm2'),
|
||||
{
|
||||
confirmButtonText: t('common.confirm'),
|
||||
cancelButtonText: t('common.cancel'),
|
||||
confirmButtonClass: 'danger'
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
overviewApi.delAPIKey(id as string, row.id, loading).then(() => {
|
||||
MsgSuccess(t('common.deleteSuccess'))
|
||||
getApiKeyList()
|
||||
})
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
function changeState(row: any) {
|
||||
const obj = {
|
||||
is_active: !row.is_active
|
||||
}
|
||||
const str = obj.is_active
|
||||
? t('views.applicationOverview.appInfo.APIKeyDialog.enabledSuccess')
|
||||
: t('views.applicationOverview.appInfo.APIKeyDialog.disabledSuccess')
|
||||
overviewApi
|
||||
.putAPIKey(id as string, row.id, obj, loading)
|
||||
.then((res) => {
|
||||
MsgSuccess(str)
|
||||
getApiKeyList()
|
||||
return true
|
||||
})
|
||||
.catch(() => {
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
function createApiKey() {
|
||||
overviewApi.postAPIKey(id as string, loading).then((res) => {
|
||||
getApiKeyList()
|
||||
})
|
||||
}
|
||||
|
||||
const open = () => {
|
||||
getApiKeyList()
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
function getApiKeyList() {
|
||||
overviewApi.getAPIKey(id as string, loading).then((res) => {
|
||||
apiKey.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
getApiKeyList()
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
:title="$t('views.applicationOverview.appInfo.SettingDisplayDialog.dialogTitle')"
|
||||
v-model="dialogVisible"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
width="550"
|
||||
>
|
||||
<el-form label-position="top" ref="displayFormRef" :model="form">
|
||||
<el-form-item>
|
||||
<span>{{
|
||||
$t('views.applicationOverview.appInfo.SettingDisplayDialog.languageLabel')
|
||||
}}</span>
|
||||
<el-select v-model="form.language" clearable>
|
||||
<el-option
|
||||
v-for="item in langList"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-space direction="vertical" alignment="start">
|
||||
<el-checkbox
|
||||
v-model="form.show_source"
|
||||
:label="
|
||||
isWorkFlow(detail.type)
|
||||
? $t('views.applicationOverview.appInfo.SettingDisplayDialog.showExecutionDetail')
|
||||
: $t('views.applicationOverview.appInfo.SettingDisplayDialog.showSourceLabel')
|
||||
"
|
||||
/>
|
||||
</el-space>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click.prevent="dialogVisible = false">{{ $t('common.cancel') }} </el-button>
|
||||
<el-button type="primary" @click="submit(displayFormRef)" :loading="loading">
|
||||
{{ $t('common.save') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import type { FormInstance, FormRules, UploadFiles } from 'element-plus'
|
||||
import applicationApi from '@/api/application'
|
||||
import { isWorkFlow } from '@/utils/application'
|
||||
import { MsgSuccess, MsgError } from '@/utils/message'
|
||||
import { getBrowserLang, langList, t } from '@/locales'
|
||||
const route = useRoute()
|
||||
const {
|
||||
params: { id }
|
||||
} = route
|
||||
|
||||
const emit = defineEmits(['refresh'])
|
||||
|
||||
const displayFormRef = ref()
|
||||
const form = ref<any>({
|
||||
show_source: false,
|
||||
language: ''
|
||||
})
|
||||
|
||||
const detail = ref<any>(null)
|
||||
|
||||
const dialogVisible = ref<boolean>(false)
|
||||
const loading = ref(false)
|
||||
|
||||
watch(dialogVisible, (bool) => {
|
||||
if (!bool) {
|
||||
form.value = {
|
||||
show_source: false,
|
||||
language: ''
|
||||
}
|
||||
}
|
||||
})
|
||||
const open = (data: any, content: any) => {
|
||||
detail.value = content
|
||||
form.value.show_source = data.show_source
|
||||
form.value.language = data.language
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const submit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate((valid, fields) => {
|
||||
if (valid) {
|
||||
applicationApi.putAccessToken(id as string, form.value, loading).then((res) => {
|
||||
emit('refresh')
|
||||
// @ts-ignore
|
||||
MsgSuccess(t('common.settingSuccess'))
|
||||
dialogVisible.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
:title="$t('views.applicationOverview.appInfo.EditAvatarDialog.title')"
|
||||
v-model="dialogVisible"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
width="550"
|
||||
>
|
||||
<el-radio-group v-model="radioType" class="radio-block mb-16">
|
||||
<el-radio value="default">
|
||||
<p>{{ $t('views.applicationOverview.appInfo.EditAvatarDialog.default') }}</p>
|
||||
<AppAvatar
|
||||
v-if="detail?.name"
|
||||
:name="detail?.name"
|
||||
pinyinColor
|
||||
class="mt-8 mb-8"
|
||||
shape="square"
|
||||
:size="32"
|
||||
/>
|
||||
</el-radio>
|
||||
<el-radio value="custom">
|
||||
<p>{{ $t('views.applicationOverview.appInfo.EditAvatarDialog.customizeUpload') }}</p>
|
||||
<div class="flex mt-8">
|
||||
<AppAvatar
|
||||
v-if="fileURL"
|
||||
shape="square"
|
||||
:size="32"
|
||||
style="background: none"
|
||||
class="mr-16"
|
||||
>
|
||||
<img :src="fileURL" alt="" />
|
||||
</AppAvatar>
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
accept="image/jpeg, image/png, image/gif"
|
||||
:on-change="onChange"
|
||||
>
|
||||
<el-button icon="Upload" :disabled="radioType !== 'custom'">{{
|
||||
$t('views.applicationOverview.appInfo.EditAvatarDialog.upload')
|
||||
}}</el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
<div class="el-upload__tip info mt-8">
|
||||
{{ $t('views.applicationOverview.appInfo.EditAvatarDialog.sizeTip') }}
|
||||
</div>
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click.prevent="dialogVisible = false"> {{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="submit" :loading="loading">
|
||||
{{ $t('common.save') }}</el-button
|
||||
>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import overviewApi from '@/api/application-overview'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { MsgSuccess, MsgError } from '@/utils/message'
|
||||
import { defaultIcon, isAppIcon } from '@/utils/common'
|
||||
import useStore from '@/stores'
|
||||
import { t } from '@/locales'
|
||||
|
||||
const { application } = useStore()
|
||||
|
||||
const route = useRoute()
|
||||
const {
|
||||
params: { id } //应用id
|
||||
} = route
|
||||
|
||||
const emit = defineEmits(['refresh'])
|
||||
|
||||
const iconFile = ref<any>(null)
|
||||
const fileURL = ref<any>(null)
|
||||
|
||||
const dialogVisible = ref<boolean>(false)
|
||||
const loading = ref(false)
|
||||
const detail = ref<any>(null)
|
||||
const radioType = ref('default')
|
||||
|
||||
watch(dialogVisible, (bool) => {
|
||||
if (!bool) {
|
||||
iconFile.value = null
|
||||
fileURL.value = null
|
||||
}
|
||||
})
|
||||
|
||||
const open = (data: any) => {
|
||||
radioType.value = isAppIcon(data.icon) ? 'custom' : 'default'
|
||||
fileURL.value = isAppIcon(data.icon) ? data.icon : null
|
||||
detail.value = cloneDeep(data)
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const onChange = (file: any) => {
|
||||
//1、判断文件大小是否合法,文件限制不能大于10MB
|
||||
const isLimit = file?.size / 1024 / 1024 < 10
|
||||
if (!isLimit) {
|
||||
// @ts-ignore
|
||||
MsgError(t('views.applicationOverview.appInfo.EditAvatarDialog.fileSizeExceeded'))
|
||||
return false
|
||||
} else {
|
||||
iconFile.value = file
|
||||
fileURL.value = URL.createObjectURL(file.raw)
|
||||
}
|
||||
}
|
||||
|
||||
function submit() {
|
||||
if (radioType.value === 'default') {
|
||||
application.asyncPutApplication(id as string, { icon: defaultIcon }, loading).then((res) => {
|
||||
emit('refresh')
|
||||
MsgSuccess(t('common.saveSuccess'))
|
||||
dialogVisible.value = false
|
||||
})
|
||||
} else if (radioType.value === 'custom' && iconFile.value) {
|
||||
let fd = new FormData()
|
||||
fd.append('file', iconFile.value.raw)
|
||||
overviewApi.putAppIcon(id as string, fd, loading).then((res: any) => {
|
||||
emit('refresh')
|
||||
MsgSuccess(t('common.saveSuccess'))
|
||||
dialogVisible.value = false
|
||||
})
|
||||
} else {
|
||||
MsgError(t('views.applicationOverview.appInfo.EditAvatarDialog.uploadImagePrompt'))
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
:title="$t('views.applicationOverview.appInfo.embedInWebsite')"
|
||||
v-model="dialogVisible"
|
||||
width="900"
|
||||
class="embed-dialog"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
>
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="8">
|
||||
<div class="border">
|
||||
<p class="title p-16 bold">
|
||||
{{ $t('views.applicationOverview.appInfo.EmbedDialog.fullscreenModeTitle') }}
|
||||
</p>
|
||||
<img src="@/assets/window1.png" alt="" class="ml-8" height="150" />
|
||||
<div class="code layout-bg border-t p-8">
|
||||
<div class="flex-between p-8">
|
||||
<span class="bold">{{
|
||||
$t('views.applicationOverview.appInfo.EmbedDialog.copyInstructions')
|
||||
}}</span>
|
||||
<el-button text @click="copyClick(source1)">
|
||||
<AppIcon iconName="app-copy"></AppIcon>
|
||||
</el-button>
|
||||
</div>
|
||||
<el-scrollbar height="150" always>
|
||||
<div class="pre-wrap p-8 pt-0">
|
||||
{{ source1 }}
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div class="border">
|
||||
<p class="title p-16 bold">
|
||||
{{ $t('views.applicationOverview.appInfo.EmbedDialog.mobileModeTitle') }}
|
||||
</p>
|
||||
<img src="@/assets/window3.png" alt="" class="ml-8" height="150" />
|
||||
<div class="code layout-bg border-t p-8">
|
||||
<div class="flex-between p-8">
|
||||
<span class="bold">{{
|
||||
$t('views.applicationOverview.appInfo.EmbedDialog.copyInstructions')
|
||||
}}</span>
|
||||
<el-button text @click="copyClick(source3)">
|
||||
<AppIcon iconName="app-copy"></AppIcon>
|
||||
</el-button>
|
||||
</div>
|
||||
<el-scrollbar height="150" always>
|
||||
<div class="pre-wrap p-8 pt-0">
|
||||
{{ source3 }}
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div class="border">
|
||||
<p class="title p-16 bold">
|
||||
{{ $t('views.applicationOverview.appInfo.EmbedDialog.floatingModeTitle') }}
|
||||
</p>
|
||||
<img src="@/assets/window2.png" alt="" class="ml-8" height="150" />
|
||||
<div class="code layout-bg border-t p-8">
|
||||
<div class="flex-between p-8">
|
||||
<span class="bold">{{
|
||||
$t('views.applicationOverview.appInfo.EmbedDialog.copyInstructions')
|
||||
}}</span>
|
||||
<el-button text @click="copyClick(source2)">
|
||||
<AppIcon iconName="app-copy"></AppIcon>
|
||||
</el-button>
|
||||
</div>
|
||||
<el-scrollbar height="150" always>
|
||||
<div class="pre-wrap p-8 pt-0">
|
||||
{{ source2 }}
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { copyClick } from '@/utils/clipboard'
|
||||
import useStore from '@/stores'
|
||||
|
||||
const { application } = useStore()
|
||||
|
||||
const props = defineProps({
|
||||
data: Object,
|
||||
apiInputParams: String
|
||||
})
|
||||
|
||||
const emit = defineEmits(['addData'])
|
||||
|
||||
const dialogVisible = ref<boolean>(false)
|
||||
|
||||
const source1 = ref('')
|
||||
|
||||
const source2 = ref('')
|
||||
const source3 = ref('')
|
||||
|
||||
const urlParams1 = computed(() => (props.apiInputParams ? '?' + props.apiInputParams : ''))
|
||||
const urlParams2 = computed(() => (props.apiInputParams ? '&' + props.apiInputParams : ''))
|
||||
const urlParams3 = computed(() =>
|
||||
props.apiInputParams ? '?mode=mobile&' + props.apiInputParams : '?mode=mobile'
|
||||
)
|
||||
watch(dialogVisible, (bool) => {
|
||||
if (!bool) {
|
||||
source1.value = ''
|
||||
source2.value = ''
|
||||
source3.value = ''
|
||||
}
|
||||
})
|
||||
|
||||
const open = (val: string) => {
|
||||
source1.value = `<iframe
|
||||
src="${application.location + val + urlParams1.value}"
|
||||
style="width: 100%; height: 100%;"
|
||||
frameborder="0"
|
||||
allow="microphone">
|
||||
</iframe>
|
||||
`
|
||||
|
||||
source2.value = `<script
|
||||
async
|
||||
defer
|
||||
src="${window.location.origin}/api/application/embed?protocol=${window.location.protocol.replace(
|
||||
':',
|
||||
''
|
||||
)}&host=${window.location.host}&token=${val}${urlParams2.value}">
|
||||
<\/script>
|
||||
`
|
||||
source3.value = `<iframe
|
||||
src="${application.location + val + urlParams3.value}"
|
||||
style="width: 100%; height: 100%;"
|
||||
frameborder="0"
|
||||
allow="microphone">
|
||||
</iframe>
|
||||
`
|
||||
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.embed-dialog {
|
||||
.title {
|
||||
color: var(--app-text-color) !important;
|
||||
}
|
||||
|
||||
.code {
|
||||
color: var(--app-text-color) !important;
|
||||
|
||||
font-weight: 400;
|
||||
font-size: 13px;
|
||||
white-space: pre;
|
||||
height: 188px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,196 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
:title="$t('views.applicationOverview.appInfo.accessControl')"
|
||||
v-model="dialogVisible"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
width="650"
|
||||
>
|
||||
<el-form label-position="top" ref="limitFormRef" :model="form">
|
||||
<!-- <el-form-item
|
||||
:label="$t('views.applicationOverview.appInfo.LimitDialog.showSourceLabel')"
|
||||
@click.prevent
|
||||
>
|
||||
<el-switch size="small" v-model="form.show_source"></el-switch>
|
||||
</el-form-item> -->
|
||||
<el-form-item
|
||||
:label="$t('views.applicationOverview.appInfo.LimitDialog.clientQueryLimitLabel')"
|
||||
>
|
||||
<el-input-number
|
||||
v-model="form.access_num"
|
||||
:min="0"
|
||||
:step="1"
|
||||
:max="10000"
|
||||
:value-on-clear="0"
|
||||
controls-position="right"
|
||||
style="width: 268px"
|
||||
step-strictly
|
||||
/>
|
||||
<span class="ml-4">{{
|
||||
$t('views.applicationOverview.appInfo.LimitDialog.timesDays')
|
||||
}}</span>
|
||||
</el-form-item>
|
||||
<!-- 身份验证 -->
|
||||
<el-form-item
|
||||
:label="$t('views.applicationOverview.appInfo.LimitDialog.authentication')"
|
||||
v-hasPermission="new ComplexPermission([], ['x-pack'], 'OR')"
|
||||
>
|
||||
<el-switch size="small" v-model="form.authentication" @change="firstGeneration"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
prop="authentication_value"
|
||||
v-if="form.authentication"
|
||||
:label="$t('views.applicationOverview.appInfo.LimitDialog.authenticationValue')"
|
||||
v-hasPermission="new ComplexPermission([], ['x-pack'], 'OR')"
|
||||
>
|
||||
<el-input
|
||||
class="authentication-append-input"
|
||||
v-model="form.authentication_value"
|
||||
readonly
|
||||
style="width: 268px"
|
||||
disabled
|
||||
>
|
||||
<template #append>
|
||||
<el-tooltip :content="$t('common.copy')" placement="top">
|
||||
<el-button
|
||||
type="primary"
|
||||
text
|
||||
@click="copyClick(form.authentication_value)"
|
||||
style="margin: 0 4px !important"
|
||||
>
|
||||
<AppIcon iconName="app-copy"></AppIcon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip :content="$t('common.refresh')" placement="top">
|
||||
<el-button
|
||||
@click="refreshAuthentication"
|
||||
type="primary"
|
||||
text
|
||||
style="margin: 0 4px 0 0 !important"
|
||||
>
|
||||
<el-icon><RefreshRight /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
:label="$t('views.applicationOverview.appInfo.LimitDialog.whitelistLabel')"
|
||||
@click.prevent
|
||||
>
|
||||
<el-switch size="small" v-model="form.white_active"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-input
|
||||
v-model="form.white_list"
|
||||
:placeholder="$t('views.applicationOverview.appInfo.LimitDialog.whitelistPlaceholder')"
|
||||
:rows="10"
|
||||
type="textarea"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click.prevent="dialogVisible = false">{{ $t('common.cancel') }} </el-button>
|
||||
<el-button type="primary" @click="submit(limitFormRef)" :loading="loading">
|
||||
{{ $t('common.save') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import applicationApi from '@/api/application'
|
||||
import { MsgSuccess } from '@/utils/message'
|
||||
import { t } from '@/locales'
|
||||
import { copyClick } from '@/utils/clipboard'
|
||||
import { ComplexPermission } from '@/utils/permission/type'
|
||||
|
||||
const route = useRoute()
|
||||
const {
|
||||
params: { id }
|
||||
} = route
|
||||
|
||||
const emit = defineEmits(['refresh'])
|
||||
|
||||
const limitFormRef = ref()
|
||||
const form = ref<any>({
|
||||
access_num: 0,
|
||||
white_active: true,
|
||||
white_list: '',
|
||||
authentication_value: '',
|
||||
authentication: false
|
||||
})
|
||||
|
||||
const dialogVisible = ref<boolean>(false)
|
||||
const loading = ref(false)
|
||||
|
||||
watch(dialogVisible, (bool) => {
|
||||
if (!bool) {
|
||||
form.value = {
|
||||
access_num: 0,
|
||||
white_active: true,
|
||||
white_list: ''
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const open = (data: any) => {
|
||||
form.value.access_num = data.access_num
|
||||
form.value.white_active = data.white_active
|
||||
form.value.white_list = data.white_list?.length ? data.white_list?.join('\n') : ''
|
||||
form.value.authentication_value = data.authentication_value
|
||||
form.value.authentication = data.authentication
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const submit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate((valid, fields) => {
|
||||
if (valid) {
|
||||
const obj = {
|
||||
white_list: form.value.white_list ? form.value.white_list.split('\n') : [],
|
||||
white_active: form.value.white_active,
|
||||
access_num: form.value.access_num,
|
||||
authentication: form.value.authentication,
|
||||
authentication_value: form.value.authentication_value
|
||||
}
|
||||
applicationApi.putAccessToken(id as string, obj, loading).then((res) => {
|
||||
emit('refresh')
|
||||
// @ts-ignore
|
||||
MsgSuccess(t('common.settingSuccess'))
|
||||
dialogVisible.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
function generateAuthenticationValue(length: number = 10) {
|
||||
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
|
||||
const randomValues = new Uint8Array(length)
|
||||
window.crypto.getRandomValues(randomValues)
|
||||
return Array.from(randomValues)
|
||||
.map((value) => chars[value % chars.length])
|
||||
.join('')
|
||||
}
|
||||
function refreshAuthentication() {
|
||||
form.value.authentication_value = generateAuthenticationValue()
|
||||
}
|
||||
|
||||
function firstGeneration() {
|
||||
if (form.value.authentication && !form.value.authentication_value) {
|
||||
form.value.authentication_value = generateAuthenticationValue()
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.authentication-append-input {
|
||||
.el-input-group__append {
|
||||
padding: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
:title="$t('common.setting')"
|
||||
v-model="dialogVisible"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
>
|
||||
<el-form label-position="top" ref="settingFormRef" :model="form">
|
||||
<el-form-item
|
||||
:label="$t('views.applicationOverview.appInfo.SettingAPIKeyDialog.allowCrossDomainLabel')"
|
||||
@click.prevent
|
||||
>
|
||||
<el-switch size="small" v-model="form.allow_cross_domain"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<el-input
|
||||
v-model="form.cross_domain_list"
|
||||
:placeholder="
|
||||
$t('views.applicationOverview.appInfo.SettingAPIKeyDialog.crossDomainPlaceholder')
|
||||
"
|
||||
:rows="10"
|
||||
type="textarea"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click.prevent="dialogVisible = false">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="submit(settingFormRef)" :loading="loading">
|
||||
{{ $t('common.save') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import overviewApi from '@/api/application-overview'
|
||||
import overviewSystemApi from '@/api/system-api-key'
|
||||
import { MsgSuccess } from '@/utils/message'
|
||||
import { t } from '@/locales'
|
||||
|
||||
const route = useRoute()
|
||||
const {
|
||||
params: { id }
|
||||
} = route
|
||||
|
||||
const emit = defineEmits(['refresh'])
|
||||
|
||||
const settingFormRef = ref()
|
||||
const form = ref<any>({
|
||||
allow_cross_domain: false,
|
||||
cross_domain_list: ''
|
||||
})
|
||||
|
||||
const dialogVisible = ref<boolean>(false)
|
||||
const loading = ref(false)
|
||||
|
||||
const APIKeyId = ref('')
|
||||
const APIType = ref('APPLICATION')
|
||||
|
||||
watch(dialogVisible, (bool) => {
|
||||
if (!bool) {
|
||||
form.value = {
|
||||
allow_cross_domain: false,
|
||||
cross_domain_list: ''
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const open = (data: any, type: string) => {
|
||||
APIKeyId.value = data.id
|
||||
APIType.value = type
|
||||
form.value.allow_cross_domain = data.allow_cross_domain
|
||||
form.value.cross_domain_list = data.cross_domain_list?.length
|
||||
? data.cross_domain_list?.join('\n')
|
||||
: ''
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const submit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate((valid, fields) => {
|
||||
if (valid) {
|
||||
const obj = {
|
||||
allow_cross_domain: form.value.allow_cross_domain,
|
||||
cross_domain_list: form.value.cross_domain_list
|
||||
? form.value.cross_domain_list.split('\n').filter(function (item: string) {
|
||||
return item !== ''
|
||||
})
|
||||
: []
|
||||
}
|
||||
|
||||
const apiCall =
|
||||
APIType.value === 'APPLICATION'
|
||||
? overviewApi.putAPIKey(id as string, APIKeyId.value, obj, loading)
|
||||
: overviewSystemApi.putAPIKey(APIKeyId.value, obj, loading)
|
||||
|
||||
apiCall.then((res) => {
|
||||
emit('refresh')
|
||||
//@ts-ignore
|
||||
MsgSuccess(t('common.settingSuccess'))
|
||||
dialogVisible.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
<template>
|
||||
<el-row :gutter="16">
|
||||
<el-col
|
||||
:xs="12"
|
||||
:sm="12"
|
||||
:md="12"
|
||||
:lg="6"
|
||||
:xl="6"
|
||||
v-for="(item, index) in statisticsType"
|
||||
:key="index"
|
||||
class="mb-16"
|
||||
>
|
||||
<el-card shadow="never">
|
||||
<div class="flex align-center ml-8 mr-8">
|
||||
<el-avatar :size="40" shape="square" :style="{ background: item.background }">
|
||||
<appIcon :iconName="item.icon" :style="{ fontSize: '24px', color: item.color }" />
|
||||
</el-avatar>
|
||||
<div class="ml-12">
|
||||
<p class="color-secondary lighter mb-4">{{ item.name }}</p>
|
||||
<div v-if="item.id !== 'starCharts'" class="flex align-baseline">
|
||||
<h2>{{ numberFormat(item.sum?.[0]) }}</h2>
|
||||
<span v-if="item.sum.length > 1" class="ml-12" style="color: #f54a45"
|
||||
>+{{ numberFormat(item.sum?.[1]) }}</span
|
||||
>
|
||||
</div>
|
||||
<div v-else class="flex align-center mr-8">
|
||||
<AppIcon iconName="app-like-color"></AppIcon>
|
||||
<h2 class="ml-4">{{ item.sum?.[0] }}</h2>
|
||||
<AppIcon class="ml-12" iconName="app-oppose-color"></AppIcon>
|
||||
<h2 class="ml-4">{{ item.sum?.[1] }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="16">
|
||||
<el-col
|
||||
:xs="24"
|
||||
:sm="24"
|
||||
:md="24"
|
||||
:lg="12"
|
||||
:xl="12"
|
||||
v-for="(item, index) in statisticsType"
|
||||
:key="index"
|
||||
class="mb-16"
|
||||
>
|
||||
<el-card shadow="never">
|
||||
<div class="p-8">
|
||||
<AppCharts height="316px" :id="item.id" type="line" :option="item.option" />
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import AppCharts from '@/components/app-charts/index.vue'
|
||||
import { getAttrsArray, getSum, numberFormat } from '@/utils/utils'
|
||||
import { t } from '@/locales'
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
const statisticsType = computed(() => [
|
||||
{
|
||||
id: 'customerCharts',
|
||||
// @ts-ignore
|
||||
name: t('views.applicationOverview.monitor.charts.customerTotal'),
|
||||
icon: 'app-user',
|
||||
background: '#EBF1FF',
|
||||
color: '#3370FF',
|
||||
sum: [
|
||||
getSum(getAttrsArray(props.data, 'customer_num') || 0),
|
||||
getSum(getAttrsArray(props.data, 'customer_added_count') || 0)
|
||||
],
|
||||
option: {
|
||||
title: t('views.applicationOverview.monitor.charts.customerTotal'),
|
||||
xData: getAttrsArray(props.data, 'day'),
|
||||
yData: [
|
||||
{
|
||||
name: t('views.applicationOverview.monitor.charts.customerTotal'),
|
||||
type: 'line',
|
||||
area: true,
|
||||
data: getAttrsArray(props.data, 'customer_num')
|
||||
},
|
||||
{
|
||||
name: t('views.applicationOverview.monitor.charts.customerNew'),
|
||||
type: 'line',
|
||||
area: true,
|
||||
data: getAttrsArray(props.data, 'customer_added_count')
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'chatRecordCharts',
|
||||
name: t('views.applicationOverview.monitor.charts.queryCount'),
|
||||
icon: 'app-question',
|
||||
background: '#FFF3E5',
|
||||
color: '#FF8800',
|
||||
sum: [getSum(getAttrsArray(props.data, 'chat_record_count') || 0)],
|
||||
option: {
|
||||
title: t('views.applicationOverview.monitor.charts.queryCount'),
|
||||
xData: getAttrsArray(props.data, 'day'),
|
||||
yData: [
|
||||
{
|
||||
type: 'line',
|
||||
data: getAttrsArray(props.data, 'chat_record_count')
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'tokensCharts',
|
||||
name: t('views.applicationOverview.monitor.charts.tokensTotal'),
|
||||
icon: 'app-tokens',
|
||||
background: '#E5FBF8',
|
||||
color: '#00D6B9',
|
||||
sum: [getSum(getAttrsArray(props.data, 'tokens_num') || 0)],
|
||||
option: {
|
||||
title: t('views.applicationOverview.monitor.charts.tokensTotal'),
|
||||
xData: getAttrsArray(props.data, 'day'),
|
||||
yData: [
|
||||
{
|
||||
type: 'line',
|
||||
data: getAttrsArray(props.data, 'tokens_num')
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'starCharts',
|
||||
name: t('views.applicationOverview.monitor.charts.userSatisfaction'),
|
||||
icon: 'app-user-stars',
|
||||
background: '#FEEDEC',
|
||||
color: '#F54A45',
|
||||
sum: [
|
||||
getSum(getAttrsArray(props.data, 'star_num') || 0),
|
||||
getSum(getAttrsArray(props.data, 'trample_num') || 0)
|
||||
],
|
||||
option: {
|
||||
title: t('views.applicationOverview.monitor.charts.userSatisfaction'),
|
||||
xData: getAttrsArray(props.data, 'day'),
|
||||
yData: [
|
||||
{
|
||||
name: t('views.applicationOverview.monitor.charts.approval'),
|
||||
type: 'line',
|
||||
data: getAttrsArray(props.data, 'star_num')
|
||||
},
|
||||
{
|
||||
name: t('views.applicationOverview.monitor.charts.disapproval'),
|
||||
type: 'line',
|
||||
data: getAttrsArray(props.data, 'trample_num')
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
])
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,639 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
:title="$t('views.applicationOverview.appInfo.SettingDisplayDialog.dialogTitle')"
|
||||
width="900"
|
||||
v-model="dialogVisible"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
:destroy-on-close="true"
|
||||
align-center
|
||||
class="display-setting-dialog"
|
||||
>
|
||||
<template #header="{ titleId, titleClass }">
|
||||
<div class="flex-between mb-8">
|
||||
<h4 :id="titleId" :class="titleClass">
|
||||
{{ $t('views.applicationOverview.appInfo.SettingDisplayDialog.dialogTitle') }}
|
||||
</h4>
|
||||
<div class="flex align-center">
|
||||
<el-button type="primary" @click.prevent="resetForm" link>
|
||||
<el-icon class="mr-4">
|
||||
<Refresh />
|
||||
</el-icon>
|
||||
{{ $t('views.applicationOverview.appInfo.SettingDisplayDialog.restoreDefault') }}
|
||||
</el-button>
|
||||
<el-divider direction="vertical" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="flex">
|
||||
<div class="setting-preview border border-r-4 mr-16" style="min-width: 400px">
|
||||
<div class="setting-preview-container">
|
||||
<div class="setting-preview-header" :style="customStyle">
|
||||
<div class="flex-between">
|
||||
<div class="flex align-center">
|
||||
<div class="mr-12 ml-24 flex">
|
||||
<AppAvatar
|
||||
v-if="isAppIcon(detail?.icon)"
|
||||
shape="square"
|
||||
:size="32"
|
||||
style="background: none"
|
||||
>
|
||||
<img :src="detail?.icon" alt="" />
|
||||
</AppAvatar>
|
||||
<AppAvatar
|
||||
v-else-if="detail?.name"
|
||||
:name="detail?.name"
|
||||
pinyinColor
|
||||
shape="square"
|
||||
:size="32"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h4 class="ellipsis">
|
||||
{{ detail?.name || $t('views.application.applicationForm.form.appName.label') }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="mr-16">
|
||||
<el-button link>
|
||||
<AppIcon
|
||||
:iconName="'app-magnify'"
|
||||
:style="{
|
||||
color: xpackForm.custom_theme?.header_font_color
|
||||
}"
|
||||
style="font-size: 20px"
|
||||
></AppIcon>
|
||||
</el-button>
|
||||
<el-button link>
|
||||
<el-icon
|
||||
:size="20"
|
||||
class="color-secondary"
|
||||
:style="{
|
||||
color: xpackForm.custom_theme?.header_font_color
|
||||
}"
|
||||
>
|
||||
<Close />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="p-16" style="position: relative">
|
||||
<div class="flex">
|
||||
<div class="avatar" v-if="xpackForm.show_avatar">
|
||||
<el-image
|
||||
v-if="imgUrl.avatar"
|
||||
:src="imgUrl.avatar"
|
||||
alt=""
|
||||
fit="cover"
|
||||
style="width: 28px; height: 28px; display: block"
|
||||
/>
|
||||
<LogoIcon
|
||||
v-else
|
||||
height="28px"
|
||||
style="width: 28px; height: 28px; display: block"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<img
|
||||
src="@/assets/display-bg2.png"
|
||||
alt=""
|
||||
:width="
|
||||
xpackForm.show_avatar
|
||||
? xpackForm.show_user_avatar
|
||||
? '232px'
|
||||
: '270px'
|
||||
: xpackForm.show_user_avatar
|
||||
? '260px'
|
||||
: '300px'
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex mt-4" style="justify-content: flex-end">
|
||||
<img
|
||||
src="@/assets/display-bg3.png"
|
||||
alt=""
|
||||
:width="
|
||||
xpackForm.show_user_avatar
|
||||
? xpackForm.show_avatar
|
||||
? '227px'
|
||||
: '255px'
|
||||
: xpackForm.show_avatar
|
||||
? '265px'
|
||||
: '292px'
|
||||
"
|
||||
style="object-fit: contain"
|
||||
/>
|
||||
<div class="avatar ml-8" v-if="xpackForm.show_user_avatar">
|
||||
<el-image
|
||||
v-if="imgUrl.user_avatar"
|
||||
:src="imgUrl.user_avatar"
|
||||
alt=""
|
||||
fit="cover"
|
||||
style="width: 28px; height: 28px; display: block"
|
||||
/>
|
||||
<AppAvatar v-else>
|
||||
<img src="@/assets/user-icon.svg" style="width: 54%" alt="" />
|
||||
</AppAvatar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
style="position: absolute; bottom: 0; padding-bottom: 8px; box-sizing: border-box"
|
||||
class="p-16 text-center w-full"
|
||||
>
|
||||
<img src="@/assets/display-bg1.png" alt="" class="w-full" />
|
||||
<el-text type="info" v-if="xpackForm.disclaimer" class="mt-8" style="font-size: 12px">
|
||||
<auto-tooltip :content="xpackForm.disclaimer_value">
|
||||
{{ xpackForm.disclaimer_value }}
|
||||
</auto-tooltip>
|
||||
</el-text>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="float_icon">
|
||||
<el-image
|
||||
v-if="imgUrl.float_icon"
|
||||
:src="imgUrl.float_icon"
|
||||
alt=""
|
||||
fit="cover"
|
||||
style="width: 40px; height: 40px; display: block"
|
||||
/>
|
||||
<img
|
||||
v-else
|
||||
src="/MaxKB.gif"
|
||||
height="50px"
|
||||
style="width: 40px; height: 40px; display: block"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-form ref="displayFormRef" :model="xpackForm">
|
||||
<el-row class="w-full mb-8">
|
||||
<el-col :span="12">
|
||||
<h5 class="mb-8">
|
||||
{{ $t('views.applicationOverview.appInfo.SettingDisplayDialog.customThemeColor') }}
|
||||
</h5>
|
||||
<div>
|
||||
<el-color-picker v-model="xpackForm.custom_theme.theme_color" />
|
||||
{{
|
||||
!xpackForm.custom_theme.theme_color
|
||||
? $t('views.applicationOverview.appInfo.SettingDisplayDialog.default')
|
||||
: ''
|
||||
}}
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<h5 class="mb-8">
|
||||
{{
|
||||
$t('views.applicationOverview.appInfo.SettingDisplayDialog.headerTitleFontColor')
|
||||
}}
|
||||
</h5>
|
||||
<el-color-picker v-model="xpackForm.custom_theme.header_font_color" />
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row class="w-full mb-8">
|
||||
<h5 class="mb-8">
|
||||
{{ $t('views.applicationOverview.appInfo.SettingDisplayDialog.languageLabel') }}
|
||||
</h5>
|
||||
<el-select v-model="xpackForm.language" clearable>
|
||||
<el-option
|
||||
v-for="item in langList"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-row>
|
||||
<el-card shadow="never" class="mb-8">
|
||||
<div class="flex-between mb-8">
|
||||
<span class="lighter">{{
|
||||
$t('views.applicationOverview.appInfo.SettingDisplayDialog.AIAvatar')
|
||||
}}</span>
|
||||
<span class="flex align-center">
|
||||
<el-checkbox v-model="xpackForm.show_avatar">{{
|
||||
$t('views.applicationOverview.appInfo.SettingDisplayDialog.display')
|
||||
}}</el-checkbox>
|
||||
<el-upload
|
||||
class="ml-8"
|
||||
ref="uploadRef"
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
accept="image/jpeg, image/png, image/gif"
|
||||
:on-change="(file: any, fileList: any) => onChange(file, fileList, 'avatar')"
|
||||
>
|
||||
<el-button size="small">
|
||||
{{ $t('views.applicationOverview.appInfo.SettingDisplayDialog.replace') }}
|
||||
</el-button>
|
||||
</el-upload>
|
||||
</span>
|
||||
</div>
|
||||
<el-text type="info" size="small">
|
||||
{{ $t('views.applicationOverview.appInfo.SettingDisplayDialog.imageMessage') }}
|
||||
</el-text>
|
||||
</el-card>
|
||||
<el-card shadow="never" class="mb-8">
|
||||
<div class="flex-between mb-8">
|
||||
<span class="lighter">{{
|
||||
$t('views.applicationOverview.appInfo.SettingDisplayDialog.askUserAvatar')
|
||||
}}</span>
|
||||
<span class="flex align-center">
|
||||
<el-checkbox v-model="xpackForm.show_user_avatar">
|
||||
{{
|
||||
$t('views.applicationOverview.appInfo.SettingDisplayDialog.display')
|
||||
}}</el-checkbox
|
||||
>
|
||||
<el-upload
|
||||
class="ml-8"
|
||||
ref="uploadRef"
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
accept="image/jpeg, image/png, image/gif"
|
||||
:on-change="(file: any, fileList: any) => onChange(file, fileList, 'user_avatar')"
|
||||
>
|
||||
<el-button size="small">
|
||||
{{ $t('views.applicationOverview.appInfo.SettingDisplayDialog.replace') }}
|
||||
</el-button>
|
||||
</el-upload>
|
||||
</span>
|
||||
</div>
|
||||
<el-text type="info" size="small"
|
||||
>{{ $t('views.applicationOverview.appInfo.SettingDisplayDialog.imageMessage') }}
|
||||
</el-text>
|
||||
</el-card>
|
||||
<el-card shadow="never" class="mb-8">
|
||||
<div class="flex-between mb-8">
|
||||
<span class="lighter">{{
|
||||
$t('views.applicationOverview.appInfo.SettingDisplayDialog.floatIcon')
|
||||
}}</span>
|
||||
<el-upload
|
||||
ref="uploadRef"
|
||||
action="#"
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
accept="image/jpeg, image/png, image/gif"
|
||||
:on-change="(file: any, fileList: any) => onChange(file, fileList, 'float_icon')"
|
||||
>
|
||||
<el-button size="small">
|
||||
{{ $t('views.applicationOverview.appInfo.SettingDisplayDialog.replace') }}
|
||||
</el-button>
|
||||
</el-upload>
|
||||
</div>
|
||||
<el-text type="info" size="small">
|
||||
{{ $t('views.applicationOverview.appInfo.SettingDisplayDialog.imageMessage') }}
|
||||
</el-text>
|
||||
<div class="border-t mt-8">
|
||||
<div class="flex-between mb-8">
|
||||
<span class="lighter">{{
|
||||
$t('views.applicationOverview.appInfo.SettingDisplayDialog.iconDefaultPosition')
|
||||
}}</span>
|
||||
<el-checkbox
|
||||
v-model="xpackForm.draggable"
|
||||
:label="
|
||||
$t('views.applicationOverview.appInfo.SettingDisplayDialog.draggablePosition')
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<el-row :gutter="8" class="w-full mb-8">
|
||||
<el-col :span="12">
|
||||
<div class="flex align-center">
|
||||
<el-select v-model="xpackForm.float_location.x.type" style="width: 80px">
|
||||
<el-option
|
||||
:label="
|
||||
$t(
|
||||
'views.applicationOverview.appInfo.SettingDisplayDialog.iconPosition.left'
|
||||
)
|
||||
"
|
||||
value="left"
|
||||
/>
|
||||
<el-option
|
||||
:label="
|
||||
$t(
|
||||
'views.applicationOverview.appInfo.SettingDisplayDialog.iconPosition.right'
|
||||
)
|
||||
"
|
||||
value="right"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input-number
|
||||
v-model="xpackForm.float_location.x.value"
|
||||
:min="0"
|
||||
:step="1"
|
||||
:precision="0"
|
||||
:value-on-clear="0"
|
||||
step-strictly
|
||||
controls-position="right"
|
||||
/>
|
||||
<span class="ml-4">px</span>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="flex align-center">
|
||||
<el-select v-model="xpackForm.float_location.y.type" style="width: 80px">
|
||||
<el-option
|
||||
:label="
|
||||
$t(
|
||||
'views.applicationOverview.appInfo.SettingDisplayDialog.iconPosition.top'
|
||||
)
|
||||
"
|
||||
value="top"
|
||||
/>
|
||||
<el-option
|
||||
:label="
|
||||
$t(
|
||||
'views.applicationOverview.appInfo.SettingDisplayDialog.iconPosition.bottom'
|
||||
)
|
||||
"
|
||||
value="bottom"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input-number
|
||||
v-model="xpackForm.float_location.y.value"
|
||||
:min="0"
|
||||
:step="1"
|
||||
:precision="0"
|
||||
:value-on-clear="0"
|
||||
step-strictly
|
||||
controls-position="right"
|
||||
/>
|
||||
<span class="ml-4">px</span>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-space direction="vertical" alignment="start" :size="2">
|
||||
<el-checkbox
|
||||
v-model="xpackForm.show_source"
|
||||
:label="
|
||||
isWorkFlow(detail.type)
|
||||
? $t('views.applicationOverview.appInfo.SettingDisplayDialog.showExecutionDetail')
|
||||
: $t('views.applicationOverview.appInfo.SettingDisplayDialog.showSourceLabel')
|
||||
"
|
||||
/>
|
||||
<el-checkbox
|
||||
v-model="xpackForm.show_history"
|
||||
:label="$t('views.applicationOverview.appInfo.SettingDisplayDialog.showHistory')"
|
||||
/>
|
||||
<el-checkbox
|
||||
v-model="xpackForm.show_guide"
|
||||
:label="$t('views.applicationOverview.appInfo.SettingDisplayDialog.displayGuide')"
|
||||
/>
|
||||
<el-checkbox
|
||||
v-model="xpackForm.disclaimer"
|
||||
:label="$t('views.applicationOverview.appInfo.SettingDisplayDialog.disclaimer')"
|
||||
@change="changeDisclaimer"
|
||||
/>
|
||||
<span v-if="xpackForm.disclaimer"
|
||||
><el-tooltip :content="xpackForm.disclaimer_value" placement="top">
|
||||
<el-input
|
||||
v-model="xpackForm.disclaimer_value"
|
||||
style="width: 422px; margin-bottom: 10px"
|
||||
@change="changeValue"
|
||||
:maxlength="128"
|
||||
/> </el-tooltip
|
||||
></span>
|
||||
</el-space>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click.prevent="dialogVisible = false">{{ $t('common.cancel') }} </el-button>
|
||||
<el-button type="primary" @click="submit(displayFormRef)" :loading="loading">
|
||||
{{ $t('common.save') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import type { FormInstance, FormRules, UploadFiles } from 'element-plus'
|
||||
import { isWorkFlow } from '@/utils/application'
|
||||
import { isAppIcon } from '@/utils/common'
|
||||
import applicationXpackApi from '@/api/application-xpack'
|
||||
import { MsgSuccess, MsgError } from '@/utils/message'
|
||||
import { langList, t } from '@/locales'
|
||||
import useStore from '@/stores'
|
||||
import { cloneDeep } from 'lodash'
|
||||
|
||||
const { user } = useStore()
|
||||
|
||||
const route = useRoute()
|
||||
const {
|
||||
params: { id }
|
||||
} = route
|
||||
|
||||
const emit = defineEmits(['refresh'])
|
||||
|
||||
const defaultSetting = {
|
||||
show_source: false,
|
||||
language: '',
|
||||
show_history: true,
|
||||
draggable: true,
|
||||
show_guide: true,
|
||||
avatar: '',
|
||||
avatar_url: '',
|
||||
float_icon: '',
|
||||
float_icon_url: '',
|
||||
user_avatar: '',
|
||||
user_avatar_url: '',
|
||||
disclaimer: false,
|
||||
disclaimer_value: t('views.applicationOverview.appInfo.SettingDisplayDialog.disclaimerValue'),
|
||||
custom_theme: {
|
||||
theme_color: '',
|
||||
header_font_color: '#1f2329'
|
||||
},
|
||||
float_location: {
|
||||
y: { type: 'bottom', value: 30 },
|
||||
x: { type: 'right', value: 0 }
|
||||
},
|
||||
show_avatar: true,
|
||||
show_user_avatar: false
|
||||
}
|
||||
|
||||
const displayFormRef = ref()
|
||||
|
||||
const xpackForm = ref<any>({
|
||||
show_source: false,
|
||||
language: '',
|
||||
show_history: false,
|
||||
draggable: false,
|
||||
show_guide: false,
|
||||
avatar: '',
|
||||
avatar_url: '',
|
||||
float_icon: '',
|
||||
float_icon_url: '',
|
||||
user_avatar: '',
|
||||
user_avatar_url: '',
|
||||
disclaimer: false,
|
||||
disclaimer_value: t('views.applicationOverview.appInfo.SettingDisplayDialog.disclaimerValue'),
|
||||
custom_theme: {
|
||||
theme_color: '',
|
||||
header_font_color: '#1f2329'
|
||||
},
|
||||
float_location: {
|
||||
y: { type: 'bottom', value: 30 },
|
||||
x: { type: 'right', value: 0 }
|
||||
},
|
||||
show_avatar: true,
|
||||
show_user_avatar: false
|
||||
})
|
||||
|
||||
const imgUrl = ref<any>({
|
||||
avatar: '',
|
||||
float_icon: '',
|
||||
user_avatar: ''
|
||||
})
|
||||
|
||||
const dialogVisible = ref<boolean>(false)
|
||||
const loading = ref(false)
|
||||
|
||||
const detail = ref<any>(null)
|
||||
|
||||
const customStyle = computed(() => {
|
||||
return {
|
||||
background: xpackForm.value.custom_theme?.theme_color,
|
||||
color: xpackForm.value.custom_theme?.header_font_color
|
||||
}
|
||||
})
|
||||
|
||||
function resetForm() {
|
||||
xpackForm.value = cloneDeep(defaultSetting)
|
||||
imgUrl.value = {
|
||||
avatar: '',
|
||||
float_icon: '',
|
||||
user_avatar: ''
|
||||
}
|
||||
}
|
||||
|
||||
const onChange = (file: any, fileList: UploadFiles, attr: string) => {
|
||||
//1、判断文件大小是否合法,文件限制不能大于 10 MB
|
||||
const isLimit = file?.size / 1024 / 1024 < 10
|
||||
if (!isLimit) {
|
||||
// @ts-ignore
|
||||
MsgError(t('views.applicationOverview.appInfo.EditAvatarDialog.fileSizeExceeded'))
|
||||
return false
|
||||
} else {
|
||||
xpackForm.value[attr] = file.raw
|
||||
imgUrl.value[attr] = URL.createObjectURL(file.raw)
|
||||
xpackForm.value[`${attr}_url`] = ''
|
||||
}
|
||||
}
|
||||
|
||||
const open = (data: any, content: any) => {
|
||||
detail.value = content
|
||||
xpackForm.value.show_source = data.show_source
|
||||
xpackForm.value.show_history = data.show_history
|
||||
xpackForm.value.language = data.language
|
||||
xpackForm.value.draggable = data.draggable
|
||||
xpackForm.value.show_guide = data.show_guide
|
||||
imgUrl.value.avatar = data.avatar
|
||||
imgUrl.value.float_icon = data.float_icon
|
||||
imgUrl.value.user_avatar = data.user_avatar
|
||||
xpackForm.value.disclaimer = data.disclaimer
|
||||
xpackForm.value.disclaimer_value = data.disclaimer_value
|
||||
if (
|
||||
xpackForm.value.disclaimer_value ===
|
||||
t('views.applicationOverview.appInfo.SettingDisplayDialog.disclaimerValue')
|
||||
) {
|
||||
xpackForm.value.disclaimer_value = t(
|
||||
'views.applicationOverview.appInfo.SettingDisplayDialog.disclaimerValue'
|
||||
)
|
||||
}
|
||||
xpackForm.value.avatar_url = data.avatar
|
||||
xpackForm.value.user_avatar_url = data.user_avatar
|
||||
xpackForm.value.float_icon_url = data.float_icon
|
||||
xpackForm.value.show_avatar = data.show_avatar
|
||||
xpackForm.value.show_user_avatar = data.show_user_avatar
|
||||
xpackForm.value.custom_theme = {
|
||||
theme_color: data.custom_theme?.theme_color || '',
|
||||
header_font_color: data.custom_theme?.header_font_color || '#1f2329'
|
||||
}
|
||||
xpackForm.value.float_location = data.float_location
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const changeValue = (value: string) => {
|
||||
xpackForm.value.disclaimer_value = value
|
||||
}
|
||||
|
||||
const changeDisclaimer = (value: boolean) => {
|
||||
xpackForm.value.disclaimer = value
|
||||
}
|
||||
|
||||
const submit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate((valid, fields) => {
|
||||
if (valid) {
|
||||
let fd = new FormData()
|
||||
Object.keys(xpackForm.value).map((item) => {
|
||||
if (['custom_theme', 'float_location'].includes(item)) {
|
||||
fd.append(item, JSON.stringify(xpackForm.value[item]))
|
||||
} else {
|
||||
fd.append(item, xpackForm.value[item])
|
||||
}
|
||||
})
|
||||
applicationXpackApi.putAccessToken(id as string, fd, loading).then((res) => {
|
||||
emit('refresh')
|
||||
// @ts-ignore
|
||||
MsgSuccess(t('common.settingSuccess'))
|
||||
dialogVisible.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.setting-preview {
|
||||
background: #f5f6f7;
|
||||
height: 570px;
|
||||
position: relative;
|
||||
|
||||
.float_icon {
|
||||
position: absolute;
|
||||
right: 8px;
|
||||
bottom: 15px;
|
||||
}
|
||||
|
||||
.setting-preview-container {
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 25px;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ffffff;
|
||||
background: var(--dialog-bg-gradient-color);
|
||||
box-shadow: 0px 4px 8px 0px rgba(31, 35, 41, 0.1);
|
||||
overflow: hidden;
|
||||
width: 330px;
|
||||
height: 520px;
|
||||
|
||||
.setting-preview-header {
|
||||
background: var(--app-header-bg-color);
|
||||
height: var(--app-header-height);
|
||||
line-height: var(--app-header-height);
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.display-setting-dialog {
|
||||
.el-dialog__header {
|
||||
padding-right: 8px;
|
||||
}
|
||||
|
||||
.el-dialog__headerbtn {
|
||||
top: 8px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,434 @@
|
|||
<template>
|
||||
<LayoutContainer :header="$t('views.applicationOverview.title')">
|
||||
<el-scrollbar>
|
||||
<div class="main-calc-height p-24">
|
||||
<h4 class="title-decoration-1 mb-16">
|
||||
{{ $t('views.applicationOverview.appInfo.header') }}
|
||||
</h4>
|
||||
<el-card shadow="never" class="overview-card" v-loading="loading">
|
||||
<div class="title flex align-center">
|
||||
<div
|
||||
class="edit-avatar mr-12"
|
||||
@mouseenter="showEditIcon = true"
|
||||
@mouseleave="showEditIcon = false"
|
||||
>
|
||||
<AppAvatar
|
||||
v-if="isAppIcon(detail?.icon)"
|
||||
shape="square"
|
||||
:size="32"
|
||||
style="background: none"
|
||||
>
|
||||
<img :src="detail?.icon" alt="" />
|
||||
</AppAvatar>
|
||||
<AppAvatar
|
||||
v-else-if="detail?.name"
|
||||
:name="detail?.name"
|
||||
pinyinColor
|
||||
shape="square"
|
||||
:size="32"
|
||||
/>
|
||||
<AppAvatar
|
||||
v-if="showEditIcon"
|
||||
shape="square"
|
||||
class="edit-mask"
|
||||
:size="32"
|
||||
@click="openEditAvatar"
|
||||
>
|
||||
<el-icon><EditPen /></el-icon>
|
||||
</AppAvatar>
|
||||
</div>
|
||||
|
||||
<h4>{{ detail?.name }}</h4>
|
||||
</div>
|
||||
|
||||
<el-row :gutter="12">
|
||||
<el-col :span="12" class="mt-16">
|
||||
<div class="flex">
|
||||
<el-text type="info">{{
|
||||
$t('views.applicationOverview.appInfo.publicAccessLink')
|
||||
}}</el-text>
|
||||
<el-switch
|
||||
v-model="accessToken.is_active"
|
||||
class="ml-8"
|
||||
size="small"
|
||||
inline-prompt
|
||||
:active-text="$t('views.applicationOverview.appInfo.openText')"
|
||||
:inactive-text="$t('views.applicationOverview.appInfo.closeText')"
|
||||
:before-change="() => changeState(accessToken.is_active)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 mb-16 url-height flex align-center" style="margin-bottom: 37px">
|
||||
<span class="vertical-middle lighter break-all ellipsis-1">
|
||||
{{ shareUrl }}
|
||||
</span>
|
||||
<el-tooltip effect="dark" :content="$t('common.copy')" placement="top">
|
||||
<el-button type="primary" text @click="copyClick(shareUrl)">
|
||||
<AppIcon iconName="app-copy"></AppIcon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-tooltip effect="dark" :content="$t('common.refresh')" placement="top">
|
||||
<el-button
|
||||
@click="refreshAccessToken"
|
||||
type="primary"
|
||||
text
|
||||
style="margin-left: 1px"
|
||||
>
|
||||
<el-icon><RefreshRight /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div>
|
||||
<el-button
|
||||
v-if="accessToken?.is_active"
|
||||
:disabled="!accessToken?.is_active"
|
||||
type="primary"
|
||||
tag="a"
|
||||
:href="shareUrl"
|
||||
target="_blank"
|
||||
>
|
||||
{{ $t('views.applicationOverview.appInfo.demo') }}
|
||||
</el-button>
|
||||
<el-button v-else :disabled="!accessToken?.is_active" type="primary">
|
||||
{{ $t('views.applicationOverview.appInfo.demo') }}
|
||||
</el-button>
|
||||
<el-button :disabled="!accessToken?.is_active" @click="openDialog">
|
||||
{{ $t('views.applicationOverview.appInfo.embedInWebsite') }}
|
||||
</el-button>
|
||||
<el-button @click="openLimitDialog">
|
||||
{{ $t('views.applicationOverview.appInfo.accessControl') }}
|
||||
</el-button>
|
||||
<el-button @click="openDisplaySettingDialog">
|
||||
{{ $t('views.applicationOverview.appInfo.displaySetting') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="12" class="mt-16">
|
||||
<div class="flex">
|
||||
<el-text type="info"
|
||||
>{{ $t('views.applicationOverview.appInfo.apiAccessCredentials') }}
|
||||
</el-text>
|
||||
</div>
|
||||
<div class="mt-4 mb-16 url-height">
|
||||
<div>
|
||||
<el-text>API {{ $t('common.fileUpload.document') }}:</el-text
|
||||
><el-button
|
||||
type="primary"
|
||||
link
|
||||
@click="toUrl(apiUrl)"
|
||||
class="vertical-middle lighter break-all"
|
||||
>
|
||||
{{ apiUrl }}
|
||||
</el-button>
|
||||
</div>
|
||||
<div class="flex align-center">
|
||||
<span class="flex">
|
||||
<el-text style="width: 80px">Base URL:</el-text>
|
||||
</span>
|
||||
|
||||
<span class="vertical-middle lighter break-all ellipsis-1">{{
|
||||
baseUrl + id
|
||||
}}</span>
|
||||
<el-tooltip effect="dark" :content="$t('common.copy')" placement="top">
|
||||
<el-button type="primary" text @click="copyClick(baseUrl + id)">
|
||||
<AppIcon iconName="app-copy"></AppIcon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<el-button @click="openAPIKeyDialog">{{
|
||||
$t('views.applicationOverview.appInfo.apiKey')
|
||||
}}</el-button>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-card>
|
||||
<h4 class="title-decoration-1 mt-16 mb-16">
|
||||
{{ $t('views.applicationOverview.monitor.monitoringStatistics') }}
|
||||
</h4>
|
||||
<div class="mb-16">
|
||||
<el-select
|
||||
v-model="history_day"
|
||||
class="mr-12"
|
||||
@change="changeDayHandle"
|
||||
style="width: 180px"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in dayOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-date-picker
|
||||
v-if="history_day === 'other'"
|
||||
v-model="daterangeValue"
|
||||
type="daterange"
|
||||
:start-placeholder="$t('views.applicationOverview.monitor.startDatePlaceholder')"
|
||||
:end-placeholder="$t('views.applicationOverview.monitor.endDatePlaceholder')"
|
||||
format="YYYY-MM-DD"
|
||||
value-format="YYYY-MM-DD"
|
||||
@change="changeDayRangeHandle"
|
||||
/>
|
||||
</div>
|
||||
<div v-loading="statisticsLoading">
|
||||
<StatisticsCharts :data="statisticsData" />
|
||||
</div>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<EmbedDialog
|
||||
ref="EmbedDialogRef"
|
||||
:data="detail"
|
||||
:api-input-params="mapToUrlParams(apiInputParams)"
|
||||
/>
|
||||
<APIKeyDialog ref="APIKeyDialogRef" />
|
||||
<LimitDialog ref="LimitDialogRef" @refresh="refresh" />
|
||||
<EditAvatarDialog ref="EditAvatarDialogRef" @refresh="refreshIcon" />
|
||||
<XPackDisplaySettingDialog
|
||||
ref="XPackDisplaySettingDialogRef"
|
||||
@refresh="refresh"
|
||||
v-if="user.isEnterprise()"
|
||||
/>
|
||||
<DisplaySettingDialog ref="DisplaySettingDialogRef" @refresh="refresh" v-else />
|
||||
</LayoutContainer>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import EmbedDialog from './component/EmbedDialog.vue'
|
||||
import APIKeyDialog from './component/APIKeyDialog.vue'
|
||||
import LimitDialog from './component/LimitDialog.vue'
|
||||
import DisplaySettingDialog from './component/DisplaySettingDialog.vue'
|
||||
import XPackDisplaySettingDialog from './component/XPackDisplaySettingDialog.vue'
|
||||
import EditAvatarDialog from './component/EditAvatarDialog.vue'
|
||||
import StatisticsCharts from './component/StatisticsCharts.vue'
|
||||
import applicationApi from '@/api/application'
|
||||
import overviewApi from '@/api/application-overview'
|
||||
import { nowDate, beforeDay } from '@/utils/time'
|
||||
import { MsgSuccess, MsgConfirm } from '@/utils/message'
|
||||
import { copyClick } from '@/utils/clipboard'
|
||||
import { isAppIcon } from '@/utils/common'
|
||||
import useStore from '@/stores'
|
||||
import { t } from '@/locales'
|
||||
const { user, application } = useStore()
|
||||
const route = useRoute()
|
||||
const {
|
||||
params: { id }
|
||||
} = route as any
|
||||
|
||||
const apiUrl = window.location.origin + '/doc/chat/'
|
||||
|
||||
const baseUrl = window.location.origin + '/api/application/'
|
||||
|
||||
const DisplaySettingDialogRef = ref()
|
||||
const XPackDisplaySettingDialogRef = ref()
|
||||
const EditAvatarDialogRef = ref()
|
||||
const LimitDialogRef = ref()
|
||||
const APIKeyDialogRef = ref()
|
||||
const EmbedDialogRef = ref()
|
||||
|
||||
const accessToken = ref<any>({})
|
||||
const detail = ref<any>(null)
|
||||
|
||||
const loading = ref(false)
|
||||
|
||||
const urlParams = computed(() =>
|
||||
mapToUrlParams(apiInputParams.value) ? '?' + mapToUrlParams(apiInputParams.value) : ''
|
||||
)
|
||||
const shareUrl = computed(
|
||||
() => application.location + accessToken.value.access_token + urlParams.value
|
||||
)
|
||||
|
||||
const dayOptions = [
|
||||
{
|
||||
value: 7,
|
||||
// @ts-ignore
|
||||
label: t('views.applicationOverview.monitor.pastDayOptions.past7Days')
|
||||
},
|
||||
{
|
||||
value: 30,
|
||||
label: t('views.applicationOverview.monitor.pastDayOptions.past30Days')
|
||||
},
|
||||
{
|
||||
value: 90,
|
||||
label: t('views.applicationOverview.monitor.pastDayOptions.past90Days')
|
||||
},
|
||||
{
|
||||
value: 183,
|
||||
label: t('views.applicationOverview.monitor.pastDayOptions.past183Days')
|
||||
},
|
||||
{
|
||||
value: 'other',
|
||||
label: t('views.applicationOverview.monitor.pastDayOptions.other')
|
||||
}
|
||||
]
|
||||
|
||||
const history_day = ref<number | string>(7)
|
||||
|
||||
// 日期组件时间
|
||||
const daterangeValue = ref('')
|
||||
|
||||
// 提交日期时间
|
||||
const daterange = ref({
|
||||
start_time: '',
|
||||
end_time: ''
|
||||
})
|
||||
|
||||
const statisticsLoading = ref(false)
|
||||
const statisticsData = ref([])
|
||||
|
||||
const showEditIcon = ref(false)
|
||||
const apiInputParams = ref([])
|
||||
|
||||
function toUrl(url: string) {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
function openDisplaySettingDialog() {
|
||||
if (user.isEnterprise()) {
|
||||
XPackDisplaySettingDialogRef.value?.open(accessToken.value, detail.value)
|
||||
} else {
|
||||
DisplaySettingDialogRef.value?.open(accessToken.value, detail.value)
|
||||
}
|
||||
}
|
||||
function openEditAvatar() {
|
||||
EditAvatarDialogRef.value.open(detail.value)
|
||||
}
|
||||
|
||||
function changeDayHandle(val: number | string) {
|
||||
if (val !== 'other') {
|
||||
daterange.value.start_time = beforeDay(val)
|
||||
daterange.value.end_time = nowDate
|
||||
getAppStatistics()
|
||||
}
|
||||
}
|
||||
|
||||
function changeDayRangeHandle(val: string) {
|
||||
daterange.value.start_time = val[0]
|
||||
daterange.value.end_time = val[1]
|
||||
getAppStatistics()
|
||||
}
|
||||
|
||||
function getAppStatistics() {
|
||||
overviewApi.getStatistics(id, daterange.value, statisticsLoading).then((res: any) => {
|
||||
statisticsData.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
function refreshAccessToken() {
|
||||
MsgConfirm(
|
||||
t('views.applicationOverview.appInfo.refreshToken.msgConfirm1'),
|
||||
t('views.applicationOverview.appInfo.refreshToken.msgConfirm2'),
|
||||
{
|
||||
confirmButtonText: t('common.confirm'),
|
||||
cancelButtonText: t('common.cancel')
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
const obj = {
|
||||
access_token_reset: true
|
||||
}
|
||||
// @ts-ignore
|
||||
const str = t('views.applicationOverview.appInfo.refreshToken.refreshSuccess')
|
||||
updateAccessToken(obj, str)
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
function changeState(bool: Boolean) {
|
||||
const obj = {
|
||||
is_active: !bool
|
||||
}
|
||||
const str = obj.is_active ? t('common.status.enableSuccess') : t('common.status.disableSuccess')
|
||||
updateAccessToken(obj, str)
|
||||
.then(() => {
|
||||
return true
|
||||
})
|
||||
.catch(() => {
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
async function updateAccessToken(obj: any, str: string) {
|
||||
applicationApi.putAccessToken(id as string, obj, loading).then((res) => {
|
||||
accessToken.value = res?.data
|
||||
MsgSuccess(str)
|
||||
})
|
||||
}
|
||||
|
||||
function openLimitDialog() {
|
||||
LimitDialogRef.value.open(accessToken.value)
|
||||
}
|
||||
|
||||
function openAPIKeyDialog() {
|
||||
APIKeyDialogRef.value.open()
|
||||
}
|
||||
function openDialog() {
|
||||
EmbedDialogRef.value.open(accessToken.value?.access_token)
|
||||
}
|
||||
function getAccessToken() {
|
||||
application.asyncGetAccessToken(id, loading).then((res: any) => {
|
||||
accessToken.value = res?.data
|
||||
})
|
||||
}
|
||||
|
||||
function getDetail() {
|
||||
application.asyncGetApplicationDetail(id, loading).then((res: any) => {
|
||||
detail.value = res.data
|
||||
detail.value.work_flow?.nodes
|
||||
?.filter((v: any) => v.id === 'base-node')
|
||||
.map((v: any) => {
|
||||
apiInputParams.value = v.properties.api_input_field_list
|
||||
? v.properties.api_input_field_list.map((v: any) => {
|
||||
return {
|
||||
name: v.variable,
|
||||
value: v.default_value
|
||||
}
|
||||
})
|
||||
: v.properties.input_field_list
|
||||
? v.properties.input_field_list
|
||||
.filter((v: any) => v.assignment_method === 'api_input')
|
||||
.map((v: any) => {
|
||||
return {
|
||||
name: v.variable,
|
||||
value: v.default_value
|
||||
}
|
||||
})
|
||||
: []
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
getAccessToken()
|
||||
}
|
||||
|
||||
function refreshIcon() {
|
||||
getDetail()
|
||||
}
|
||||
|
||||
function mapToUrlParams(map: any[]) {
|
||||
const params = new URLSearchParams()
|
||||
|
||||
map.forEach((item: any) => {
|
||||
params.append(encodeURIComponent(item.name), encodeURIComponent(item.value))
|
||||
})
|
||||
|
||||
return params.toString() // 返回 URL 查询字符串
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
getAccessToken()
|
||||
changeDayHandle(history_day.value)
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.overview-card {
|
||||
position: relative;
|
||||
.active-button {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 21px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,302 @@
|
|||
<template>
|
||||
<div v-show="show" class="workflow-dropdown-menu border border-r-4">
|
||||
<el-tabs v-model="activeName" class="workflow-dropdown-tabs">
|
||||
<div style="display: flex; width: 100%; justify-content: center" class="mb-4">
|
||||
<el-input
|
||||
v-model="search_text"
|
||||
style="width: 240px"
|
||||
:placeholder="$t('views.applicationWorkflow.searchBar.placeholder')"
|
||||
>
|
||||
<template #suffix>
|
||||
<el-icon class="el-input__icon"><search /></el-icon>
|
||||
</template>
|
||||
</el-input>
|
||||
</div>
|
||||
|
||||
<el-tab-pane :label="$t('views.applicationWorkflow.baseComponent')" name="base">
|
||||
<el-scrollbar height="400">
|
||||
<div v-if="filter_menu_nodes.length > 0">
|
||||
<template v-for="(item, index) in filter_menu_nodes" :key="index">
|
||||
<div
|
||||
class="workflow-dropdown-item cursor flex p-8-12"
|
||||
@click.stop="clickNodes(item)"
|
||||
@mousedown.stop="onmousedown(item)"
|
||||
>
|
||||
<component :is="iconComponent(`${item.type}-icon`)" class="mr-8 mt-4" :size="32" />
|
||||
<div class="pre-wrap">
|
||||
<div class="lighter">{{ item.label }}</div>
|
||||
<el-text type="info" size="small">{{ item.text }}</el-text>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else class="ml-16 mt-8">
|
||||
<el-text type="info">{{ $t('views.applicationWorkflow.tip.noData') }}</el-text>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('views.functionLib.title')" name="function">
|
||||
<el-scrollbar height="400">
|
||||
<div
|
||||
class="workflow-dropdown-item cursor flex p-8-12"
|
||||
@click.stop="clickNodes(functionNode)"
|
||||
@mousedown.stop="onmousedown(functionNode)"
|
||||
>
|
||||
<component :is="iconComponent(`function-lib-node-icon`)" class="mr-8 mt-4" :size="32" />
|
||||
<div class="pre-wrap">
|
||||
<div class="lighter">{{ functionNode.label }}</div>
|
||||
<el-text type="info" size="small">{{ functionNode.text }}</el-text>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-for="(item, index) in filter_function_lib_list" :key="index">
|
||||
<div
|
||||
class="workflow-dropdown-item cursor flex p-8-12 align-center"
|
||||
@click.stop="clickNodes(functionLibNode, item, 'function')"
|
||||
@mousedown.stop="onmousedown(functionLibNode, item, 'function')"
|
||||
>
|
||||
<component
|
||||
:is="iconComponent(`function-lib-node-icon`)"
|
||||
class="mr-8"
|
||||
:size="32"
|
||||
:item="item"
|
||||
/>
|
||||
|
||||
<div class="pre-wrap">
|
||||
<div class="lighter ellipsis-1" :title="item.name">{{ item.name }}</div>
|
||||
<p>
|
||||
<el-text
|
||||
class="ellipsis-1"
|
||||
type="info"
|
||||
size="small"
|
||||
:title="item.desc"
|
||||
v-if="item.desc"
|
||||
>{{ item.desc }}</el-text
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-scrollbar>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('views.application.title')" name="application">
|
||||
<el-scrollbar height="400">
|
||||
<div v-if="filter_application_list.length > 0">
|
||||
<template v-for="(item, index) in filter_application_list" :key="index">
|
||||
<div
|
||||
class="workflow-dropdown-item cursor flex align-center p-8-12"
|
||||
@click.stop="clickNodes(applicationNode, item, 'application')"
|
||||
@mousedown.stop="onmousedown(applicationNode, item, 'application')"
|
||||
>
|
||||
<component
|
||||
:is="iconComponent(`application-node-icon`)"
|
||||
class="mr-8"
|
||||
:size="32"
|
||||
:item="item"
|
||||
/>
|
||||
<div class="pre-wrap" style="width: 60%">
|
||||
<div class="lighter ellipsis" :title="item.name">
|
||||
{{ item.name }}
|
||||
</div>
|
||||
<p>
|
||||
<el-text
|
||||
class="ellipsis"
|
||||
type="info"
|
||||
size="small"
|
||||
:title="item.desc"
|
||||
v-if="item.desc"
|
||||
>{{ item.desc }}</el-text
|
||||
>
|
||||
</p>
|
||||
</div>
|
||||
<div class="status-tag" style="margin-left: auto">
|
||||
<el-tag type="warning" v-if="isWorkFlow(item.type)" style="height: 22px">
|
||||
{{ $t('views.application.workflow') }}</el-tag
|
||||
>
|
||||
<el-tag class="blue-tag" v-else style="height: 22px">{{
|
||||
$t('views.application.simple')
|
||||
}}</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else class="ml-16 mt-8">
|
||||
<el-text type="info">{{ $t('views.applicationWorkflow.tip.noData') }}</el-text>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { menuNodes, functionLibNode, functionNode, applicationNode } from '@/workflow/common/data'
|
||||
import { iconComponent } from '@/workflow/icons/utils'
|
||||
import applicationApi from '@/api/application'
|
||||
import { isWorkFlow } from '@/utils/application'
|
||||
import { isAppIcon } from '@/utils/common'
|
||||
const search_text = ref<string>('')
|
||||
const props = defineProps({
|
||||
show: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
workflowRef: Object
|
||||
})
|
||||
|
||||
const emit = defineEmits(['clickNodes', 'onmousedown'])
|
||||
|
||||
const loading = ref(false)
|
||||
const activeName = ref('base')
|
||||
|
||||
const functionLibList = ref<any[]>([])
|
||||
const filter_function_lib_list = computed(() => {
|
||||
return functionLibList.value.filter((item: any) =>
|
||||
item.name.toLocaleLowerCase().includes(search_text.value.toLocaleLowerCase())
|
||||
)
|
||||
})
|
||||
const applicationList = ref<any[]>([])
|
||||
const filter_application_list = computed(() => {
|
||||
return applicationList.value.filter((item: any) =>
|
||||
item.name.toLocaleLowerCase().includes(search_text.value.toLocaleLowerCase())
|
||||
)
|
||||
})
|
||||
|
||||
const filter_menu_nodes = computed(() => {
|
||||
return menuNodes.filter((item) =>
|
||||
item.label.toLocaleLowerCase().includes(search_text.value.toLocaleLowerCase())
|
||||
)
|
||||
})
|
||||
function clickNodes(item: any, data?: any, type?: string) {
|
||||
if (data) {
|
||||
item['properties']['stepName'] = data.name
|
||||
if (type == 'function') {
|
||||
item['properties']['node_data'] = {
|
||||
...data,
|
||||
function_lib_id: data.id,
|
||||
input_field_list: data.input_field_list.map((field: any) => ({
|
||||
...field,
|
||||
value: field.source == 'reference' ? [] : ''
|
||||
}))
|
||||
}
|
||||
}
|
||||
if (type == 'application') {
|
||||
if (isWorkFlow(data.type)) {
|
||||
const nodeData = data.work_flow.nodes[0].properties.node_data
|
||||
const fileUploadSetting = nodeData.file_upload_setting
|
||||
item['properties']['node_data'] = {
|
||||
name: data.name,
|
||||
icon: data.icon,
|
||||
application_id: data.id,
|
||||
api_input_field_list: data.work_flow.nodes[0].properties.api_input_field_list,
|
||||
user_input_field_list: data.work_flow.nodes[0].properties.user_input_field_list,
|
||||
...(!fileUploadSetting
|
||||
? {}
|
||||
: {
|
||||
...(fileUploadSetting.document ? { document_list: [] } : {}),
|
||||
...(fileUploadSetting.image ? { image_list: [] } : {}),
|
||||
...(fileUploadSetting.audio ? { audio_list: [] } : {})
|
||||
})
|
||||
}
|
||||
} else {
|
||||
item['properties']['node_data'] = {
|
||||
name: data.name,
|
||||
icon: data.icon,
|
||||
application_id: data.id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
props.workflowRef?.addNode(item)
|
||||
|
||||
emit('clickNodes', item)
|
||||
}
|
||||
|
||||
function onmousedown(item: any, data?: any, type?: string) {
|
||||
if (data) {
|
||||
item['properties']['stepName'] = data.name
|
||||
if (type == 'function') {
|
||||
item['properties']['node_data'] = {
|
||||
...data,
|
||||
function_lib_id: data.id,
|
||||
input_field_list: data.input_field_list.map((field: any) => ({
|
||||
...field,
|
||||
value: field.source == 'reference' ? [] : ''
|
||||
}))
|
||||
}
|
||||
}
|
||||
if (type == 'application') {
|
||||
if (isWorkFlow(data.type)) {
|
||||
const nodeData = data.work_flow.nodes[0].properties.node_data
|
||||
const fileUploadSetting = nodeData.file_upload_setting
|
||||
item['properties']['node_data'] = {
|
||||
name: data.name,
|
||||
icon: data.icon,
|
||||
application_id: data.id,
|
||||
api_input_field_list: data.work_flow.nodes[0].properties.api_input_field_list,
|
||||
user_input_field_list: data.work_flow.nodes[0].properties.user_input_field_list,
|
||||
...(!fileUploadSetting
|
||||
? {}
|
||||
: {
|
||||
...(fileUploadSetting.document ? { document_list: [] } : {}),
|
||||
...(fileUploadSetting.image ? { image_list: [] } : {}),
|
||||
...(fileUploadSetting.audio ? { audio_list: [] } : {})
|
||||
})
|
||||
}
|
||||
} else {
|
||||
item['properties']['node_data'] = {
|
||||
name: data.name,
|
||||
icon: data.icon,
|
||||
application_id: data.id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
props.workflowRef?.onmousedown(item)
|
||||
emit('onmousedown', item)
|
||||
}
|
||||
|
||||
function getList() {
|
||||
applicationApi.listFunctionLib(props.id, loading).then((res: any) => {
|
||||
functionLibList.value = res.data
|
||||
})
|
||||
applicationApi.getApplicationList(props.id, loading).then((res: any) => {
|
||||
applicationList.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.workflow-dropdown-menu {
|
||||
-moz-user-select: none; /* Firefox */
|
||||
-webkit-user-select: none; /* WebKit内核 */
|
||||
-ms-user-select: none; /* IE10及以后 */
|
||||
-khtml-user-select: none; /* 早期浏览器 */
|
||||
-o-user-select: none; /* Opera */
|
||||
user-select: none; /* CSS3属性 */
|
||||
position: absolute;
|
||||
top: 49px;
|
||||
right: 122px;
|
||||
z-index: 99;
|
||||
width: 268px;
|
||||
box-shadow: 0px 4px 8px 0px var(--app-text-color-light-1);
|
||||
background: #ffffff;
|
||||
padding-bottom: 8px;
|
||||
|
||||
.title {
|
||||
padding: 12px 12px 4px;
|
||||
}
|
||||
.workflow-dropdown-item {
|
||||
&:hover {
|
||||
background: var(--app-text-color-light-1);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
<template>
|
||||
<div class="workflow-publish-history border-l">
|
||||
<h4 class="border-b p-16-24">{{ $t('views.applicationWorkflow.setting.releaseHistory') }}</h4>
|
||||
<div class="list-height pt-0">
|
||||
<el-scrollbar>
|
||||
<div class="p-8 pt-0">
|
||||
<common-list
|
||||
:data="LogData"
|
||||
class="mt-8"
|
||||
v-loading="loading"
|
||||
@click="clickListHandle"
|
||||
@mouseenter="mouseenter"
|
||||
@mouseleave="mouseId = ''"
|
||||
>
|
||||
<template #default="{ row, index }">
|
||||
<div class="flex-between">
|
||||
<div style="max-width: 80%">
|
||||
<h5 :class="index === 0 ? 'primary' : ''" class="flex">
|
||||
<ReadWrite
|
||||
@change="editName($event, row)"
|
||||
:data="row.name || datetimeFormat(row.update_time)"
|
||||
trigger="manual"
|
||||
:write="row.writeStatus"
|
||||
@close="closeWrite(row)"
|
||||
/>
|
||||
<el-tag v-if="index === 0" class="default-tag ml-4">{{
|
||||
$t('views.applicationWorkflow.setting.latestRelease')
|
||||
}}</el-tag>
|
||||
</h5>
|
||||
<el-text type="info" class="color-secondary flex mt-8">
|
||||
<AppAvatar :size="20" class="avatar-grey mr-4">
|
||||
<el-icon><UserFilled /></el-icon>
|
||||
</AppAvatar>
|
||||
{{ row.publish_user_name }}
|
||||
</el-text>
|
||||
</div>
|
||||
|
||||
<div @click.stop v-show="mouseId === row.id">
|
||||
<el-dropdown trigger="click" :teleported="false">
|
||||
<el-button text>
|
||||
<el-icon><MoreFilled /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click.stop="openEditVersion(row)">
|
||||
<el-icon><EditPen /></el-icon>
|
||||
{{ $t('common.edit') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click="refreshVersion(row)">
|
||||
<el-icon><RefreshLeft /></el-icon>
|
||||
{{ $t('views.applicationWorkflow.setting.restoreCurrentVersion') }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #empty>
|
||||
<div class="text-center">
|
||||
<el-text type="info"> {{ $t('chat.noHistory') }}</el-text>
|
||||
</div>
|
||||
</template>
|
||||
</common-list>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import applicationApi from '@/api/application'
|
||||
import { datetimeFormat } from '@/utils/time'
|
||||
import { MsgSuccess, MsgError } from '@/utils/message'
|
||||
import { t } from '@/locales'
|
||||
const route = useRoute()
|
||||
const {
|
||||
params: { id }
|
||||
} = route as any
|
||||
|
||||
const emit = defineEmits(['click', 'refreshVersion'])
|
||||
const loading = ref(false)
|
||||
const LogData = ref<any[]>([])
|
||||
|
||||
const mouseId = ref('')
|
||||
|
||||
function mouseenter(row: any) {
|
||||
mouseId.value = row.id
|
||||
}
|
||||
|
||||
function clickListHandle(item: any) {
|
||||
emit('click', item)
|
||||
}
|
||||
|
||||
function refreshVersion(item: any) {
|
||||
emit('refreshVersion', item)
|
||||
}
|
||||
|
||||
function openEditVersion(item: any) {
|
||||
item['writeStatus'] = true
|
||||
}
|
||||
|
||||
function closeWrite(item: any) {
|
||||
item['writeStatus'] = false
|
||||
}
|
||||
|
||||
function editName(val: string, item: any) {
|
||||
if (val) {
|
||||
const obj = {
|
||||
name: val
|
||||
}
|
||||
applicationApi.putWorkFlowVersion(id as string, item.id, obj, loading).then(() => {
|
||||
MsgSuccess(t('common.modifySuccess'))
|
||||
item['writeStatus'] = false
|
||||
getList()
|
||||
})
|
||||
} else {
|
||||
MsgError(t('views.applicationWorkflow.tip.nameMessage'))
|
||||
}
|
||||
}
|
||||
|
||||
function getList() {
|
||||
applicationApi.getWorkFlowVersion(id, loading).then((res: any) => {
|
||||
LogData.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getList()
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.workflow-publish-history {
|
||||
width: 320px;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 57px;
|
||||
background: #ffffff;
|
||||
height: calc(100vh - 57px);
|
||||
z-index: 9;
|
||||
.list-height {
|
||||
height: calc(100vh - 120px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,487 @@
|
|||
<template>
|
||||
<div class="application-workflow" v-loading="loading">
|
||||
<div class="header border-b flex-between p-12-24">
|
||||
<div class="flex align-center">
|
||||
<back-button @click="back"></back-button>
|
||||
<h4>{{ detail?.name }}</h4>
|
||||
<div v-if="showHistory && disablePublic">
|
||||
<el-text type="info" class="ml-16 color-secondary"
|
||||
>{{ $t('views.applicationWorkflow.info.previewVersion') }}
|
||||
{{ currentVersion.name || datetimeFormat(currentVersion.update_time) }}</el-text
|
||||
>
|
||||
</div>
|
||||
<el-text type="info" class="ml-16 color-secondary" v-else-if="saveTime"
|
||||
>{{ $t('views.applicationWorkflow.info.saveTime')
|
||||
}}{{ datetimeFormat(saveTime) }}</el-text
|
||||
>
|
||||
</div>
|
||||
<div v-if="showHistory && disablePublic">
|
||||
<el-button type="primary" class="mr-8" @click="refreshVersion()">
|
||||
{{ $t('views.applicationWorkflow.setting.restoreVersion') }}
|
||||
</el-button>
|
||||
<el-divider direction="vertical" />
|
||||
<el-button text @click="closeHistory">
|
||||
<el-icon><Close /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
<div v-else>
|
||||
<el-button icon="Plus" @click="showPopover = !showPopover">
|
||||
{{ $t('views.applicationWorkflow.setting.addComponent') }}
|
||||
</el-button>
|
||||
<el-button @click="clickShowDebug" :disabled="showDebug">
|
||||
<AppIcon iconName="app-play-outlined" class="mr-4"></AppIcon>
|
||||
{{ $t('views.applicationWorkflow.setting.debug') }}</el-button
|
||||
>
|
||||
<el-button @click="saveApplication(true)">
|
||||
<AppIcon iconName="app-save-outlined" class="mr-4"></AppIcon>
|
||||
{{ $t('common.save') }}
|
||||
</el-button>
|
||||
<el-button type="primary" @click="publicHandle">
|
||||
{{ $t('views.applicationWorkflow.setting.public') }}
|
||||
</el-button>
|
||||
|
||||
<el-dropdown trigger="click">
|
||||
<el-button text @click.stop class="ml-8 mt-4">
|
||||
<el-icon class="rotate-90"><MoreFilled /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="openHistory">
|
||||
<AppIcon iconName="app-history-outlined"></AppIcon>
|
||||
{{ $t('views.applicationWorkflow.setting.releaseHistory') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item>
|
||||
<AppIcon iconName="app-save-outlined"></AppIcon>
|
||||
{{ $t('views.applicationWorkflow.setting.autoSave') }}
|
||||
<div class="ml-4">
|
||||
<el-switch size="small" v-model="isSave" @change="changeSave" />
|
||||
</div>
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 下拉框 -->
|
||||
<el-collapse-transition>
|
||||
<DropdownMenu
|
||||
:show="showPopover"
|
||||
:id="id"
|
||||
v-click-outside="clickoutside"
|
||||
@clickNodes="clickNodes"
|
||||
@onmousedown="onmousedown"
|
||||
:workflowRef="workflowRef"
|
||||
/>
|
||||
</el-collapse-transition>
|
||||
<!-- 主画布 -->
|
||||
<div class="workflow-main" ref="workflowMainRef">
|
||||
<workflow ref="workflowRef" v-if="detail" :data="detail?.work_flow" />
|
||||
</div>
|
||||
<!-- 调试 -->
|
||||
<el-collapse-transition>
|
||||
<div class="workflow-debug-container" :class="enlarge ? 'enlarge' : ''" v-if="showDebug">
|
||||
<div class="workflow-debug-header" :class="!isDefaultTheme ? 'custom-header' : ''">
|
||||
<div class="flex-between">
|
||||
<div class="flex align-center">
|
||||
<div class="mr-12 ml-24 flex">
|
||||
<AppAvatar
|
||||
v-if="isAppIcon(detail?.icon)"
|
||||
shape="square"
|
||||
:size="32"
|
||||
style="background: none"
|
||||
>
|
||||
<img :src="detail?.icon" alt="" />
|
||||
</AppAvatar>
|
||||
<AppAvatar
|
||||
v-else-if="detail?.name"
|
||||
:name="detail?.name"
|
||||
pinyinColor
|
||||
shape="square"
|
||||
:size="32"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h4>
|
||||
{{ detail?.name || $t('views.application.applicationForm.form.appName.label') }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="mr-16">
|
||||
<el-button link @click="enlarge = !enlarge">
|
||||
<AppIcon
|
||||
:iconName="enlarge ? 'app-minify' : 'app-magnify'"
|
||||
class="color-secondary"
|
||||
style="font-size: 20px"
|
||||
></AppIcon>
|
||||
</el-button>
|
||||
<el-button link @click="showDebug = false">
|
||||
<el-icon :size="20" class="color-secondary"><Close /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="scrollbar-height">
|
||||
<AiChat :application-details="detail" :type="'debug-ai-chat'"></AiChat>
|
||||
</div>
|
||||
</div>
|
||||
</el-collapse-transition>
|
||||
<!-- 发布历史 -->
|
||||
<PublishHistory
|
||||
v-if="showHistory"
|
||||
@click="checkVersion"
|
||||
v-click-outside="clickoutsideHistory"
|
||||
@refreshVersion="refreshVersion"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount, computed, nextTick } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import type { Action } from 'element-plus'
|
||||
import Workflow from '@/workflow/index.vue'
|
||||
import DropdownMenu from '@/views/application-workflow/component/DropdownMenu.vue'
|
||||
import PublishHistory from '@/views/application-workflow/component/PublishHistory.vue'
|
||||
import applicationApi from '@/api/application'
|
||||
import { isAppIcon } from '@/utils/common'
|
||||
import { MsgSuccess, MsgError, MsgConfirm } from '@/utils/message'
|
||||
import { datetimeFormat } from '@/utils/time'
|
||||
import useStore from '@/stores'
|
||||
import { WorkFlowInstance } from '@/workflow/common/validate'
|
||||
import { hasPermission } from '@/utils/permission'
|
||||
import { t } from '@/locales'
|
||||
|
||||
const { user, application } = useStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
|
||||
const isDefaultTheme = computed(() => {
|
||||
return user.isDefaultTheme()
|
||||
})
|
||||
const {
|
||||
params: { id }
|
||||
} = route as any
|
||||
|
||||
let interval: any
|
||||
const workflowRef = ref()
|
||||
const workflowMainRef = ref()
|
||||
const loading = ref(false)
|
||||
const detail = ref<any>(null)
|
||||
|
||||
const showPopover = ref(false)
|
||||
const showDebug = ref(false)
|
||||
const enlarge = ref(false)
|
||||
const saveTime = ref<any>('')
|
||||
const isSave = ref(false)
|
||||
const showHistory = ref(false)
|
||||
const disablePublic = ref(false)
|
||||
const currentVersion = ref<any>({})
|
||||
const cloneWorkFlow = ref(null)
|
||||
|
||||
function back() {
|
||||
if (JSON.stringify(cloneWorkFlow.value) !== JSON.stringify(getGraphData())) {
|
||||
MsgConfirm(t('common.tip'), t('views.applicationWorkflow.tip.saveMessage'), {
|
||||
confirmButtonText: t('views.applicationWorkflow.setting.exitSave'),
|
||||
cancelButtonText: t('views.applicationWorkflow.setting.exit'),
|
||||
type: 'warning',
|
||||
distinguishCancelAndClose: true
|
||||
})
|
||||
.then(() => {
|
||||
saveApplication(true, true)
|
||||
})
|
||||
.catch((action: Action) => {
|
||||
action === 'cancel' && router.push({ path: `/application/${id}/WORK_FLOW/overview` })
|
||||
})
|
||||
} else {
|
||||
router.push({ path: `/application/${id}/WORK_FLOW/overview` })
|
||||
}
|
||||
}
|
||||
function clickoutsideHistory() {
|
||||
if (!disablePublic.value) {
|
||||
showHistory.value = false
|
||||
disablePublic.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function refreshVersion(item?: any) {
|
||||
if (item) {
|
||||
renderGraphData(item)
|
||||
}
|
||||
if (hasPermission(`APPLICATION:MANAGE:${id}`, 'AND') && isSave.value) {
|
||||
initInterval()
|
||||
}
|
||||
showHistory.value = false
|
||||
disablePublic.value = false
|
||||
}
|
||||
|
||||
function checkVersion(item: any) {
|
||||
disablePublic.value = true
|
||||
currentVersion.value = item
|
||||
renderGraphData(item)
|
||||
closeInterval()
|
||||
}
|
||||
|
||||
function renderGraphData(item: any) {
|
||||
item.work_flow['nodes'].map((v: any) => {
|
||||
v['properties']['noRender'] = true
|
||||
})
|
||||
detail.value.work_flow = item.work_flow
|
||||
saveTime.value = item?.update_time
|
||||
workflowRef.value?.clearGraphData()
|
||||
nextTick(() => {
|
||||
workflowRef.value?.render(item.work_flow)
|
||||
})
|
||||
}
|
||||
|
||||
function closeHistory() {
|
||||
getDetail()
|
||||
if (hasPermission(`APPLICATION:MANAGE:${id}`, 'AND') && isSave.value) {
|
||||
initInterval()
|
||||
}
|
||||
showHistory.value = false
|
||||
disablePublic.value = false
|
||||
}
|
||||
|
||||
function openHistory() {
|
||||
showHistory.value = true
|
||||
}
|
||||
|
||||
function changeSave(bool: boolean) {
|
||||
bool ? initInterval() : closeInterval()
|
||||
localStorage.setItem('workflowAutoSave', bool.toString())
|
||||
}
|
||||
|
||||
function clickNodes(item: any) {
|
||||
// workflowRef.value?.addNode(item)
|
||||
showPopover.value = false
|
||||
}
|
||||
|
||||
function onmousedown(item: any) {
|
||||
// workflowRef.value?.onmousedown(item)
|
||||
showPopover.value = false
|
||||
}
|
||||
|
||||
function clickoutside() {
|
||||
showPopover.value = false
|
||||
}
|
||||
async function publicHandle() {
|
||||
// 后执行发布
|
||||
workflowRef.value
|
||||
?.validate()
|
||||
.then(async () => {
|
||||
const obj = {
|
||||
work_flow: getGraphData()
|
||||
}
|
||||
await application.asyncPutApplication(id, obj, loading)
|
||||
const workflow = new WorkFlowInstance(obj.work_flow)
|
||||
try {
|
||||
workflow.is_valid()
|
||||
} catch (e: any) {
|
||||
MsgError(e.toString())
|
||||
return
|
||||
}
|
||||
applicationApi.putPublishApplication(id as String, obj, loading).then(() => {
|
||||
|
||||
application.asyncGetApplicationDetail(id, loading).then((res: any) => {
|
||||
detail.value.name = res.data.name
|
||||
MsgSuccess(t('views.applicationWorkflow.tip.publicSuccess'))
|
||||
})
|
||||
})
|
||||
})
|
||||
.catch((res: any) => {
|
||||
const node = res.node
|
||||
const err_message = res.errMessage
|
||||
if (typeof err_message == 'string') {
|
||||
MsgError(
|
||||
res.node.properties?.stepName +
|
||||
` ${t('views.applicationWorkflow.node').toLowerCase()} ` +
|
||||
err_message.toLowerCase()
|
||||
)
|
||||
} else {
|
||||
const keys = Object.keys(err_message)
|
||||
MsgError(
|
||||
node.properties?.stepName +
|
||||
` ${t('views.applicationWorkflow.node').toLowerCase()} ` +
|
||||
err_message[keys[0]]?.[0]?.message.toLowerCase()
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const clickShowDebug = () => {
|
||||
workflowRef.value
|
||||
?.validate()
|
||||
.then(() => {
|
||||
const graphData = getGraphData()
|
||||
const workflow = new WorkFlowInstance(graphData)
|
||||
try {
|
||||
workflow.is_valid()
|
||||
detail.value = {
|
||||
...detail.value,
|
||||
type: 'WORK_FLOW',
|
||||
...workflow.get_base_node()?.properties.node_data,
|
||||
work_flow: getGraphData()
|
||||
}
|
||||
|
||||
showDebug.value = true
|
||||
} catch (e: any) {
|
||||
MsgError(e.toString())
|
||||
}
|
||||
})
|
||||
.catch((res: any) => {
|
||||
const node = res.node
|
||||
const err_message = res.errMessage
|
||||
if (typeof err_message == 'string') {
|
||||
MsgError(
|
||||
res.node.properties?.stepName + ` ${t('views.applicationWorkflow.node')},` + err_message
|
||||
)
|
||||
} else {
|
||||
const keys = Object.keys(err_message)
|
||||
MsgError(
|
||||
node.properties?.stepName +
|
||||
` ${t('views.applicationWorkflow.node')},` +
|
||||
err_message[keys[0]]?.[0]?.message
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
// function clickoutsideDebug(e: any) {
|
||||
// if (workflowMainRef.value && e && e.target && workflowMainRef.value.contains(e?.target)) {
|
||||
// showDebug.value = false
|
||||
// }
|
||||
// }
|
||||
|
||||
function getGraphData() {
|
||||
return workflowRef.value?.getGraphData()
|
||||
}
|
||||
|
||||
function getDetail() {
|
||||
application.asyncGetApplicationDetail(id).then((res: any) => {
|
||||
res.data?.work_flow['nodes'].map((v: any) => {
|
||||
v['properties']['noRender'] = true
|
||||
})
|
||||
detail.value = res.data
|
||||
detail.value.stt_model_id = res.data.stt_model
|
||||
detail.value.tts_model_id = res.data.tts_model
|
||||
detail.value.tts_type = res.data.tts_type
|
||||
saveTime.value = res.data?.update_time
|
||||
application.asyncGetAccessToken(id, loading).then((res: any) => {
|
||||
detail.value = { ...detail.value, ...res.data }
|
||||
})
|
||||
workflowRef.value?.clearGraphData()
|
||||
nextTick(() => {
|
||||
workflowRef.value?.render(detail.value.work_flow)
|
||||
cloneWorkFlow.value = getGraphData()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function saveApplication(bool?: boolean, back?: boolean) {
|
||||
const obj = {
|
||||
work_flow: getGraphData()
|
||||
}
|
||||
loading.value = back || false
|
||||
application
|
||||
.asyncPutApplication(id, obj)
|
||||
.then((res) => {
|
||||
saveTime.value = new Date()
|
||||
if (bool) {
|
||||
cloneWorkFlow.value = getGraphData()
|
||||
MsgSuccess(t('common.saveSuccess'))
|
||||
if (back) {
|
||||
router.push({ path: `/application/${id}/WORK_FLOW/overview` })
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 定时保存
|
||||
*/
|
||||
const initInterval = () => {
|
||||
interval = setInterval(() => {
|
||||
saveApplication()
|
||||
}, 60000)
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭定时
|
||||
*/
|
||||
const closeInterval = () => {
|
||||
if (interval) {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getDetail()
|
||||
const workflowAutoSave = localStorage.getItem('workflowAutoSave')
|
||||
isSave.value = workflowAutoSave === 'true' ? true : false
|
||||
// 初始化定时任务
|
||||
if (hasPermission(`APPLICATION:MANAGE:${id}`, 'AND') && isSave.value) {
|
||||
initInterval()
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// 清除定时任务
|
||||
closeInterval()
|
||||
workflowRef.value?.clearGraphData()
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.application-workflow {
|
||||
background: var(--app-layout-bg-color);
|
||||
height: 100%;
|
||||
.header {
|
||||
background: #ffffff;
|
||||
}
|
||||
.workflow-main {
|
||||
height: calc(100vh - 62px);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.workflow-dropdown-tabs {
|
||||
.el-tabs__nav-wrap {
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.workflow-debug-container {
|
||||
z-index: 2000;
|
||||
position: relative;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #ffffff;
|
||||
background: var(--dialog-bg-gradient-color);
|
||||
box-shadow: 0px 4px 8px 0px rgba(31, 35, 41, 0.1);
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
overflow: hidden;
|
||||
width: 450px;
|
||||
height: 600px;
|
||||
.workflow-debug-header {
|
||||
background: var(--app-header-bg-color);
|
||||
height: var(--app-header-height);
|
||||
line-height: var(--app-header-height);
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
}
|
||||
.scrollbar-height {
|
||||
height: calc(100% - var(--app-header-height) - 24px);
|
||||
padding-top: 24px;
|
||||
}
|
||||
&.enlarge {
|
||||
width: 50% !important;
|
||||
height: 100% !important;
|
||||
bottom: 0 !important;
|
||||
right: 0 !important;
|
||||
}
|
||||
.chat-width {
|
||||
max-width: 100% !important;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
<template>
|
||||
<div class="p-16-24">
|
||||
<h4 class="mb-16">{{ $t('views.application.applicationAccess.title') }}</h4>
|
||||
|
||||
<el-row :gutter="16">
|
||||
<el-col
|
||||
:xs="24"
|
||||
:sm="24"
|
||||
:md="12"
|
||||
:lg="12"
|
||||
:xl="12"
|
||||
class="mb-16"
|
||||
v-for="(item, index) in platforms"
|
||||
:key="index"
|
||||
>
|
||||
<el-card shadow="hover" class="border-none cursor" style="--el-card-padding: 24px">
|
||||
<div class="flex-between">
|
||||
<div class="flex align-center ml-8 mr-8">
|
||||
<img :src="item.logoSrc" alt="" class="icon" />
|
||||
<div class="ml-12">
|
||||
<h5 class="mb-4">{{ item.name }}</h5>
|
||||
<el-text type="info" style="font-size: 12px">{{ item.description }}</el-text>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<el-switch
|
||||
size="small"
|
||||
v-model="item.isActive"
|
||||
@change="changeStatus(item.key, item.isActive)"
|
||||
:disabled="!item.exists"
|
||||
/>
|
||||
<el-divider direction="vertical" />
|
||||
<el-button class="mr-4" @click="openDrawer(item.key)">{{
|
||||
$t('views.application.applicationAccess.setting')
|
||||
}}</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<AccessSettingDrawer ref="AccessSettingDrawerRef" @refresh="refresh" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, onMounted } from 'vue'
|
||||
import AccessSettingDrawer from './component/AccessSettingDrawer.vue'
|
||||
import applicationApi from '@/api/application'
|
||||
import { MsgSuccess } from '@/utils/message'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { t } from '@/locales'
|
||||
|
||||
// 平台数据
|
||||
const platforms = reactive([
|
||||
{
|
||||
key: 'wecom',
|
||||
logoSrc: new URL(`../../assets/logo_wechat-work.svg`, import.meta.url).href,
|
||||
name: t('views.application.applicationAccess.wecom'),
|
||||
description: t('views.application.applicationAccess.wecomTip'),
|
||||
isActive: false,
|
||||
exists: false
|
||||
},
|
||||
{
|
||||
key: 'dingtalk',
|
||||
logoSrc: new URL(`../../assets/logo_dingtalk.svg`, import.meta.url).href,
|
||||
name: t('views.application.applicationAccess.dingtalk'),
|
||||
description: t('views.application.applicationAccess.dingtalkTip'),
|
||||
isActive: false,
|
||||
exists: false
|
||||
},
|
||||
{
|
||||
key: 'wechat',
|
||||
logoSrc: new URL(`../../assets/logo_wechat.svg`, import.meta.url).href,
|
||||
name: t('views.application.applicationAccess.wechat'),
|
||||
description: t('views.application.applicationAccess.wechatTip'),
|
||||
isActive: false,
|
||||
exists: false
|
||||
},
|
||||
{
|
||||
key: 'feishu',
|
||||
logoSrc: new URL(`../../assets/logo_lark.svg`, import.meta.url).href,
|
||||
name: t('views.application.applicationAccess.lark'),
|
||||
description: t('views.application.applicationAccess.larkTip'),
|
||||
isActive: false,
|
||||
exists: false
|
||||
},
|
||||
{
|
||||
key: 'slack',
|
||||
logoSrc: new URL(`../../assets/logo_slack.svg`, import.meta.url).href,
|
||||
name: t('views.application.applicationAccess.slack'),
|
||||
description: t('views.application.applicationAccess.slackTip'),
|
||||
isActive: false,
|
||||
exists: false
|
||||
}
|
||||
])
|
||||
|
||||
const AccessSettingDrawerRef = ref()
|
||||
const loading = ref(false)
|
||||
const route = useRoute()
|
||||
const {
|
||||
params: { id }
|
||||
} = route as any
|
||||
|
||||
function openDrawer(key: string) {
|
||||
AccessSettingDrawerRef.value.open(id, key)
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
getPlatformStatus()
|
||||
}
|
||||
|
||||
function getPlatformStatus() {
|
||||
loading.value = true
|
||||
applicationApi.getPlatformStatus(id).then((res: any) => {
|
||||
platforms.forEach((platform) => {
|
||||
platform.isActive = res.data[platform.key][1]
|
||||
platform.exists = res.data[platform.key][0]
|
||||
})
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function changeStatus(type: string, value: boolean) {
|
||||
const data = {
|
||||
type: type,
|
||||
status: value
|
||||
}
|
||||
applicationApi.updatePlatformStatus(id, data).then(() => {
|
||||
MsgSuccess(t('common.saveSuccess'))
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getPlatformStatus()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.p-16-24 {
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.mb-16 {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.flex-between {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.align-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.ml-8 {
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
.mr-8 {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.ml-12 {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
.mr-4 {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.cursor {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.icon {
|
||||
width: 32px; // 设置图标宽度
|
||||
height: 32px; // 设置图标高度
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,816 @@
|
|||
<template>
|
||||
<LayoutContainer class="create-application">
|
||||
<template #header>
|
||||
<div class="flex-between w-full">
|
||||
<h3>
|
||||
{{ $t('common.setting') }}
|
||||
</h3>
|
||||
<el-button type="primary" @click="submit(applicationFormRef)" :disabled="loading">
|
||||
{{ $t('views.application.applicationForm.buttons.publish') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-row v-loading="loading">
|
||||
<el-col :span="10">
|
||||
<div class="p-24 mb-16" style="padding-bottom: 0">
|
||||
<h4 class="title-decoration-1">
|
||||
{{ $t('views.applicationOverview.appInfo.header') }}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="scrollbar-height-left">
|
||||
<el-scrollbar>
|
||||
<el-form
|
||||
hide-required-asterisk
|
||||
ref="applicationFormRef"
|
||||
:model="applicationForm"
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
require-asterisk-position="right"
|
||||
class="p-24"
|
||||
style="padding-top: 0"
|
||||
>
|
||||
<el-form-item prop="name">
|
||||
<template #label>
|
||||
<div class="flex-between">
|
||||
<span
|
||||
>{{ $t('views.application.applicationForm.form.appName.label') }}
|
||||
<span class="danger">*</span></span
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<el-input
|
||||
v-model="applicationForm.name"
|
||||
maxlength="64"
|
||||
:placeholder="$t('views.application.applicationForm.form.appName.placeholder')"
|
||||
show-word-limit
|
||||
@blur="applicationForm.name = applicationForm.name?.trim()"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
:label="$t('views.application.applicationForm.form.appDescription.label')"
|
||||
>
|
||||
<el-input
|
||||
v-model="applicationForm.desc"
|
||||
type="textarea"
|
||||
:placeholder="
|
||||
$t('views.application.applicationForm.form.appDescription.placeholder')
|
||||
"
|
||||
:rows="3"
|
||||
maxlength="256"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('views.application.applicationForm.form.aiModel.label')">
|
||||
<template #label>
|
||||
<div class="flex-between">
|
||||
<span>{{ $t('views.application.applicationForm.form.aiModel.label') }}</span>
|
||||
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
@click="openAIParamSettingDialog"
|
||||
:disabled="!applicationForm.model_id"
|
||||
>
|
||||
{{ $t('common.paramSetting') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<ModelSelect
|
||||
v-model="applicationForm.model_id"
|
||||
:placeholder="$t('views.application.applicationForm.form.aiModel.placeholder')"
|
||||
:options="modelOptions"
|
||||
@change="model_change"
|
||||
@submitModel="getModel"
|
||||
showFooter
|
||||
:model-type="'LLM'"
|
||||
></ModelSelect>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
:label="$t('views.application.applicationForm.form.roleSettings.label')"
|
||||
>
|
||||
<MdEditorMagnify
|
||||
:title="$t('views.application.applicationForm.form.roleSettings.label')"
|
||||
v-model="applicationForm.model_setting.system"
|
||||
style="height: 120px"
|
||||
@submitDialog="submitSystemDialog"
|
||||
:placeholder="
|
||||
$t('views.application.applicationForm.form.roleSettings.placeholder')
|
||||
"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
prop="model_setting.no_references_prompt"
|
||||
:rules="{
|
||||
required: applicationForm.model_id,
|
||||
message: $t('views.application.applicationForm.form.prompt.requiredMessage'),
|
||||
trigger: 'blur'
|
||||
}"
|
||||
>
|
||||
<template #label>
|
||||
<div class="flex align-center">
|
||||
<span class="mr-4"
|
||||
>{{
|
||||
$t('views.application.applicationForm.form.prompt.label') +
|
||||
$t('views.application.applicationForm.form.prompt.noReferences')
|
||||
}}
|
||||
</span>
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
:content="
|
||||
$t('views.application.applicationForm.form.prompt.noReferencesTooltip', {
|
||||
question: '{question}'
|
||||
})
|
||||
"
|
||||
placement="right"
|
||||
popper-class="max-w-350"
|
||||
>
|
||||
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
|
||||
</el-tooltip>
|
||||
<span class="danger ml-4" v-if="applicationForm.model_id">*</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<MdEditorMagnify
|
||||
:title="
|
||||
$t('views.application.applicationForm.form.prompt.label') +
|
||||
$t('views.application.applicationForm.form.prompt.noReferences')
|
||||
"
|
||||
v-model="applicationForm.model_setting.no_references_prompt"
|
||||
style="height: 120px"
|
||||
@submitDialog="submitNoReferencesPromptDialog"
|
||||
placeholder="{question}"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
:label="$t('views.application.applicationForm.form.historyRecord.label')"
|
||||
@click.prevent
|
||||
>
|
||||
<el-input-number
|
||||
v-model="applicationForm.dialogue_number"
|
||||
:min="0"
|
||||
:value-on-clear="0"
|
||||
controls-position="right"
|
||||
class="w-full"
|
||||
:step="1"
|
||||
:step-strictly="true"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
label="$t('views.application.applicationForm.form.relatedKnowledgeBase')"
|
||||
>
|
||||
<template #label>
|
||||
<div class="flex-between">
|
||||
<span>{{
|
||||
$t('views.application.applicationForm.form.relatedKnowledge.label')
|
||||
}}</span>
|
||||
<div>
|
||||
<el-button type="primary" link @click="openParamSettingDialog">
|
||||
<AppIcon iconName="app-operation" class="mr-4"></AppIcon>
|
||||
{{ $t('common.paramSetting') }}
|
||||
</el-button>
|
||||
<el-button type="primary" link @click="openDatasetDialog">
|
||||
<el-icon class="mr-4">
|
||||
<Plus />
|
||||
</el-icon>
|
||||
{{ $t('common.add') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="w-full">
|
||||
<el-text type="info" v-if="applicationForm.dataset_id_list?.length === 0"
|
||||
>{{ $t('views.application.applicationForm.form.relatedKnowledge.placeholder') }}
|
||||
</el-text>
|
||||
<el-row :gutter="12" v-else>
|
||||
<el-col
|
||||
:xs="24"
|
||||
:sm="24"
|
||||
:md="24"
|
||||
:lg="12"
|
||||
:xl="12"
|
||||
class="mb-8"
|
||||
v-for="(item, index) in applicationForm.dataset_id_list"
|
||||
:key="index"
|
||||
>
|
||||
<el-card class="relate-dataset-card border-r-4" shadow="never">
|
||||
<div class="flex-between">
|
||||
<div class="flex align-center" style="width: 80%">
|
||||
<AppAvatar
|
||||
v-if="relatedObject(datasetList, item, 'id')?.type === '1'"
|
||||
class="mr-8 avatar-purple"
|
||||
shape="square"
|
||||
:size="32"
|
||||
>
|
||||
<img src="@/assets/knowledge/icon_web.svg" style="width: 58%" alt="" />
|
||||
</AppAvatar>
|
||||
<AppAvatar
|
||||
v-else-if="relatedObject(datasetList, item, 'id')?.type === '2'"
|
||||
class="mr-8 avatar-purple"
|
||||
shape="square"
|
||||
:size="32"
|
||||
style="background: none"
|
||||
>
|
||||
<img src="@/assets/knowledge/logo_lark.svg" style="width: 100%" alt="" />
|
||||
</AppAvatar>
|
||||
<AppAvatar v-else class="mr-8 avatar-blue" shape="square" :size="32">
|
||||
<img src="@/assets/knowledge/icon_document.svg" style="width: 58%" alt="" />
|
||||
</AppAvatar>
|
||||
|
||||
<span
|
||||
class="ellipsis cursor"
|
||||
:title="relatedObject(datasetList, item, 'id')?.name"
|
||||
>
|
||||
{{ relatedObject(datasetList, item, 'id')?.name }}</span
|
||||
>
|
||||
</div>
|
||||
<el-button text @click="removeDataset(item)">
|
||||
<el-icon>
|
||||
<Close />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
:label="$t('views.application.applicationForm.form.prompt.label')"
|
||||
prop="model_setting.prompt"
|
||||
:rules="{
|
||||
required: applicationForm.model_id,
|
||||
message: $t('views.application.applicationForm.form.prompt.requiredMessage'),
|
||||
trigger: 'blur'
|
||||
}"
|
||||
>
|
||||
<template #label>
|
||||
<div class="flex align-center">
|
||||
<span class="mr-4">
|
||||
{{ $t('views.application.applicationForm.form.prompt.label') }}
|
||||
{{ $t('views.application.applicationForm.form.prompt.references') }}
|
||||
</span>
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
:content="
|
||||
$t('views.application.applicationForm.form.prompt.referencesTooltip', {
|
||||
data: '{data}',
|
||||
question: '{question}'
|
||||
})
|
||||
"
|
||||
popper-class="max-w-350"
|
||||
placement="right"
|
||||
>
|
||||
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
|
||||
</el-tooltip>
|
||||
<span class="danger ml-4" v-if="applicationForm.model_id">*</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<MdEditorMagnify
|
||||
:title="
|
||||
$t('views.application.applicationForm.form.prompt.label') +
|
||||
$t('views.application.applicationForm.form.prompt.references')
|
||||
"
|
||||
v-model="applicationForm.model_setting.prompt"
|
||||
style="height: 150px"
|
||||
@submitDialog="submitPromptDialog"
|
||||
:placeholder="defaultPrompt"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('views.application.applicationForm.form.prologue')">
|
||||
<MdEditorMagnify
|
||||
:title="$t('views.application.applicationForm.form.prologue')"
|
||||
v-model="applicationForm.prologue"
|
||||
style="height: 150px"
|
||||
@submitDialog="submitPrologueDialog"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item @click.prevent>
|
||||
<template #label>
|
||||
<div class="flex-between">
|
||||
<span class="mr-4">
|
||||
{{ $t('views.application.applicationForm.form.reasoningContent.label') }}
|
||||
</span>
|
||||
|
||||
<div class="flex">
|
||||
<el-button type="primary" link @click="openReasoningParamSettingDialog">
|
||||
<el-icon><Setting /></el-icon>
|
||||
</el-button>
|
||||
<el-switch
|
||||
class="ml-8"
|
||||
size="small"
|
||||
v-model="applicationForm.model_setting.reasoning_content_enable"
|
||||
@change="sttModelEnableChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
prop="stt_model_id"
|
||||
:rules="{
|
||||
required: applicationForm.stt_model_enable,
|
||||
message: $t('views.application.applicationForm.form.voiceInput.requiredMessage'),
|
||||
trigger: 'change'
|
||||
}"
|
||||
>
|
||||
<template #label>
|
||||
<div class="flex-between">
|
||||
<span class="mr-4">
|
||||
{{ $t('views.application.applicationForm.form.voiceInput.label') }}
|
||||
<span class="danger" v-if="applicationForm.stt_model_enable">*</span>
|
||||
</span>
|
||||
|
||||
<div class="flex">
|
||||
<el-checkbox
|
||||
v-if="applicationForm.stt_model_enable"
|
||||
v-model="applicationForm.stt_autosend"
|
||||
>{{
|
||||
$t('views.application.applicationForm.form.voiceInput.autoSend')
|
||||
}}</el-checkbox
|
||||
>
|
||||
<el-switch
|
||||
class="ml-8"
|
||||
size="small"
|
||||
v-model="applicationForm.stt_model_enable"
|
||||
@change="sttModelEnableChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<ModelSelect
|
||||
v-show="applicationForm.stt_model_enable"
|
||||
v-model="applicationForm.stt_model_id"
|
||||
:placeholder="$t('views.application.applicationForm.form.voiceInput.placeholder')"
|
||||
:options="sttModelOptions"
|
||||
:model-type="'STT'"
|
||||
></ModelSelect>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
prop="tts_model_id"
|
||||
:rules="{
|
||||
required: applicationForm.tts_type === 'TTS' && applicationForm.tts_model_enable,
|
||||
message: $t('views.application.applicationForm.form.voicePlay.requiredMessage'),
|
||||
trigger: 'change'
|
||||
}"
|
||||
>
|
||||
<template #label>
|
||||
<div class="flex-between">
|
||||
<span class="mr-4"
|
||||
>{{ $t('views.application.applicationForm.form.voicePlay.label') }}
|
||||
<span
|
||||
class="danger"
|
||||
v-if="
|
||||
applicationForm.tts_type === 'TTS' && applicationForm.tts_model_enable
|
||||
"
|
||||
>*</span
|
||||
>
|
||||
</span>
|
||||
<div class="flex">
|
||||
<el-checkbox
|
||||
v-if="applicationForm.tts_model_enable"
|
||||
v-model="applicationForm.tts_autoplay"
|
||||
>{{
|
||||
$t('views.application.applicationForm.form.voicePlay.autoPlay')
|
||||
}}</el-checkbox
|
||||
>
|
||||
<el-switch
|
||||
class="ml-8"
|
||||
size="small"
|
||||
v-model="applicationForm.tts_model_enable"
|
||||
@change="ttsModelEnableChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="w-full">
|
||||
<el-radio-group
|
||||
v-model="applicationForm.tts_type"
|
||||
v-show="applicationForm.tts_model_enable"
|
||||
class="mb-8"
|
||||
>
|
||||
<el-radio value="BROWSER">{{
|
||||
$t('views.application.applicationForm.form.voicePlay.browser')
|
||||
}}</el-radio>
|
||||
<el-radio value="TTS">{{
|
||||
$t('views.application.applicationForm.form.voicePlay.tts')
|
||||
}}</el-radio>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div class="flex-between w-full">
|
||||
<ModelSelect
|
||||
v-if="applicationForm.tts_type === 'TTS' && applicationForm.tts_model_enable"
|
||||
v-model="applicationForm.tts_model_id"
|
||||
:placeholder="
|
||||
$t('views.application.applicationForm.form.voicePlay.placeholder')
|
||||
"
|
||||
:options="ttsModelOptions"
|
||||
@change="ttsModelChange()"
|
||||
:model-type="'TTS'"
|
||||
></ModelSelect>
|
||||
|
||||
<el-button
|
||||
v-if="applicationForm.tts_type === 'TTS'"
|
||||
@click="openTTSParamSettingDialog"
|
||||
:disabled="!applicationForm.tts_model_id"
|
||||
class="ml-8"
|
||||
>
|
||||
<el-icon><Operation /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="14" class="p-24 border-l">
|
||||
<h4 class="title-decoration-1 mb-16">
|
||||
{{ $t('views.application.applicationForm.title.appTest') }}
|
||||
</h4>
|
||||
<div class="dialog-bg">
|
||||
<div class="flex align-center p-16 mb-8">
|
||||
<div
|
||||
class="edit-avatar mr-12"
|
||||
@mouseenter="showEditIcon = true"
|
||||
@mouseleave="showEditIcon = false"
|
||||
>
|
||||
<AppAvatar
|
||||
v-if="isAppIcon(applicationForm?.icon)"
|
||||
shape="square"
|
||||
:size="32"
|
||||
style="background: none"
|
||||
>
|
||||
<img :src="applicationForm?.icon" alt="" />
|
||||
</AppAvatar>
|
||||
<AppAvatar
|
||||
v-else-if="applicationForm?.name"
|
||||
:name="applicationForm?.name"
|
||||
pinyinColor
|
||||
shape="square"
|
||||
:size="32"
|
||||
/>
|
||||
<AppAvatar
|
||||
v-if="showEditIcon"
|
||||
shape="square"
|
||||
class="edit-mask"
|
||||
:size="32"
|
||||
@click="openEditAvatar"
|
||||
>
|
||||
<el-icon><EditPen /></el-icon>
|
||||
</AppAvatar>
|
||||
</div>
|
||||
<h4>
|
||||
{{
|
||||
applicationForm?.name || $t('views.application.applicationForm.form.appName.label')
|
||||
}}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="scrollbar-height">
|
||||
<AiChat :applicationDetails="applicationForm" :type="'debug-ai-chat'"></AiChat>
|
||||
</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<AIModeParamSettingDialog ref="AIModeParamSettingDialogRef" @refresh="refreshForm" />
|
||||
<TTSModeParamSettingDialog ref="TTSModeParamSettingDialogRef" @refresh="refreshTTSForm" />
|
||||
<ParamSettingDialog ref="ParamSettingDialogRef" @refresh="refreshParam" />
|
||||
<AddDatasetDialog
|
||||
ref="AddDatasetDialogRef"
|
||||
@addData="addDataset"
|
||||
:data="datasetList"
|
||||
@refresh="refresh"
|
||||
:loading="datasetLoading"
|
||||
/>
|
||||
|
||||
<EditAvatarDialog ref="EditAvatarDialogRef" @refresh="refreshIcon" />
|
||||
<ReasoningParamSettingDialog
|
||||
ref="ReasoningParamSettingDialogRef"
|
||||
@refresh="submitReasoningDialog"
|
||||
/>
|
||||
</LayoutContainer>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, onMounted } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { groupBy } from 'lodash'
|
||||
import AIModeParamSettingDialog from './component/AIModeParamSettingDialog.vue'
|
||||
import ParamSettingDialog from './component/ParamSettingDialog.vue'
|
||||
import AddDatasetDialog from './component/AddDatasetDialog.vue'
|
||||
import EditAvatarDialog from '@/views/application-overview/component/EditAvatarDialog.vue'
|
||||
import applicationApi from '@/api/application'
|
||||
import { isAppIcon } from '@/utils/common'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import type { ApplicationFormType } from '@/api/type/application'
|
||||
import { relatedObject } from '@/utils/utils'
|
||||
import { MsgSuccess, MsgWarning } from '@/utils/message'
|
||||
import useStore from '@/stores'
|
||||
import { t } from '@/locales'
|
||||
import TTSModeParamSettingDialog from './component/TTSModeParamSettingDialog.vue'
|
||||
import ReasoningParamSettingDialog from './component/ReasoningParamSettingDialog.vue'
|
||||
|
||||
const { model, application } = useStore()
|
||||
|
||||
const route = useRoute()
|
||||
const {
|
||||
params: { id }
|
||||
} = route as any
|
||||
// @ts-ignore
|
||||
const defaultPrompt = t('views.application.applicationForm.form.prompt.defaultPrompt', {
|
||||
data: '{data}',
|
||||
question: '{question}'
|
||||
})
|
||||
|
||||
const optimizationPrompt =
|
||||
t('views.application.applicationForm.dialog.defaultPrompt1', {
|
||||
question: '{question}'
|
||||
}) +
|
||||
'<data></data>' +
|
||||
t('views.application.applicationForm.dialog.defaultPrompt2')
|
||||
|
||||
const AIModeParamSettingDialogRef = ref<InstanceType<typeof AIModeParamSettingDialog>>()
|
||||
const ReasoningParamSettingDialogRef = ref<InstanceType<typeof ReasoningParamSettingDialog>>()
|
||||
const TTSModeParamSettingDialogRef = ref<InstanceType<typeof TTSModeParamSettingDialog>>()
|
||||
const ParamSettingDialogRef = ref<InstanceType<typeof ParamSettingDialog>>()
|
||||
|
||||
const applicationFormRef = ref<FormInstance>()
|
||||
const AddDatasetDialogRef = ref()
|
||||
const EditAvatarDialogRef = ref()
|
||||
|
||||
const loading = ref(false)
|
||||
const datasetLoading = ref(false)
|
||||
const applicationForm = ref<ApplicationFormType>({
|
||||
name: '',
|
||||
desc: '',
|
||||
model_id: '',
|
||||
dialogue_number: 1,
|
||||
prologue: t('views.application.applicationForm.form.defaultPrologue'),
|
||||
dataset_id_list: [],
|
||||
dataset_setting: {
|
||||
top_n: 3,
|
||||
similarity: 0.6,
|
||||
max_paragraph_char_number: 5000,
|
||||
search_mode: 'embedding',
|
||||
no_references_setting: {
|
||||
status: 'ai_questioning',
|
||||
value: '{question}'
|
||||
}
|
||||
},
|
||||
model_setting: {
|
||||
prompt: defaultPrompt,
|
||||
system: t('views.application.applicationForm.form.roleSettings.placeholder'),
|
||||
no_references_prompt: '{question}',
|
||||
reasoning_content_enable: false
|
||||
},
|
||||
model_params_setting: {},
|
||||
problem_optimization: false,
|
||||
problem_optimization_prompt: optimizationPrompt,
|
||||
stt_model_id: '',
|
||||
tts_model_id: '',
|
||||
stt_model_enable: false,
|
||||
tts_model_enable: false,
|
||||
tts_type: 'BROWSER',
|
||||
type: 'SIMPLE'
|
||||
})
|
||||
|
||||
const rules = reactive<FormRules<ApplicationFormType>>({
|
||||
name: [
|
||||
{
|
||||
required: true,
|
||||
message: t('views.application.applicationForm.form.appName.placeholder'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
})
|
||||
const modelOptions = ref<any>(null)
|
||||
const datasetList = ref([])
|
||||
const sttModelOptions = ref<any>(null)
|
||||
const ttsModelOptions = ref<any>(null)
|
||||
const showEditIcon = ref(false)
|
||||
|
||||
function submitPrologueDialog(val: string) {
|
||||
applicationForm.value.prologue = val
|
||||
}
|
||||
function submitPromptDialog(val: string) {
|
||||
applicationForm.value.model_setting.prompt = val
|
||||
}
|
||||
function submitNoReferencesPromptDialog(val: string) {
|
||||
applicationForm.value.model_setting.no_references_prompt = val
|
||||
}
|
||||
function submitSystemDialog(val: string) {
|
||||
applicationForm.value.model_setting.system = val
|
||||
}
|
||||
function submitReasoningDialog(val: any) {
|
||||
applicationForm.value.model_setting = {
|
||||
...applicationForm.value.model_setting,
|
||||
...val
|
||||
}
|
||||
}
|
||||
|
||||
const submit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate((valid, fields) => {
|
||||
if (valid) {
|
||||
application.asyncPutApplication(id, applicationForm.value, loading).then((res) => {
|
||||
MsgSuccess(t('common.saveSuccess'))
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
const model_change = (model_id?: string) => {
|
||||
applicationForm.value.model_id = model_id
|
||||
if (model_id) {
|
||||
AIModeParamSettingDialogRef.value?.reset_default(model_id, id)
|
||||
} else {
|
||||
refreshForm({})
|
||||
}
|
||||
}
|
||||
const openAIParamSettingDialog = () => {
|
||||
if (applicationForm.value.model_id) {
|
||||
AIModeParamSettingDialogRef.value?.open(
|
||||
applicationForm.value.model_id,
|
||||
id,
|
||||
applicationForm.value.model_params_setting
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const openReasoningParamSettingDialog = () => {
|
||||
ReasoningParamSettingDialogRef.value?.open(applicationForm.value.model_setting)
|
||||
}
|
||||
|
||||
const openTTSParamSettingDialog = () => {
|
||||
if (applicationForm.value.tts_model_id) {
|
||||
TTSModeParamSettingDialogRef.value?.open(
|
||||
applicationForm.value.tts_model_id,
|
||||
id,
|
||||
applicationForm.value.tts_model_params_setting
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const openParamSettingDialog = () => {
|
||||
ParamSettingDialogRef.value?.open(applicationForm.value)
|
||||
}
|
||||
|
||||
function refreshParam(data: any) {
|
||||
applicationForm.value = { ...applicationForm.value, ...data }
|
||||
}
|
||||
|
||||
function refreshForm(data: any) {
|
||||
applicationForm.value.model_params_setting = data
|
||||
}
|
||||
|
||||
function refreshTTSForm(data: any) {
|
||||
applicationForm.value.tts_model_params_setting = data
|
||||
}
|
||||
|
||||
function removeDataset(id: any) {
|
||||
if (applicationForm.value.dataset_id_list) {
|
||||
applicationForm.value.dataset_id_list.splice(
|
||||
applicationForm.value.dataset_id_list.indexOf(id),
|
||||
1
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function addDataset(val: Array<string>) {
|
||||
applicationForm.value.dataset_id_list = val
|
||||
}
|
||||
|
||||
function openDatasetDialog() {
|
||||
AddDatasetDialogRef.value.open(applicationForm.value.dataset_id_list)
|
||||
}
|
||||
|
||||
function getDetail() {
|
||||
application.asyncGetApplicationDetail(id, loading).then((res: any) => {
|
||||
applicationForm.value = res.data
|
||||
applicationForm.value.model_id = res.data.model
|
||||
applicationForm.value.stt_model_id = res.data.stt_model
|
||||
applicationForm.value.tts_model_id = res.data.tts_model
|
||||
applicationForm.value.tts_type = res.data.tts_type
|
||||
applicationForm.value.model_setting.no_references_prompt =
|
||||
res.data.model_setting.no_references_prompt || '{question}'
|
||||
application.asyncGetAccessToken(id, loading).then((res: any) => {
|
||||
applicationForm.value = { ...applicationForm.value, ...res.data }
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
function getDataset() {
|
||||
application.asyncGetApplicationDataset(id, datasetLoading).then((res: any) => {
|
||||
datasetList.value = res.data
|
||||
})
|
||||
}
|
||||
|
||||
function getModel() {
|
||||
loading.value = true
|
||||
applicationApi
|
||||
.getApplicationModel(id)
|
||||
.then((res: any) => {
|
||||
modelOptions.value = groupBy(res?.data, 'provider')
|
||||
loading.value = false
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function getSTTModel() {
|
||||
loading.value = true
|
||||
applicationApi
|
||||
.getApplicationSTTModel(id)
|
||||
.then((res: any) => {
|
||||
sttModelOptions.value = groupBy(res?.data, 'provider')
|
||||
loading.value = false
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function getTTSModel() {
|
||||
loading.value = true
|
||||
applicationApi
|
||||
.getApplicationTTSModel(id)
|
||||
.then((res: any) => {
|
||||
ttsModelOptions.value = groupBy(res?.data, 'provider')
|
||||
loading.value = false
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function ttsModelChange() {
|
||||
if (applicationForm.value.tts_model_id) {
|
||||
TTSModeParamSettingDialogRef.value?.reset_default(applicationForm.value.tts_model_id, id)
|
||||
} else {
|
||||
refreshTTSForm({})
|
||||
}
|
||||
}
|
||||
|
||||
function ttsModelEnableChange() {
|
||||
if (!applicationForm.value.tts_model_enable) {
|
||||
applicationForm.value.tts_model_id = ''
|
||||
applicationForm.value.tts_type = 'BROWSER'
|
||||
}
|
||||
}
|
||||
|
||||
function sttModelEnableChange() {
|
||||
if (!applicationForm.value.stt_model_enable) {
|
||||
applicationForm.value.stt_model_id = ''
|
||||
}
|
||||
}
|
||||
|
||||
function openEditAvatar() {
|
||||
EditAvatarDialogRef.value.open(applicationForm.value)
|
||||
}
|
||||
function refreshIcon() {
|
||||
getDetail()
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
getDataset()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getModel()
|
||||
getDataset()
|
||||
getDetail()
|
||||
getSTTModel()
|
||||
getTTSModel()
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.create-application {
|
||||
.relate-dataset-card {
|
||||
color: var(--app-text-color);
|
||||
}
|
||||
|
||||
.dialog-bg {
|
||||
border-radius: 8px;
|
||||
background: var(--dialog-bg-gradient-color);
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.scrollbar-height-left {
|
||||
height: calc(var(--app-main-height) - 64px);
|
||||
}
|
||||
|
||||
.scrollbar-height {
|
||||
height: calc(var(--app-main-height) - 166px);
|
||||
}
|
||||
}
|
||||
|
||||
.prologue-md-editor {
|
||||
height: 150px;
|
||||
}
|
||||
|
||||
:deep(.el-form-item__label) {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
align-center
|
||||
:title="$t('common.paramSetting')"
|
||||
v-model="dialogVisible"
|
||||
style="width: 550px"
|
||||
append-to-body
|
||||
destroy-on-close
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
>
|
||||
<DynamicsForm
|
||||
v-model="form_data"
|
||||
:model="form_data"
|
||||
label-position="top"
|
||||
require-asterisk-position="right"
|
||||
:render_data="model_form_field"
|
||||
ref="dynamicsFormRef"
|
||||
>
|
||||
</DynamicsForm>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click.prevent="dialogVisible = false">
|
||||
{{ $t('common.cancel') }}
|
||||
</el-button>
|
||||
<el-button type="primary" @click="submit" :loading="loading">
|
||||
{{ $t('common.confirm') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import type { FormField } from '@/components/dynamics-form/type'
|
||||
import modelAPi from '@/api/model'
|
||||
import applicationApi from '@/api/application'
|
||||
import DynamicsForm from '@/components/dynamics-form/index.vue'
|
||||
const model_form_field = ref<Array<FormField>>([])
|
||||
const emit = defineEmits(['refresh'])
|
||||
const dynamicsFormRef = ref<InstanceType<typeof DynamicsForm>>()
|
||||
const form_data = ref<any>({})
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const getApi = (model_id: string, application_id?: string) => {
|
||||
return application_id
|
||||
? applicationApi.getModelParamsForm(application_id, model_id, loading)
|
||||
: modelAPi.getModelParamsForm(model_id, loading)
|
||||
}
|
||||
const open = (model_id: string, application_id?: string, model_setting_data?: any) => {
|
||||
form_data.value = {}
|
||||
const api = getApi(model_id, application_id)
|
||||
api.then((ok) => {
|
||||
model_form_field.value = ok.data
|
||||
// 渲染动态表单
|
||||
dynamicsFormRef.value?.render(model_form_field.value, model_setting_data)
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const reset_default = (model_id: string, application_id?: string) => {
|
||||
const api = getApi(model_id, application_id)
|
||||
api.then((ok) => {
|
||||
model_form_field.value = ok.data
|
||||
const model_setting_data = ok.data
|
||||
.map((item) => {
|
||||
if (item.show_default_value === false) {
|
||||
return { [item.field]: undefined }
|
||||
} else {
|
||||
return { [item.field]: item.default_value }
|
||||
}
|
||||
})
|
||||
.reduce((x, y) => ({ ...x, ...y }), {})
|
||||
|
||||
emit('refresh', model_setting_data)
|
||||
})
|
||||
}
|
||||
|
||||
const submit = async () => {
|
||||
dynamicsFormRef.value?.validate().then(() => {
|
||||
emit('refresh', form_data.value)
|
||||
dialogVisible.value = false
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({ open, reset_default })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,400 @@
|
|||
<template>
|
||||
<el-drawer v-model="visible" size="60%" :append-to-body="true">
|
||||
<template #header>
|
||||
<div class="flex align-center" style="margin-left: -8px">
|
||||
<h4>{{ drawerTitle }}</h4>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<el-form
|
||||
v-if="dataLoaded"
|
||||
ref="formRef"
|
||||
:model="form[configType]"
|
||||
label-width="120px"
|
||||
:rules="rules[configType]"
|
||||
label-position="top"
|
||||
require-asterisk-position="right"
|
||||
>
|
||||
<h4 class="title-decoration-1 mb-16">{{ infoTitle }}</h4>
|
||||
|
||||
<template v-for="(item, key) in configFields[configType]" :key="key">
|
||||
<el-form-item :label="item.label" :prop="key">
|
||||
<el-input
|
||||
v-model="form[configType][key]"
|
||||
:type="isPasswordField(key) ? (passwordVisible[key] ? 'text' : 'password') : 'text'"
|
||||
:placeholder="item.placeholder"
|
||||
:show-password="isPasswordField(key)"
|
||||
>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
</template>
|
||||
<div v-if="configType === 'wechat'" class="flex align-center mb-16">
|
||||
<span class="lighter mr-8">{{
|
||||
$t('views.application.applicationAccess.wecomSetting.authenticationSuccessful')
|
||||
}}</span>
|
||||
<el-switch v-if="configType === 'wechat'" v-model="form[configType].is_certification" />
|
||||
</div>
|
||||
|
||||
<h4 class="title-decoration-1 mb-16">
|
||||
{{ $t('views.application.applicationAccess.callback') }}
|
||||
</h4>
|
||||
<el-form-item label="URL" prop="callback_url">
|
||||
<el-input
|
||||
v-model="form[configType].callback_url"
|
||||
:placeholder="$t('views.application.applicationAccess.callbackTip')"
|
||||
readonly
|
||||
>
|
||||
<template #append>
|
||||
<el-button @click="copyClick(form[configType].callback_url)">
|
||||
<AppIcon iconName="app-copy"></AppIcon>
|
||||
</el-button>
|
||||
</template>
|
||||
</el-input>
|
||||
<el-text type="info" v-if="configType === 'wechat'">
|
||||
{{ $t('views.application.applicationAccess.copyUrl') }}
|
||||
<a
|
||||
class="primary"
|
||||
href="https://mp.weixin.qq.com/advanced/advanced?action=dev&t=advanced/dev"
|
||||
target="_blank"
|
||||
>{{ $t('views.application.applicationAccess.wechatPlatform') }}</a
|
||||
>{{ $t('views.application.applicationAccess.wechatSetting.urlInfo') }}
|
||||
</el-text>
|
||||
<el-text type="info" v-if="configType === 'dingtalk'">
|
||||
{{ $t('views.application.applicationAccess.copyUrl') }}
|
||||
<a
|
||||
class="primary"
|
||||
href="https://open-dev.dingtalk.com/fe/app?hash=%23%2Fcorp%2Fapp#/corp/app"
|
||||
target="_blank"
|
||||
>{{ $t('views.application.applicationAccess.dingtalkPlatform') }}</a
|
||||
>{{ $t('views.application.applicationAccess.dingtalkSetting.urlInfo') }}
|
||||
</el-text>
|
||||
<el-text type="info" v-if="configType === 'wecom'">
|
||||
{{ $t('views.application.applicationAccess.copyUrl') }}
|
||||
<a
|
||||
class="primary"
|
||||
href="https://work.weixin.qq.com/wework_admin/frame#apps"
|
||||
target="_blank"
|
||||
>{{ $t('views.application.applicationAccess.wecomPlatform') }}</a
|
||||
>{{ $t('views.application.applicationAccess.wecomSetting.urlInfo') }}
|
||||
</el-text>
|
||||
<el-text type="info" v-if="configType === 'feishu'">
|
||||
{{ $t('views.application.applicationAccess.copyUrl') }}
|
||||
<a class="primary" href="https://open.feishu.cn/app/" target="_blank">{{
|
||||
$t('views.application.applicationAccess.larkPlatform')
|
||||
}}</a
|
||||
>{{ $t('views.application.applicationAccess.larkSetting.urlInfo') }}
|
||||
</el-text>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<div>
|
||||
<el-button @click="closeDrawer">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="submit" :disabled="loading">
|
||||
{{ $t('common.save') }}
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</el-drawer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, computed } from 'vue'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import applicationApi from '@/api/application'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { MsgError, MsgSuccess } from '@/utils/message'
|
||||
import { copyClick } from '@/utils/clipboard'
|
||||
import { t } from '@/locales'
|
||||
|
||||
type PlatformType = 'wechat' | 'dingtalk' | 'wecom' | 'feishu' | 'slack'
|
||||
|
||||
const formRef = ref<FormInstance>()
|
||||
const visible = ref(false)
|
||||
const loading = ref(false)
|
||||
const dataLoaded = ref(false)
|
||||
const configType = ref<PlatformType>('wechat')
|
||||
const route = useRoute()
|
||||
const emit = defineEmits(['refresh'])
|
||||
const {
|
||||
params: { id }
|
||||
} = route as any
|
||||
|
||||
const form = reactive<any>({
|
||||
wechat: {
|
||||
app_id: '',
|
||||
app_secret: '',
|
||||
token: '',
|
||||
encoding_aes_key: '',
|
||||
is_certification: false,
|
||||
callback_url: ''
|
||||
},
|
||||
dingtalk: { client_id: '', client_secret: '', callback_url: '' },
|
||||
wecom: {
|
||||
app_id: '',
|
||||
agent_id: '',
|
||||
secret: '',
|
||||
token: '',
|
||||
encoding_aes_key: '',
|
||||
callback_url: ''
|
||||
},
|
||||
feishu: { app_id: '', app_secret: '', verification_token: '', callback_url: '' },
|
||||
slack: { signing_secret: '', bot_user_token: '', callback_url: '' }
|
||||
})
|
||||
|
||||
const rules = reactive<{ [propName: string]: any }>({
|
||||
wechat: {
|
||||
app_id: [
|
||||
{
|
||||
required: true,
|
||||
message: t('views.application.applicationAccess.wechatSetting.appIdPlaceholder'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
app_secret: [
|
||||
{
|
||||
required: true,
|
||||
message: t('views.application.applicationAccess.wechatSetting.appSecretPlaceholder'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
token: [
|
||||
{
|
||||
required: true,
|
||||
message: t('views.application.applicationAccess.wechatSetting.tokenPlaceholder'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
encoding_aes_key: [
|
||||
{
|
||||
required: true,
|
||||
message: t('views.application.applicationAccess.wechatSetting.aesKeyPlaceholder'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
},
|
||||
dingtalk: {
|
||||
client_id: [
|
||||
{
|
||||
required: true,
|
||||
message: t('views.application.applicationAccess.dingtalkSetting.clientIdPlaceholder'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
client_secret: [
|
||||
{
|
||||
required: true,
|
||||
message: t('views.application.applicationAccess.dingtalkSetting.clientSecretPlaceholder'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
},
|
||||
wecom: {
|
||||
app_id: [
|
||||
{
|
||||
required: true,
|
||||
message: t('views.application.applicationAccess.wecomSetting.cropIdPlaceholder'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
agent_id: [
|
||||
{
|
||||
required: true,
|
||||
message: t('views.application.applicationAccess.wecomSetting.agentIdPlaceholder'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
secret: [
|
||||
{
|
||||
required: true,
|
||||
message: t('views.application.applicationAccess.wecomSetting.secretPlaceholder'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
token: [
|
||||
{
|
||||
required: true,
|
||||
message: t('views.application.applicationAccess.wecomSetting.tokenPlaceholder'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
encoding_aes_key: [
|
||||
{
|
||||
required: true,
|
||||
message: t('views.application.applicationAccess.wecomSetting.encodingAesKeyPlaceholder'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
},
|
||||
feishu: {
|
||||
app_id: [
|
||||
{
|
||||
required: true,
|
||||
message: t('views.application.applicationAccess.larkSetting.appIdPlaceholder'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
app_secret: [
|
||||
{
|
||||
required: true,
|
||||
message: t('views.application.applicationAccess.larkSetting.appSecretPlaceholder'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
verification_token: [
|
||||
{
|
||||
required: false,
|
||||
message: t('views.application.applicationAccess.larkSetting.verificationTokenPlaceholder'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
},
|
||||
slack: {
|
||||
signing_secret: [
|
||||
{
|
||||
required: true,
|
||||
message: t('views.application.applicationAccess.slackSetting.signingSecretPlaceholder'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
bot_user_token: [
|
||||
{
|
||||
required: true,
|
||||
message: t('views.application.applicationAccess.slackSetting.botUserTokenPlaceholder'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
const configFields: { [propName: string]: { [propName: string]: any } } = {
|
||||
wechat: {
|
||||
app_id: {
|
||||
label: t('views.application.applicationAccess.wechatSetting.appId'),
|
||||
placeholder: ''
|
||||
},
|
||||
app_secret: {
|
||||
label: t('views.application.applicationAccess.wechatSetting.appSecret'),
|
||||
placeholder: ''
|
||||
},
|
||||
token: { label: t('views.application.applicationAccess.wechatSetting.token'), placeholder: '' },
|
||||
encoding_aes_key: {
|
||||
label: t('views.application.applicationAccess.wechatSetting.aesKey'),
|
||||
placeholder: ''
|
||||
}
|
||||
},
|
||||
dingtalk: {
|
||||
client_id: { label: 'Client ID', placeholder: '' },
|
||||
client_secret: { label: 'Client Secret', placeholder: '' }
|
||||
},
|
||||
wecom: {
|
||||
app_id: {
|
||||
label: t('views.application.applicationAccess.wecomSetting.cropId'),
|
||||
placeholder: ''
|
||||
},
|
||||
agent_id: { label: 'Agent ID', placeholder: '' },
|
||||
secret: { label: 'Secret', placeholder: '' },
|
||||
token: { label: 'Token', placeholder: '' },
|
||||
encoding_aes_key: { label: 'EncodingAESKey', placeholder: '' }
|
||||
},
|
||||
feishu: {
|
||||
app_id: { label: 'App ID', placeholder: '' },
|
||||
app_secret: { label: 'App Secret', placeholder: '' },
|
||||
verification_token: { label: 'Verification Token', placeholder: '' }
|
||||
},
|
||||
slack: {
|
||||
signing_secret: { label: 'Signing Secret', placeholder: '' },
|
||||
bot_user_token: { label: 'Bot User Token', placeholder: '' }
|
||||
}
|
||||
}
|
||||
|
||||
const passwordFields = new Set([
|
||||
'app_secret',
|
||||
'client_secret',
|
||||
'secret',
|
||||
'bot_user_token',
|
||||
'signing_secret'
|
||||
])
|
||||
|
||||
const drawerTitle = computed(
|
||||
() =>
|
||||
({
|
||||
wechat: t('views.application.applicationAccess.wechatSetting.title'),
|
||||
dingtalk: t('views.application.applicationAccess.dingtalkSetting.title'),
|
||||
wecom: t('views.application.applicationAccess.wecomSetting.title'),
|
||||
feishu: t('views.application.applicationAccess.larkSetting.title'),
|
||||
slack: t('views.application.applicationAccess.slackSetting.title')
|
||||
}[configType.value])
|
||||
)
|
||||
|
||||
const infoTitle = computed(
|
||||
() =>
|
||||
({
|
||||
wechat: t('views.applicationOverview.appInfo.header'),
|
||||
dingtalk: t('views.applicationOverview.appInfo.header'),
|
||||
wecom: t('views.applicationOverview.appInfo.header'),
|
||||
feishu: t('views.applicationOverview.appInfo.header'),
|
||||
slack: t('views.applicationOverview.appInfo.header')
|
||||
}[configType.value])
|
||||
)
|
||||
|
||||
const passwordVisible = reactive<Record<string, boolean>>(
|
||||
Object.keys(configFields[configType.value]).reduce(
|
||||
(acc, key) => {
|
||||
if (passwordFields.has(key)) {
|
||||
acc[key] = false
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{} as Record<string, boolean>
|
||||
)
|
||||
)
|
||||
|
||||
const isPasswordField = (key: any) => passwordFields.has(key)
|
||||
|
||||
const closeDrawer = () => {
|
||||
visible.value = false
|
||||
}
|
||||
|
||||
const submit = async () => {
|
||||
if (loading.value) return
|
||||
|
||||
formRef.value?.validate(async (valid) => {
|
||||
if (valid) {
|
||||
try {
|
||||
applicationApi
|
||||
.updatePlatformConfig(id, configType.value, form[configType.value], loading)
|
||||
.then(() => {
|
||||
MsgSuccess(t('common.saveSuccess'))
|
||||
closeDrawer()
|
||||
emit('refresh')
|
||||
})
|
||||
} catch {
|
||||
MsgError(t('views.application.tip.saveErrorMessage'))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const open = async (id: string, type: PlatformType) => {
|
||||
visible.value = true
|
||||
configType.value = type
|
||||
loading.value = true
|
||||
dataLoaded.value = false
|
||||
formRef.value?.resetFields()
|
||||
try {
|
||||
const res = await applicationApi.getPlatformConfig(id, type)
|
||||
if (res.data) {
|
||||
form[configType.value] = res.data
|
||||
}
|
||||
dataLoaded.value = true
|
||||
} catch {
|
||||
MsgError(t('views.application.tip.loadingErrorMessage'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
form[configType.value].callback_url = `${window.location.origin}/api/${type}/${id}`
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
:title="$t('views.application.applicationForm.dialog.addDataset')"
|
||||
v-model="dialogVisible"
|
||||
width="600"
|
||||
append-to-body
|
||||
class="addDataset-dialog"
|
||||
align-center
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
>
|
||||
<template #header="{ titleId, titleClass }">
|
||||
<div class="flex-between mb-8">
|
||||
<h4 :id="titleId" :class="titleClass">
|
||||
{{ $t('views.application.applicationForm.dialog.addDataset') }}
|
||||
</h4>
|
||||
<div class="flex align-center mr-8">
|
||||
<el-button link class="ml-16" @click="refresh">
|
||||
<el-icon class="mr-4"><Refresh /></el-icon>{{ $t('common.refresh') }}
|
||||
</el-button>
|
||||
<el-divider direction="vertical" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-between">
|
||||
<el-text type="info" class="color-secondary">
|
||||
{{ $t('views.application.applicationForm.dialog.addDatasetPlaceholder') }}
|
||||
</el-text>
|
||||
<el-input
|
||||
v-model="searchValue"
|
||||
:placeholder="$t('common.search')"
|
||||
prefix-icon="Search"
|
||||
class="w-240"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<el-scrollbar>
|
||||
<div class="max-height">
|
||||
<el-row :gutter="12" v-loading="loading">
|
||||
<el-col :span="12" v-for="(item, index) in filterData" :key="index" class="mb-16">
|
||||
<CardCheckbox value-field="id" :data="item" v-model="checkList" @change="changeHandle">
|
||||
<span class="ellipsis cursor" :title="item.name"> {{ item.name }}</span>
|
||||
</CardCheckbox>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
<template #footer>
|
||||
<div class="flex-between">
|
||||
<div class="flex">
|
||||
<el-text type="info" class="color-secondary mr-8" v-if="checkList.length > 0">
|
||||
{{ $t('views.application.applicationForm.dialog.selected') }} {{ checkList.length }}
|
||||
{{ $t('views.application.applicationForm.dialog.countDataset') }}
|
||||
</el-text>
|
||||
<el-button link type="primary" v-if="checkList.length > 0" @click="clearCheck">
|
||||
{{ $t('common.clear') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<span>
|
||||
<el-button @click.prevent="dialogVisible = false">
|
||||
{{ $t('common.cancel') }}
|
||||
</el-button>
|
||||
<el-button type="primary" @click="submitHandle">
|
||||
{{ $t('common.confirm') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, watch } from 'vue'
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array<any>,
|
||||
default: () => []
|
||||
},
|
||||
loading: Boolean
|
||||
})
|
||||
|
||||
const emit = defineEmits(['addData', 'refresh'])
|
||||
|
||||
const dialogVisible = ref<boolean>(false)
|
||||
const checkList = ref([])
|
||||
const currentEmbedding = ref('')
|
||||
const searchValue = ref('')
|
||||
const searchDate = ref<any[]>([])
|
||||
|
||||
const filterData = computed(() => {
|
||||
return currentEmbedding.value
|
||||
? searchDate.value.filter((v) => v.embedding_mode_id === currentEmbedding.value)
|
||||
: searchDate.value
|
||||
})
|
||||
|
||||
watch(dialogVisible, (bool) => {
|
||||
if (!bool) {
|
||||
checkList.value = []
|
||||
currentEmbedding.value = ''
|
||||
searchValue.value = ''
|
||||
}
|
||||
})
|
||||
|
||||
watch(searchValue, (val) => {
|
||||
if (val) {
|
||||
searchDate.value = props.data.filter((v) => v.name.includes(val))
|
||||
} else {
|
||||
searchDate.value = props.data
|
||||
}
|
||||
})
|
||||
|
||||
function changeHandle() {
|
||||
if (checkList.value.length > 0) {
|
||||
currentEmbedding.value = props.data.filter(
|
||||
(v) => v.id === checkList.value[0]
|
||||
)[0].embedding_mode_id
|
||||
} else if (checkList.value.length === 0) {
|
||||
currentEmbedding.value = ''
|
||||
}
|
||||
}
|
||||
function clearCheck() {
|
||||
checkList.value = []
|
||||
currentEmbedding.value = ''
|
||||
}
|
||||
|
||||
const open = (checked: any) => {
|
||||
searchDate.value = props.data
|
||||
checkList.value = checked
|
||||
if (checkList.value.length > 0) {
|
||||
currentEmbedding.value = props.data.filter(
|
||||
(v) => v.id === checkList.value[0]
|
||||
)[0].embedding_mode_id
|
||||
}
|
||||
|
||||
dialogVisible.value = true
|
||||
}
|
||||
const submitHandle = () => {
|
||||
emit('addData', checkList.value)
|
||||
dialogVisible.value = false
|
||||
}
|
||||
|
||||
const refresh = () => {
|
||||
emit('refresh')
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.addDataset-dialog {
|
||||
padding: 0;
|
||||
.el-dialog__header {
|
||||
padding: 24px 24px 8px 24px;
|
||||
}
|
||||
.el-dialog__body {
|
||||
padding: 8px !important;
|
||||
}
|
||||
.el-dialog__footer {
|
||||
padding: 8px 24px 24px 24px;
|
||||
}
|
||||
|
||||
.el-dialog__headerbtn {
|
||||
top: 9px;
|
||||
}
|
||||
.max-height {
|
||||
max-height: calc(100vh - 260px);
|
||||
padding: 0 16px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,178 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
:title="$t('views.application.copyApplication')"
|
||||
v-model="dialogVisible"
|
||||
width="650"
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
>
|
||||
<el-form
|
||||
ref="applicationFormRef"
|
||||
:model="applicationForm"
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
require-asterisk-position="right"
|
||||
>
|
||||
<el-form-item :label="$t('views.application.applicationForm.form.appName.label')" prop="name">
|
||||
<el-input
|
||||
v-model="applicationForm.name"
|
||||
maxlength="64"
|
||||
:placeholder="$t('views.application.applicationForm.form.appName.placeholder')"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('views.application.applicationForm.form.appDescription.label')">
|
||||
<el-input
|
||||
v-model="applicationForm.desc"
|
||||
type="textarea"
|
||||
:placeholder="$t('views.application.applicationForm.form.appDescription.placeholder')"
|
||||
:rows="3"
|
||||
maxlength="256"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click.prevent="dialogVisible = false" :loading="loading">
|
||||
{{ $t('common.cancel') }}
|
||||
</el-button>
|
||||
<el-button type="primary" @click="submitValid(applicationFormRef)" :loading="loading">
|
||||
{{ $t('common.copy') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, reactive } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import type { ApplicationFormType } from '@/api/type/application'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import applicationApi from '@/api/application'
|
||||
import { MsgSuccess, MsgAlert } from '@/utils/message'
|
||||
import { isWorkFlow } from '@/utils/application'
|
||||
import { t } from '@/locales'
|
||||
import useStore from '@/stores'
|
||||
import { ValidType, ValidCount } from '@/enums/common'
|
||||
const router = useRouter()
|
||||
const { common, user } = useStore()
|
||||
|
||||
// @ts-ignore
|
||||
const defaultPrompt = t('views.application.applicationForm.form.prompt.defaultPrompt', {
|
||||
data: '{data}',
|
||||
question: '{question}'
|
||||
})
|
||||
const applicationFormRef = ref()
|
||||
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref<boolean>(false)
|
||||
// @ts-ignore
|
||||
const applicationForm = ref<ApplicationFormType>({
|
||||
name: '',
|
||||
desc: '',
|
||||
model_id: '',
|
||||
dialogue_number: 0,
|
||||
prologue: t('views.application.applicationForm.form.defaultPrologue'),
|
||||
dataset_id_list: [],
|
||||
dataset_setting: {
|
||||
top_n: 3,
|
||||
similarity: 0.6,
|
||||
max_paragraph_char_number: 5000,
|
||||
search_mode: 'embedding',
|
||||
no_references_setting: {
|
||||
status: 'ai_questioning',
|
||||
value: '{question}'
|
||||
}
|
||||
},
|
||||
model_setting: {
|
||||
prompt: defaultPrompt
|
||||
},
|
||||
problem_optimization: false,
|
||||
type: 'SIMPLE'
|
||||
})
|
||||
|
||||
const rules = reactive<FormRules<ApplicationFormType>>({
|
||||
name: [
|
||||
{
|
||||
required: true,
|
||||
message: t('views.application.applicationForm.form.appName.placeholder'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
watch(dialogVisible, (bool) => {
|
||||
if (!bool) {
|
||||
applicationForm.value = {
|
||||
name: '',
|
||||
desc: '',
|
||||
model_id: '',
|
||||
dialogue_number: 0,
|
||||
prologue: t('views.application.applicationForm.form.defaultPrologue'),
|
||||
dataset_id_list: [],
|
||||
dataset_setting: {
|
||||
top_n: 3,
|
||||
similarity: 0.6,
|
||||
max_paragraph_char_number: 5000,
|
||||
search_mode: 'embedding',
|
||||
no_references_setting: {
|
||||
status: 'ai_questioning',
|
||||
value: '{question}'
|
||||
}
|
||||
},
|
||||
model_setting: {
|
||||
prompt: defaultPrompt
|
||||
},
|
||||
problem_optimization: false,
|
||||
type: 'SIMPLE'
|
||||
}
|
||||
applicationFormRef.value?.clearValidate()
|
||||
}
|
||||
})
|
||||
|
||||
const open = (data: any) => {
|
||||
const obj = cloneDeep(data)
|
||||
delete obj['id']
|
||||
obj['name'] = obj['name'] + ` ${t('views.application.applicationForm.title.copy')}`
|
||||
applicationForm.value = obj
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const submitValid = (formEl: FormInstance | undefined) => {
|
||||
if (user.isEnterprise()) {
|
||||
submitHandle(formEl)
|
||||
} else {
|
||||
common
|
||||
.asyncGetValid(ValidType.Application, ValidCount.Application, loading)
|
||||
.then(async (res: any) => {
|
||||
if (res?.data) {
|
||||
submitHandle(formEl)
|
||||
} else {
|
||||
MsgAlert(t('common.tip'), t('views.application.tip.professionalMessage'))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
const submitHandle = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
applicationApi.postApplication(applicationForm.value, loading).then((res) => {
|
||||
MsgSuccess(t('common.createSuccess'))
|
||||
if (isWorkFlow(applicationForm.value.type)) {
|
||||
router.push({ path: `/application/${res.data.id}/workflow` })
|
||||
} else {
|
||||
router.push({ path: `/application/${res.data.id}/${res.data.type}/setting` })
|
||||
}
|
||||
dialogVisible.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,271 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
:title="$t('views.application.createApplication')"
|
||||
v-model="dialogVisible"
|
||||
width="650"
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
>
|
||||
<el-form
|
||||
ref="applicationFormRef"
|
||||
:model="applicationForm"
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
require-asterisk-position="right"
|
||||
@submit.prevent
|
||||
>
|
||||
<el-form-item :label="$t('views.application.applicationForm.form.appName.label')" prop="name">
|
||||
<el-input
|
||||
v-model="applicationForm.name"
|
||||
maxlength="64"
|
||||
:placeholder="$t('views.application.applicationForm.form.appName.placeholder')"
|
||||
show-word-limit
|
||||
@blur="applicationForm.name = applicationForm.name?.trim()"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('views.application.applicationForm.form.appDescription.label')">
|
||||
<el-input
|
||||
v-model="applicationForm.desc"
|
||||
type="textarea"
|
||||
:placeholder="$t('views.application.applicationForm.form.appDescription.placeholder')"
|
||||
:rows="3"
|
||||
maxlength="256"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('views.application.applicationForm.form.appType.label')">
|
||||
<el-radio-group v-model="applicationForm.type" class="card__radio">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-card shadow="never" :class="applicationForm.type === 'SIMPLE' ? 'active' : ''">
|
||||
<el-radio value="SIMPLE" size="large">
|
||||
<p class="mb-4">{{ $t('views.application.simple') }}</p>
|
||||
<el-text type="info">{{
|
||||
$t('views.application.applicationForm.form.appType.simplePlaceholder')
|
||||
}}</el-text>
|
||||
</el-radio>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card shadow="never" :class="isWorkFlow(applicationForm.type) ? 'active' : ''">
|
||||
<el-radio value="WORK_FLOW" size="large">
|
||||
<p class="mb-4">{{ $t('views.application.workflow') }}</p>
|
||||
<el-text type="info">{{
|
||||
$t('views.application.applicationForm.form.appType.workflowPlaceholder')
|
||||
}}</el-text>
|
||||
</el-radio>
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
:label="$t('views.document.upload.template')"
|
||||
v-if="applicationForm.type === 'WORK_FLOW'"
|
||||
>
|
||||
<div class="w-full">
|
||||
<el-row :gutter="16">
|
||||
<el-col :span="12">
|
||||
<el-card
|
||||
class="radio-card cursor"
|
||||
shadow="never"
|
||||
@click="selectedType('blank')"
|
||||
:class="appTemplate === 'blank' ? 'active' : ''"
|
||||
>
|
||||
{{ $t('views.application.applicationForm.form.appTemplate.blankApp') }}
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-card
|
||||
class="radio-card cursor"
|
||||
shadow="never"
|
||||
:class="appTemplate === 'assistant' ? 'active' : ''"
|
||||
@click="selectedType('assistant')"
|
||||
>
|
||||
{{ $t('views.application.applicationForm.form.appTemplate.assistantApp') }}
|
||||
</el-card>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click.prevent="dialogVisible = false" :loading="loading">
|
||||
{{ $t('common.cancel') }}
|
||||
</el-button>
|
||||
<el-button type="primary" @click="submitHandle(applicationFormRef)" :loading="loading">
|
||||
{{ $t('common.create') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, reactive } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import type { ApplicationFormType } from '@/api/type/application'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import applicationApi from '@/api/application'
|
||||
import { MsgSuccess, MsgAlert } from '@/utils/message'
|
||||
import { isWorkFlow } from '@/utils/application'
|
||||
import { baseNodes } from '@/workflow/common/data'
|
||||
import { t } from '@/locales'
|
||||
const router = useRouter()
|
||||
const emit = defineEmits(['refresh'])
|
||||
|
||||
// @ts-ignore
|
||||
const defaultPrompt = t('views.application.applicationForm.form.prompt.defaultPrompt', {
|
||||
data: '{data}',
|
||||
question: '{question}'
|
||||
})
|
||||
|
||||
const optimizationPrompt =
|
||||
t('views.application.applicationForm.dialog.defaultPrompt1', {
|
||||
question: '{question}'
|
||||
}) +
|
||||
'<data></data>' +
|
||||
t('views.application.applicationForm.dialog.defaultPrompt2')
|
||||
|
||||
const workflowDefault = ref<any>({
|
||||
edges: [],
|
||||
nodes: baseNodes
|
||||
})
|
||||
const appTemplate = ref('blank')
|
||||
|
||||
const applicationFormRef = ref()
|
||||
|
||||
const loading = ref(false)
|
||||
const dialogVisible = ref<boolean>(false)
|
||||
|
||||
const applicationForm = ref<ApplicationFormType>({
|
||||
name: '',
|
||||
desc: '',
|
||||
model_id: '',
|
||||
dialogue_number: 1,
|
||||
prologue: t('views.application.applicationForm.form.defaultPrologue'),
|
||||
dataset_id_list: [],
|
||||
dataset_setting: {
|
||||
top_n: 3,
|
||||
similarity: 0.6,
|
||||
max_paragraph_char_number: 5000,
|
||||
search_mode: 'embedding',
|
||||
no_references_setting: {
|
||||
status: 'ai_questioning',
|
||||
value: '{question}'
|
||||
}
|
||||
},
|
||||
model_setting: {
|
||||
prompt: defaultPrompt,
|
||||
system: t('views.application.applicationForm.form.roleSettings.placeholder'),
|
||||
no_references_prompt: '{question}'
|
||||
},
|
||||
model_params_setting: {},
|
||||
problem_optimization: false,
|
||||
problem_optimization_prompt: optimizationPrompt,
|
||||
stt_model_id: '',
|
||||
tts_model_id: '',
|
||||
stt_model_enable: false,
|
||||
tts_model_enable: false,
|
||||
tts_type: 'BROWSER',
|
||||
type: 'SIMPLE'
|
||||
})
|
||||
|
||||
const rules = reactive<FormRules<ApplicationFormType>>({
|
||||
name: [
|
||||
{
|
||||
required: true,
|
||||
message: t('views.application.applicationForm.form.appName.placeholder'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
model_id: [
|
||||
{
|
||||
required: false,
|
||||
message: t('views.application.applicationForm.form.aiModel.placeholder'),
|
||||
trigger: 'change'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
watch(dialogVisible, (bool) => {
|
||||
if (!bool) {
|
||||
applicationForm.value = {
|
||||
name: '',
|
||||
desc: '',
|
||||
model_id: '',
|
||||
dialogue_number: 1,
|
||||
prologue: t('views.application.applicationForm.form.defaultPrologue'),
|
||||
dataset_id_list: [],
|
||||
dataset_setting: {
|
||||
top_n: 3,
|
||||
similarity: 0.6,
|
||||
max_paragraph_char_number: 5000,
|
||||
search_mode: 'embedding',
|
||||
no_references_setting: {
|
||||
status: 'ai_questioning',
|
||||
value: '{question}'
|
||||
}
|
||||
},
|
||||
model_setting: {
|
||||
prompt: defaultPrompt,
|
||||
system: t('views.application.applicationForm.form.roleSettings.placeholder'),
|
||||
no_references_prompt: '{question}'
|
||||
},
|
||||
model_params_setting: {},
|
||||
problem_optimization: false,
|
||||
problem_optimization_prompt: optimizationPrompt,
|
||||
stt_model_id: '',
|
||||
tts_model_id: '',
|
||||
stt_model_enable: false,
|
||||
tts_model_enable: false,
|
||||
tts_type: 'BROWSER',
|
||||
type: 'SIMPLE'
|
||||
}
|
||||
applicationFormRef.value?.clearValidate()
|
||||
}
|
||||
})
|
||||
|
||||
const open = () => {
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const submitHandle = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
if (isWorkFlow(applicationForm.value.type) && appTemplate.value === 'blank') {
|
||||
workflowDefault.value.nodes[0].properties.node_data.desc = applicationForm.value.desc
|
||||
workflowDefault.value.nodes[0].properties.node_data.name = applicationForm.value.name
|
||||
applicationForm.value['work_flow'] = workflowDefault.value
|
||||
}
|
||||
applicationApi.postApplication(applicationForm.value, loading).then((res) => {
|
||||
MsgSuccess(t('common.createSuccess'))
|
||||
emit('refresh')
|
||||
if (isWorkFlow(applicationForm.value.type)) {
|
||||
router.push({ path: `/application/${res.data.id}/workflow` })
|
||||
} else {
|
||||
router.push({ path: `/application/${res.data.id}/${res.data.type}/setting` })
|
||||
}
|
||||
dialogVisible.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function selectedType(type: string) {
|
||||
appTemplate.value = type
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.radio-card {
|
||||
line-height: 22px;
|
||||
&.active {
|
||||
border-color: var(--el-color-primary);
|
||||
color: var(--el-color-primary);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,91 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
align-center
|
||||
:title="$t('common.setting')"
|
||||
v-model="dialogVisible"
|
||||
style="width: 550px"
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
>
|
||||
<el-form
|
||||
label-position="top"
|
||||
ref="paramFormRef"
|
||||
:model="form"
|
||||
require-asterisk-position="right"
|
||||
>
|
||||
<el-form-item label="MCP" prop="mcp_enable">
|
||||
<el-switch v-model="form.mcp_enable" />
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="form.mcp_enable"
|
||||
:label="$t('views.applicationWorkflow.nodes.mcpNode.configLabel')"
|
||||
prop="mcp_servers"
|
||||
:rules="[{ required: true, message: $t('common.required') }]"
|
||||
>
|
||||
<el-input
|
||||
v-model="form.mcp_servers"
|
||||
:rows="6"
|
||||
type="textarea"
|
||||
:placeholder="mcpServerJson"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click.prevent="dialogVisible = false">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="submit()" :loading="loading">
|
||||
{{ $t('common.save') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from 'vue'
|
||||
|
||||
const emit = defineEmits(['refresh'])
|
||||
|
||||
const paramFormRef = ref()
|
||||
|
||||
const mcpServerJson = `{
|
||||
"math": {
|
||||
"url": "your_server",
|
||||
"transport": "sse"
|
||||
}
|
||||
}`
|
||||
|
||||
const form = ref<any>({
|
||||
mcp_servers: '',
|
||||
mcp_enable: false
|
||||
})
|
||||
|
||||
const dialogVisible = ref<boolean>(false)
|
||||
const loading = ref(false)
|
||||
watch(dialogVisible, (bool) => {
|
||||
if (!bool) {
|
||||
form.value = {
|
||||
mcp_servers: '',
|
||||
mcp_enable: false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const open = (data: any) => {
|
||||
form.value = { ...form.value, ...data }
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const submit = () => {
|
||||
paramFormRef.value.validate().then((valid: any) => {
|
||||
if (valid) {
|
||||
emit('refresh', form.value)
|
||||
dialogVisible.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,327 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
align-center
|
||||
:title="$t('common.paramSetting')"
|
||||
class="param-dialog"
|
||||
v-model="dialogVisible"
|
||||
style="width: 550px"
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
>
|
||||
<el-scrollbar max-height="550">
|
||||
<div class="p-16">
|
||||
<el-form label-position="top" ref="paramFormRef" :model="form" v-loading="loading">
|
||||
<el-form-item :label="$t('views.application.applicationForm.dialog.selectSearchMode')">
|
||||
<el-radio-group
|
||||
v-model="form.dataset_setting.search_mode"
|
||||
class="card__radio"
|
||||
@change="changeHandle"
|
||||
>
|
||||
<el-card
|
||||
shadow="never"
|
||||
class="mb-16"
|
||||
:class="form.search_mode === 'embedding' ? 'active' : ''"
|
||||
>
|
||||
<el-radio value="embedding" size="large">
|
||||
<p class="mb-4">
|
||||
{{ $t('views.application.applicationForm.dialog.vectorSearch') }}
|
||||
</p>
|
||||
<el-text type="info">{{
|
||||
$t('views.application.applicationForm.dialog.vectorSearchTooltip')
|
||||
}}</el-text>
|
||||
</el-radio>
|
||||
</el-card>
|
||||
<el-card
|
||||
shadow="never"
|
||||
class="mb-16"
|
||||
:class="form.dataset_setting.search_mode === 'keywords' ? 'active' : ''"
|
||||
>
|
||||
<el-radio value="keywords" size="large">
|
||||
<p class="mb-4">
|
||||
{{ $t('views.application.applicationForm.dialog.fullTextSearch') }}
|
||||
</p>
|
||||
<el-text type="info">{{
|
||||
$t('views.application.applicationForm.dialog.fullTextSearchTooltip')
|
||||
}}</el-text>
|
||||
</el-radio>
|
||||
</el-card>
|
||||
<el-card
|
||||
shadow="never"
|
||||
:class="form.dataset_setting.search_mode === 'blend' ? 'active' : ''"
|
||||
>
|
||||
<el-radio value="blend" size="large">
|
||||
<p class="mb-4">
|
||||
{{ $t('views.application.applicationForm.dialog.hybridSearch') }}
|
||||
</p>
|
||||
<el-text type="info">{{
|
||||
$t('views.application.applicationForm.dialog.hybridSearchTooltip')
|
||||
}}</el-text>
|
||||
</el-radio>
|
||||
</el-card>
|
||||
</el-radio-group>
|
||||
</el-form-item>
|
||||
<el-row :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="flex align-center">
|
||||
<span class="mr-4">{{
|
||||
$t('views.application.applicationForm.dialog.similarityThreshold')
|
||||
}}</span>
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
:content="$t('views.application.applicationForm.dialog.similarityTooltip')"
|
||||
placement="right"
|
||||
>
|
||||
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-input-number
|
||||
v-model="form.dataset_setting.similarity"
|
||||
:min="0"
|
||||
:max="form.dataset_setting.search_mode === 'blend' ? 2 : 1"
|
||||
:precision="3"
|
||||
:step="0.1"
|
||||
:value-on-clear="0"
|
||||
controls-position="right"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t('views.application.applicationForm.dialog.topReferences')">
|
||||
<el-input-number
|
||||
v-model="form.dataset_setting.top_n"
|
||||
:min="1"
|
||||
:max="10000"
|
||||
:value-on-clear="1"
|
||||
controls-position="right"
|
||||
class="w-full"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-form-item :label="$t('views.application.applicationForm.dialog.maxCharacters')">
|
||||
<el-slider
|
||||
v-model="form.dataset_setting.max_paragraph_char_number"
|
||||
show-input
|
||||
:show-input-controls="false"
|
||||
:min="500"
|
||||
:max="100000"
|
||||
class="custom-slider"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
v-if="!isWorkflowType"
|
||||
:label="$t('views.application.applicationForm.dialog.noReferencesAction')"
|
||||
>
|
||||
<el-form
|
||||
label-position="top"
|
||||
ref="noReferencesformRef"
|
||||
:model="noReferencesform"
|
||||
:rules="noReferencesRules"
|
||||
:hide-required-asterisk="true"
|
||||
class="w-full"
|
||||
>
|
||||
<el-radio-group
|
||||
v-model="form.dataset_setting.no_references_setting.status"
|
||||
class="radio-block-avatar"
|
||||
>
|
||||
<el-radio value="ai_questioning">
|
||||
<p>
|
||||
{{ $t('views.application.applicationForm.dialog.continueQuestioning') }}
|
||||
</p>
|
||||
</el-radio>
|
||||
|
||||
<el-radio value="designated_answer">
|
||||
<p>{{ $t('views.application.applicationForm.dialog.provideAnswer') }}</p>
|
||||
<el-form-item
|
||||
v-if="form.dataset_setting.no_references_setting.status === 'designated_answer'"
|
||||
prop="designated_answer"
|
||||
>
|
||||
<el-input
|
||||
v-model="noReferencesform.designated_answer"
|
||||
:rows="2"
|
||||
type="textarea"
|
||||
maxlength="2048"
|
||||
:placeholder="defaultValue['designated_answer']"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-form>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item @click.prevent v-if="!isWorkflowType">
|
||||
<template #label>
|
||||
<div class="flex align-center">
|
||||
<span class="mr-4">{{
|
||||
$t('views.application.applicationForm.form.problemOptimization.label')
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
<el-switch size="small" v-model="form.problem_optimization"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
v-if="form.problem_optimization"
|
||||
:label="$t('views.application.applicationForm.form.prompt.label')"
|
||||
>
|
||||
<el-input
|
||||
v-model="form.problem_optimization_prompt"
|
||||
:rows="6"
|
||||
type="textarea"
|
||||
maxlength="2048"
|
||||
:placeholder="defaultPrompt"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer p-16">
|
||||
<el-button @click.prevent="dialogVisible = false">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="submit(noReferencesformRef)" :loading="loading">
|
||||
{{ $t('common.save') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, reactive } from 'vue'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { isWorkFlow } from '@/utils/application'
|
||||
import { t } from '@/locales'
|
||||
const emit = defineEmits(['refresh'])
|
||||
|
||||
const paramFormRef = ref()
|
||||
const noReferencesformRef = ref()
|
||||
|
||||
const defaultValue = {
|
||||
ai_questioning: '{question}',
|
||||
// @ts-ignore
|
||||
designated_answer: t('views.application.applicationForm.dialog.designated_answer')
|
||||
}
|
||||
|
||||
const defaultPrompt =
|
||||
t('views.application.applicationForm.dialog.defaultPrompt1', {
|
||||
question: '{question}'
|
||||
}) +
|
||||
'<data></data>' +
|
||||
t('views.application.applicationForm.dialog.defaultPrompt2')
|
||||
|
||||
const form = ref<any>({
|
||||
dataset_setting: {
|
||||
search_mode: 'embedding',
|
||||
top_n: 3,
|
||||
similarity: 0.6,
|
||||
max_paragraph_char_number: 5000,
|
||||
no_references_setting: {
|
||||
status: 'ai_questioning',
|
||||
value: '{question}'
|
||||
}
|
||||
},
|
||||
problem_optimization: false,
|
||||
problem_optimization_prompt: defaultPrompt
|
||||
})
|
||||
|
||||
const noReferencesform = ref<any>({
|
||||
ai_questioning: defaultValue['ai_questioning'],
|
||||
designated_answer: defaultValue['designated_answer']
|
||||
})
|
||||
|
||||
const noReferencesRules = reactive<FormRules<any>>({
|
||||
ai_questioning: [
|
||||
{
|
||||
required: true,
|
||||
message: t('views.application.applicationForm.form.aiModel.placeholder'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
],
|
||||
designated_answer: [
|
||||
{
|
||||
required: true,
|
||||
message: t('views.application.applicationForm.form.prompt.requiredMessage'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const dialogVisible = ref<boolean>(false)
|
||||
const loading = ref(false)
|
||||
|
||||
const isWorkflowType = ref(false)
|
||||
|
||||
watch(dialogVisible, (bool) => {
|
||||
if (!bool) {
|
||||
// form.value = {
|
||||
// dataset_setting: {
|
||||
// search_mode: 'embedding',
|
||||
// top_n: 3,
|
||||
// similarity: 0.6,
|
||||
// max_paragraph_char_number: 5000,
|
||||
// no_references_setting: {
|
||||
// status: 'ai_questioning',
|
||||
// value: '{question}'
|
||||
// }
|
||||
// },
|
||||
// problem_optimization: false,
|
||||
// problem_optimization_prompt: ''
|
||||
// }
|
||||
noReferencesform.value = {
|
||||
ai_questioning: defaultValue['ai_questioning'],
|
||||
designated_answer: defaultValue['designated_answer']
|
||||
}
|
||||
noReferencesformRef.value?.clearValidate()
|
||||
}
|
||||
})
|
||||
|
||||
const open = (data: any, type?: string) => {
|
||||
isWorkflowType.value = isWorkFlow(type)
|
||||
form.value = {
|
||||
dataset_setting: { ...data.dataset_setting },
|
||||
problem_optimization: data.problem_optimization,
|
||||
problem_optimization_prompt: data.problem_optimization_prompt
|
||||
}
|
||||
if (!isWorkflowType.value) {
|
||||
noReferencesform.value[form.value.dataset_setting.no_references_setting.status] =
|
||||
form.value.dataset_setting.no_references_setting.value
|
||||
}
|
||||
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const submit = async (formEl: FormInstance | undefined) => {
|
||||
if (isWorkflowType.value) {
|
||||
delete form.value['no_references_setting']
|
||||
emit('refresh', form.value)
|
||||
dialogVisible.value = false
|
||||
} else {
|
||||
if (!formEl) return
|
||||
await formEl.validate((valid, fields) => {
|
||||
if (valid) {
|
||||
form.value.dataset_setting.no_references_setting.value =
|
||||
noReferencesform.value[form.value.dataset_setting.no_references_setting.status]
|
||||
emit('refresh', form.value)
|
||||
dialogVisible.value = false
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function changeHandle(val: string) {
|
||||
if (val === 'keywords') {
|
||||
form.value.dataset_setting.similarity = 0
|
||||
} else {
|
||||
form.value.dataset_setting.similarity = 0.6
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
align-center
|
||||
:title="$t('common.setting')"
|
||||
class="param-dialog"
|
||||
v-model="dialogVisible"
|
||||
style="width: 550px"
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
>
|
||||
<el-form label-position="top" ref="paramFormRef" :model="form" class="p-12-16">
|
||||
<el-text type="info" class="color-secondary">{{
|
||||
$t('views.application.applicationForm.form.reasoningContent.tooltip')
|
||||
}}</el-text>
|
||||
<el-row class="mt-16" :gutter="20">
|
||||
<el-col :span="12">
|
||||
<el-form-item
|
||||
:label="$t('views.application.applicationForm.form.reasoningContent.start')"
|
||||
>
|
||||
<el-input
|
||||
type="textarea"
|
||||
v-model="form.reasoning_content_start"
|
||||
:rows="6"
|
||||
maxlength="50"
|
||||
placeholder="<think>"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<el-form-item :label="$t('views.application.applicationForm.form.reasoningContent.end')">
|
||||
<el-input
|
||||
type="textarea"
|
||||
v-model="form.reasoning_content_end"
|
||||
:rows="6"
|
||||
maxlength="50"
|
||||
placeholder="</think>"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-form>
|
||||
|
||||
<template #footer>
|
||||
<span class="dialog-footer p-16">
|
||||
<el-button @click.prevent="dialogVisible = false">{{ $t('common.cancel') }}</el-button>
|
||||
<el-button type="primary" @click="submit()" :loading="loading">
|
||||
{{ $t('common.save') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, reactive } from 'vue'
|
||||
|
||||
const emit = defineEmits(['refresh'])
|
||||
|
||||
const form = ref<any>({
|
||||
reasoning_content_start: '<think>',
|
||||
reasoning_content_end: '</think>'
|
||||
})
|
||||
|
||||
const dialogVisible = ref<boolean>(false)
|
||||
const loading = ref(false)
|
||||
watch(dialogVisible, (bool) => {
|
||||
if (!bool) {
|
||||
form.value = {
|
||||
reasoning_content_start: '<think>',
|
||||
reasoning_content_end: '</think>'
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const open = (data: any) => {
|
||||
form.value = { ...form.value, ...data }
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const submit = () => {
|
||||
emit('refresh', form.value)
|
||||
dialogVisible.value = false
|
||||
}
|
||||
|
||||
defineExpose({ open })
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,153 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
align-center
|
||||
:title="$t('common.paramSetting')"
|
||||
v-model="dialogVisible"
|
||||
style="width: 550px"
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
>
|
||||
<DynamicsForm
|
||||
v-model="form_data"
|
||||
:model="form_data"
|
||||
label-position="top"
|
||||
require-asterisk-position="right"
|
||||
:render_data="model_form_field"
|
||||
ref="dynamicsFormRef"
|
||||
>
|
||||
</DynamicsForm>
|
||||
|
||||
<template #footer>
|
||||
<div class="flex-between">
|
||||
<el-button @click="testPlay" :loading="playLoading">
|
||||
<AppIcon iconName="app-video-play" class="mr-4"></AppIcon>
|
||||
{{ $t('views.application.applicationForm.form.voicePlay.listeningTest') }}
|
||||
</el-button>
|
||||
|
||||
<span class="dialog-footer">
|
||||
<el-button @click.prevent="dialogVisible = false">
|
||||
{{ $t('common.cancel') }}
|
||||
</el-button>
|
||||
<el-button type="primary" @click="submit" :loading="loading">
|
||||
{{ $t('common.confirm') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</el-dialog>
|
||||
<!-- 先渲染,不然不能播放 -->
|
||||
<audio ref="audioPlayer" controls hidden="hidden"></audio>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import type { FormField } from '@/components/dynamics-form/type'
|
||||
import modelAPi from '@/api/model'
|
||||
import applicationApi from '@/api/application'
|
||||
import DynamicsForm from '@/components/dynamics-form/index.vue'
|
||||
import { keys } from 'lodash'
|
||||
import { app } from '@/main'
|
||||
import { MsgError } from '@/utils/message'
|
||||
|
||||
const {
|
||||
params: { id }
|
||||
} = app.config.globalProperties.$route as any
|
||||
|
||||
const tts_model_id = ref('')
|
||||
const model_form_field = ref<Array<FormField>>([])
|
||||
const emit = defineEmits(['refresh'])
|
||||
const dynamicsFormRef = ref<InstanceType<typeof DynamicsForm>>()
|
||||
const form_data = ref<any>({})
|
||||
const dialogVisible = ref(false)
|
||||
const loading = ref(false)
|
||||
const playLoading = ref(false)
|
||||
const getApi = (model_id: string, application_id?: string) => {
|
||||
return application_id
|
||||
? applicationApi.getModelParamsForm(application_id, model_id, loading)
|
||||
: modelAPi.getModelParamsForm(model_id, loading)
|
||||
}
|
||||
const open = (model_id: string, application_id?: string, model_setting_data?: any) => {
|
||||
form_data.value = {}
|
||||
tts_model_id.value = model_id
|
||||
const api = getApi(model_id, application_id)
|
||||
api.then((ok) => {
|
||||
model_form_field.value = ok.data
|
||||
const resp = ok.data
|
||||
.map((item: any) => ({
|
||||
[item.field]: item.show_default_value !== false ? item.default_value : undefined
|
||||
}))
|
||||
.reduce((x, y) => ({ ...x, ...y }), {})
|
||||
// 删除不存在的字段
|
||||
if (model_setting_data) {
|
||||
Object.keys(model_setting_data).forEach((key) => {
|
||||
if (!(key in resp)) {
|
||||
delete model_setting_data[key]
|
||||
}
|
||||
})
|
||||
}
|
||||
model_setting_data = { ...resp, ...model_setting_data }
|
||||
// 渲染动态表单
|
||||
dynamicsFormRef.value?.render(model_form_field.value, model_setting_data)
|
||||
})
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const reset_default = (model_id: string, application_id?: string) => {
|
||||
const api = getApi(model_id, application_id)
|
||||
api.then((ok) => {
|
||||
model_form_field.value = ok.data
|
||||
const model_setting_data = ok.data
|
||||
.map((item) => ({
|
||||
[item.field]: item.show_default_value !== false ? item.default_value : undefined
|
||||
}))
|
||||
.reduce((x, y) => ({ ...x, ...y }), {})
|
||||
|
||||
emit('refresh', model_setting_data)
|
||||
})
|
||||
}
|
||||
|
||||
const submit = async () => {
|
||||
dynamicsFormRef.value?.validate().then(() => {
|
||||
emit('refresh', form_data.value)
|
||||
dialogVisible.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const audioPlayer = ref<HTMLAudioElement | null>(null)
|
||||
const testPlay = () => {
|
||||
const data = {
|
||||
...form_data.value,
|
||||
tts_model_id: tts_model_id.value
|
||||
}
|
||||
applicationApi
|
||||
.playDemoText(id as string, data, playLoading)
|
||||
.then(async (res: any) => {
|
||||
if (res.type === 'application/json') {
|
||||
const text = await res.text()
|
||||
MsgError(text)
|
||||
return
|
||||
}
|
||||
// 创建 Blob 对象
|
||||
const blob = new Blob([res], { type: 'audio/mp3' })
|
||||
|
||||
// 创建对象 URL
|
||||
const url = URL.createObjectURL(blob)
|
||||
|
||||
// 检查 audioPlayer 是否已经引用了 DOM 元素
|
||||
if (audioPlayer.value instanceof HTMLAudioElement) {
|
||||
audioPlayer.value.src = url
|
||||
audioPlayer.value.play() // 自动播放音频
|
||||
} else {
|
||||
console.error('audioPlayer.value is not an instance of HTMLAudioElement')
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err: ', err)
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({ open, reset_default })
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,444 @@
|
|||
<template>
|
||||
<div class="application-list-container p-24" style="padding-top: 16px">
|
||||
<div class="flex-between mb-16">
|
||||
<h4>{{ $t('views.application.title') }}</h4>
|
||||
<div class="flex-between">
|
||||
<el-select
|
||||
v-model="selectUserId"
|
||||
class="mr-12"
|
||||
@change="searchHandle"
|
||||
style="max-width: 240px; width: 150px"
|
||||
>
|
||||
<el-option
|
||||
v-for="item in userOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="searchValue"
|
||||
@change="searchHandle"
|
||||
:placeholder="$t('views.application.searchBar.placeholder')"
|
||||
prefix-icon="Search"
|
||||
class="w-240"
|
||||
style="min-width: 240px"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div v-loading.fullscreen.lock="paginationConfig.current_page === 1 && loading">
|
||||
<InfiniteScroll
|
||||
:size="applicationList.length"
|
||||
:total="paginationConfig.total"
|
||||
:page_size="paginationConfig.page_size"
|
||||
v-model:current_page="paginationConfig.current_page"
|
||||
@load="getList"
|
||||
:loading="loading"
|
||||
>
|
||||
<el-row :gutter="15">
|
||||
<el-col :xs="24" :sm="12" :md="8" :lg="6" :xl="6" class="mb-16">
|
||||
<el-card shadow="hover" class="application-card-add" style="--el-card-padding: 8px">
|
||||
<div class="card-add-button flex align-center cursor p-8" @click="openCreateDialog">
|
||||
<AppIcon iconName="app-add-application" class="mr-8"></AppIcon>
|
||||
{{ $t('views.application.createApplication') }}
|
||||
</div>
|
||||
<el-divider style="margin: 8px 0" />
|
||||
<el-upload
|
||||
ref="elUploadRef"
|
||||
:file-list="[]"
|
||||
action="#"
|
||||
multiple
|
||||
:auto-upload="false"
|
||||
:show-file-list="false"
|
||||
:limit="1"
|
||||
:on-change="(file: any, fileList: any) => importApplication(file)"
|
||||
class="card-add-button"
|
||||
>
|
||||
<div class="flex align-center cursor p-8">
|
||||
<AppIcon iconName="app-import" class="mr-8"></AppIcon>
|
||||
{{ $t('views.application.importApplication') }}
|
||||
</div>
|
||||
</el-upload>
|
||||
</el-card>
|
||||
</el-col>
|
||||
<el-col
|
||||
:xs="24"
|
||||
:sm="12"
|
||||
:md="8"
|
||||
:lg="6"
|
||||
:xl="6"
|
||||
v-for="(item, index) in applicationList"
|
||||
:key="index"
|
||||
class="mb-16"
|
||||
>
|
||||
<CardBox
|
||||
:title="item.name"
|
||||
:description="item.desc"
|
||||
class="application-card cursor"
|
||||
@click="router.push({ path: `/application/${item.id}/${item.type}/overview` })"
|
||||
>
|
||||
<template #icon>
|
||||
<AppAvatar
|
||||
v-if="isAppIcon(item?.icon)"
|
||||
shape="square"
|
||||
:size="32"
|
||||
style="background: none"
|
||||
class="mr-8"
|
||||
>
|
||||
<img :src="item?.icon" alt="" />
|
||||
</AppAvatar>
|
||||
<AppAvatar
|
||||
v-else-if="item?.name"
|
||||
:name="item?.name"
|
||||
pinyinColor
|
||||
shape="square"
|
||||
:size="32"
|
||||
class="mr-8"
|
||||
/>
|
||||
</template>
|
||||
<template #subTitle>
|
||||
<el-text class="color-secondary" size="small">
|
||||
<auto-tooltip :content="item.username">
|
||||
{{ $t('common.creator') }}: {{ item.username }}
|
||||
</auto-tooltip>
|
||||
</el-text>
|
||||
</template>
|
||||
<div class="status-tag">
|
||||
<el-tag type="warning" v-if="isWorkFlow(item.type)" style="height: 22px">
|
||||
{{ $t('views.application.workflow') }}
|
||||
</el-tag>
|
||||
<el-tag class="blue-tag" v-else style="height: 22px">
|
||||
{{ $t('views.application.simple') }}
|
||||
</el-tag>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<div class="footer-content">
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
:content="$t('views.application.setting.demo')"
|
||||
placement="top"
|
||||
>
|
||||
<el-button text @click.stop @click="getAccessToken(item.id)">
|
||||
<AppIcon iconName="app-view"></AppIcon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-divider direction="vertical" />
|
||||
<el-tooltip effect="dark" :content="$t('common.setting')" placement="top">
|
||||
<el-button text @click.stop="settingApplication(item)">
|
||||
<AppIcon iconName="Setting"></AppIcon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
<el-divider direction="vertical" />
|
||||
<span @click.stop>
|
||||
<el-dropdown trigger="click">
|
||||
<el-button text @click.stop>
|
||||
<el-icon><MoreFilled /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item
|
||||
v-if="is_show_copy_button(item)"
|
||||
@click="copyApplication(item)"
|
||||
>
|
||||
<AppIcon iconName="app-copy"></AppIcon>
|
||||
{{ $t('common.copy') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click.stop="exportApplication(item)">
|
||||
<AppIcon iconName="app-export"></AppIcon>
|
||||
|
||||
{{ $t('common.export') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item icon="Delete" @click.stop="deleteApplication(item)">{{
|
||||
$t('common.delete')
|
||||
}}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</CardBox>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</InfiniteScroll>
|
||||
</div>
|
||||
<CreateApplicationDialog ref="CreateApplicationDialogRef" />
|
||||
<CopyApplicationDialog ref="CopyApplicationDialogRef" />
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, reactive } from 'vue'
|
||||
import applicationApi from '@/api/application'
|
||||
import CreateApplicationDialog from './component/CreateApplicationDialog.vue'
|
||||
import CopyApplicationDialog from './component/CopyApplicationDialog.vue'
|
||||
import { MsgSuccess, MsgConfirm, MsgAlert, MsgError } from '@/utils/message'
|
||||
import { isAppIcon } from '@/utils/common'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { isWorkFlow } from '@/utils/application'
|
||||
import { ValidType, ValidCount } from '@/enums/common'
|
||||
import { t } from '@/locales'
|
||||
import useStore from '@/stores'
|
||||
|
||||
const elUploadRef = ref<any>()
|
||||
const { application, user, common } = useStore()
|
||||
const router = useRouter()
|
||||
|
||||
const CopyApplicationDialogRef = ref()
|
||||
const CreateApplicationDialogRef = ref()
|
||||
const loading = ref(false)
|
||||
|
||||
const applicationList = ref<any[]>([])
|
||||
|
||||
const paginationConfig = reactive({
|
||||
current_page: 1,
|
||||
page_size: 30,
|
||||
total: 0
|
||||
})
|
||||
|
||||
interface UserOption {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
const userOptions = ref<UserOption[]>([])
|
||||
|
||||
const selectUserId = ref('all')
|
||||
|
||||
const searchValue = ref('')
|
||||
|
||||
const apiInputParams = ref([])
|
||||
|
||||
function copyApplication(row: any) {
|
||||
application.asyncGetApplicationDetail(row.id, loading).then((res: any) => {
|
||||
if (res?.data) {
|
||||
CopyApplicationDialogRef.value.open({ ...res.data, model_id: res.data.model })
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const is_show_copy_button = (row: any) => {
|
||||
return user.userInfo ? user.userInfo.id == row.user_id : false
|
||||
}
|
||||
|
||||
function settingApplication(row: any) {
|
||||
if (isWorkFlow(row.type)) {
|
||||
router.push({ path: `/application/${row.id}/workflow` })
|
||||
} else {
|
||||
router.push({ path: `/application/${row.id}/${row.type}/setting` })
|
||||
}
|
||||
}
|
||||
|
||||
const exportApplication = (application: any) => {
|
||||
applicationApi.exportApplication(application.id, application.name, loading).catch((e) => {
|
||||
if (e.response.status !== 403) {
|
||||
e.response.data.text().then((res: string) => {
|
||||
MsgError(`${t('views.application.tip.ExportError')}:${JSON.parse(res).message}`)
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
const importApplication = (file: any) => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file.raw, file.name)
|
||||
elUploadRef.value.clearFiles()
|
||||
applicationApi
|
||||
.importApplication(formData, loading)
|
||||
.then(async (res: any) => {
|
||||
if (res?.data) {
|
||||
searchHandle()
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
if (e.code === 400) {
|
||||
MsgConfirm(t('common.tip'), t('views.application.tip.professionalMessage'), {
|
||||
cancelButtonText: t('common.confirm'),
|
||||
confirmButtonText: t('common.professional')
|
||||
}).then(() => {
|
||||
window.open('https://maxkb.cn/pricing.html', '_blank')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function openCreateDialog() {
|
||||
common
|
||||
.asyncGetValid(ValidType.Application, ValidCount.Application, loading)
|
||||
.then(async (res: any) => {
|
||||
if (res?.data) {
|
||||
CreateApplicationDialogRef.value.open()
|
||||
} else if (res?.code === 400) {
|
||||
MsgConfirm(t('common.tip'), t('views.application.tip.professionalMessage'), {
|
||||
cancelButtonText: t('common.confirm'),
|
||||
confirmButtonText: t('common.professional')
|
||||
}).then(() => {
|
||||
window.open('https://maxkb.cn/pricing.html', '_blank')
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function searchHandle() {
|
||||
if (user.userInfo) {
|
||||
localStorage.setItem(user.userInfo.id + 'application', selectUserId.value)
|
||||
}
|
||||
applicationList.value = []
|
||||
paginationConfig.current_page = 1
|
||||
paginationConfig.total = 0
|
||||
getList()
|
||||
}
|
||||
|
||||
function mapToUrlParams(map: any[]) {
|
||||
const params = new URLSearchParams()
|
||||
|
||||
map.forEach((item: any) => {
|
||||
params.append(encodeURIComponent(item.name), encodeURIComponent(item.value))
|
||||
})
|
||||
|
||||
return params.toString() // 返回 URL 查询字符串
|
||||
}
|
||||
|
||||
function getAccessToken(id: string) {
|
||||
applicationList.value
|
||||
.filter((app) => app.id === id)[0]
|
||||
?.work_flow?.nodes?.filter((v: any) => v.id === 'base-node')
|
||||
.map((v: any) => {
|
||||
apiInputParams.value = v.properties.api_input_field_list
|
||||
? v.properties.api_input_field_list.map((v: any) => {
|
||||
return {
|
||||
name: v.variable,
|
||||
value: v.default_value
|
||||
}
|
||||
})
|
||||
: v.properties.input_field_list
|
||||
? v.properties.input_field_list
|
||||
.filter((v: any) => v.assignment_method === 'api_input')
|
||||
.map((v: any) => {
|
||||
return {
|
||||
name: v.variable,
|
||||
value: v.default_value
|
||||
}
|
||||
})
|
||||
: []
|
||||
})
|
||||
|
||||
const apiParams = mapToUrlParams(apiInputParams.value)
|
||||
? '?' + mapToUrlParams(apiInputParams.value)
|
||||
: ''
|
||||
application.asyncGetAccessToken(id, loading).then((res: any) => {
|
||||
window.open(application.location + res?.data?.access_token + apiParams)
|
||||
})
|
||||
}
|
||||
|
||||
function deleteApplication(row: any) {
|
||||
MsgConfirm(
|
||||
// @ts-ignore
|
||||
`${t('views.application.delete.confirmTitle')}${row.name} ?`,
|
||||
t('views.application.delete.confirmMessage'),
|
||||
{
|
||||
confirmButtonText: t('common.confirm'),
|
||||
cancelButtonText: t('common.cancel'),
|
||||
confirmButtonClass: 'danger'
|
||||
}
|
||||
)
|
||||
.then(() => {
|
||||
applicationApi.delApplication(row.id, loading).then(() => {
|
||||
const index = applicationList.value.findIndex((v) => v.id === row.id)
|
||||
applicationList.value.splice(index, 1)
|
||||
MsgSuccess(t('common.deleteSuccess'))
|
||||
})
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
|
||||
function getList() {
|
||||
const params = {
|
||||
...(searchValue.value && { name: searchValue.value }),
|
||||
...(selectUserId.value &&
|
||||
selectUserId.value !== 'all' && { select_user_id: selectUserId.value })
|
||||
}
|
||||
applicationApi.getApplication(paginationConfig, params, loading).then((res) => {
|
||||
res.data.records.forEach((item: any) => {
|
||||
if (user.userInfo && item.user_id === user.userInfo.id) {
|
||||
item.username = user.userInfo.username
|
||||
} else {
|
||||
item.username = userOptions.value.find((v) => v.value === item.user_id)?.label
|
||||
}
|
||||
})
|
||||
applicationList.value = [...applicationList.value, ...res.data.records]
|
||||
paginationConfig.total = res.data.total
|
||||
})
|
||||
}
|
||||
|
||||
function getUserList() {
|
||||
applicationApi.getUserList('APPLICATION', loading).then((res) => {
|
||||
if (res.data) {
|
||||
userOptions.value = res.data.map((item: any) => {
|
||||
return {
|
||||
label: item.username,
|
||||
value: item.id
|
||||
}
|
||||
})
|
||||
if (user.userInfo) {
|
||||
const selectUserIdValue = localStorage.getItem(user.userInfo.id + 'application')
|
||||
if (selectUserIdValue && userOptions.value.find((v) => v.value === selectUserIdValue)) {
|
||||
selectUserId.value = selectUserIdValue
|
||||
}
|
||||
}
|
||||
getList()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getUserList()
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.application-card-add {
|
||||
width: 100%;
|
||||
font-size: 14px;
|
||||
min-height: var(--card-min-height);
|
||||
border: 1px dashed var(--el-border-color);
|
||||
background: var(--el-disabled-bg-color);
|
||||
border-radius: 8px;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:hover {
|
||||
border: 1px solid var(--el-card-bg-color);
|
||||
background-color: var(--el-card-bg-color);
|
||||
}
|
||||
|
||||
.card-add-button {
|
||||
&:hover {
|
||||
border-radius: 4px;
|
||||
background: var(--app-text-color-light-1);
|
||||
}
|
||||
|
||||
:deep(.el-upload) {
|
||||
display: block;
|
||||
width: 100%;
|
||||
color: var(--el-text-color-regular);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.application-card {
|
||||
.status-tag {
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 15px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-custom-switch {
|
||||
padding: 5px 11px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
|
||||
span {
|
||||
margin-right: 26px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,84 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
:modelValue="show"
|
||||
modal-class="positioned-mask"
|
||||
width="300"
|
||||
:title="$t('chat.passwordValidator.title')"
|
||||
custom-class="no-close-button"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
:show-close="false"
|
||||
top="25vh"
|
||||
center
|
||||
:modal="true"
|
||||
>
|
||||
<el-form ref="FormRef" :model="form" @submit.prevent="validator">
|
||||
<el-form-item prop="value" :rules="rules.value">
|
||||
<el-input show-password v-model="form.value" />
|
||||
</el-form-item>
|
||||
<el-button class="w-full mt-8" type="primary" @click="validator" :loading="loading">
|
||||
{{ $t('common.confirm') }}</el-button
|
||||
>
|
||||
</el-form>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import useStore from '@/stores'
|
||||
import { t } from '@/locales'
|
||||
const route = useRoute()
|
||||
const FormRef = ref()
|
||||
const {
|
||||
params: { accessToken }
|
||||
} = route as any
|
||||
const { application } = useStore()
|
||||
const props = defineProps<{ applicationProfile: any; modelValue: boolean }>()
|
||||
const loading = ref<boolean>(false)
|
||||
const show = computed(() => {
|
||||
if (props.applicationProfile) {
|
||||
if (props.modelValue) {
|
||||
return false
|
||||
}
|
||||
return props.applicationProfile.authentication
|
||||
}
|
||||
return false
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const auth = () => {
|
||||
return application.asyncAppAuthentication(accessToken, loading, form.value).then(() => {
|
||||
emit('update:modelValue', true)
|
||||
})
|
||||
}
|
||||
const validator_auth = (rule: any, value: string, callback: any) => {
|
||||
if (value === '') {
|
||||
callback(new Error(t('chat.passwordValidator.errorMessage1')))
|
||||
} else {
|
||||
auth().catch(() => {
|
||||
callback(new Error(t('chat.passwordValidator.errorMessage2')))
|
||||
})
|
||||
}
|
||||
}
|
||||
const validator = () => {
|
||||
FormRef.value.validate()
|
||||
}
|
||||
|
||||
const rules = {
|
||||
value: [{ required: true, validator: validator_auth, trigger: 'manual' }]
|
||||
}
|
||||
|
||||
const form = ref({
|
||||
type: 'password',
|
||||
value: ''
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.positioned-mask {
|
||||
top: var(--app-header-height);
|
||||
height: calc(100% - var(--app-header-height));
|
||||
.el-overlay-dialog {
|
||||
top: var(--app-header-height);
|
||||
height: calc(100% - var(--app-header-height));
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
<template>
|
||||
<div class="chat-pc__header" :style="customStyle">
|
||||
<div class="flex align-center">
|
||||
<div class="mr-12 ml-24 flex">
|
||||
<AppAvatar
|
||||
v-if="isAppIcon(application_profile?.icon)"
|
||||
shape="square"
|
||||
:size="32"
|
||||
style="background: none"
|
||||
>
|
||||
<img :src="application_profile?.icon" alt="" />
|
||||
</AppAvatar>
|
||||
<AppAvatar
|
||||
v-else-if="application_profile?.name"
|
||||
:name="application_profile?.name"
|
||||
pinyinColor
|
||||
shape="square"
|
||||
:size="32"
|
||||
/>
|
||||
</div>
|
||||
<h4>{{ application_profile?.name }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<component
|
||||
:is="auth_components[`/src/views/chat/auth/component/${auth_type}.vue`].default"
|
||||
v-model="is_auth"
|
||||
:applicationProfile="application_profile"
|
||||
/>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { isAppIcon } from '@/utils/common'
|
||||
|
||||
const auth_components: any = import.meta.glob('@/views/chat/auth/component/*.vue', {
|
||||
eager: true
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{ modelValue: boolean; application_profile: any; auth_type?: string; style?: any }>(),
|
||||
{
|
||||
auth_type: 'password',
|
||||
style: {}
|
||||
}
|
||||
)
|
||||
const is_auth = computed({
|
||||
get: () => {
|
||||
return props.modelValue
|
||||
},
|
||||
set: (v) => {
|
||||
emit('update:modelValue', v)
|
||||
}
|
||||
})
|
||||
|
||||
const customStyle = computed(() => {
|
||||
return {
|
||||
background: props.application_profile?.custom_theme?.theme_color,
|
||||
color: props.application_profile?.custom_theme?.header_font_color,
|
||||
border: 'none',
|
||||
...props.style
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
<template>
|
||||
<div class="chat layout-bg" v-loading="loading">
|
||||
<div class="chat__header" :class="!isDefaultTheme ? 'custom-header' : ''">
|
||||
<div class="chat-width flex align-center">
|
||||
<div class="mr-12 ml-24 flex">
|
||||
<AppAvatar
|
||||
v-if="isAppIcon(applicationDetail?.icon)"
|
||||
shape="square"
|
||||
:size="32"
|
||||
style="background: none"
|
||||
>
|
||||
<img :src="applicationDetail?.icon" alt="" />
|
||||
</AppAvatar>
|
||||
<AppAvatar
|
||||
v-else-if="applicationDetail?.name"
|
||||
:name="applicationDetail?.name"
|
||||
pinyinColor
|
||||
shape="square"
|
||||
:size="32"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h2>{{ applicationDetail?.name }}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat__main chat-width">
|
||||
<AiChat
|
||||
v-model:applicationDetails="applicationDetail"
|
||||
type="ai-chat"
|
||||
:available="applicationAvailable"
|
||||
:appId="applicationDetail?.id"
|
||||
:record="recordList"
|
||||
:chatId="currentChatId"
|
||||
@refresh="refresh"
|
||||
>
|
||||
<template #operateBefore>
|
||||
<div>
|
||||
<el-button type="primary" link class="new-chat-button mb-8" @click="newChat">
|
||||
<el-icon><Plus /></el-icon><span class="ml-4">{{ $t('chat.createChat') }}</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</AiChat>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { isAppIcon } from '@/utils/common'
|
||||
import useStore from '@/stores'
|
||||
|
||||
const { user } = useStore()
|
||||
|
||||
const isDefaultTheme = computed(() => {
|
||||
return user.isDefaultTheme()
|
||||
})
|
||||
|
||||
const loading = ref(false)
|
||||
const props = defineProps<{
|
||||
application_profile: any
|
||||
applicationAvailable: boolean
|
||||
}>()
|
||||
const applicationDetail = computed({
|
||||
get: () => {
|
||||
return props.application_profile
|
||||
},
|
||||
set: (v) => {}
|
||||
})
|
||||
const recordList = ref([])
|
||||
const currentChatId = ref('')
|
||||
|
||||
function newChat() {
|
||||
currentChatId.value = 'new'
|
||||
recordList.value = []
|
||||
}
|
||||
function refresh(id: string) {
|
||||
currentChatId.value = id
|
||||
}
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.chat {
|
||||
overflow: hidden;
|
||||
&__header {
|
||||
background: var(--app-header-bg-color);
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
height: var(--app-header-height);
|
||||
line-height: var(--app-header-height);
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
}
|
||||
&__main {
|
||||
padding-top: calc(var(--app-header-height) + 24px);
|
||||
height: calc(100vh - var(--app-header-height) - 24px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-width {
|
||||
// max-width: 80%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,404 @@
|
|||
<template>
|
||||
<div
|
||||
class="chat-embed layout-bg"
|
||||
:class="{ 'chat-embed--popup': isPopup }"
|
||||
v-loading="loading"
|
||||
:style="{
|
||||
'--el-color-primary': applicationDetail?.custom_theme?.theme_color,
|
||||
'--el-color-primary-light-9': hexToRgba(applicationDetail?.custom_theme?.theme_color, 0.1)
|
||||
}"
|
||||
>
|
||||
<div class="chat-embed__header" :style="customStyle">
|
||||
<div class="flex align-center">
|
||||
<div class="mr-12 ml-24 flex">
|
||||
<AppAvatar
|
||||
v-if="isAppIcon(applicationDetail?.icon)"
|
||||
shape="square"
|
||||
:size="32"
|
||||
style="background: none"
|
||||
>
|
||||
<img :src="applicationDetail?.icon" alt="" />
|
||||
</AppAvatar>
|
||||
<AppAvatar
|
||||
v-else-if="applicationDetail?.name"
|
||||
:name="applicationDetail?.name"
|
||||
pinyinColor
|
||||
shape="square"
|
||||
:size="32"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h4>{{ applicationDetail?.name }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="chat-embed__main">
|
||||
<AiChat
|
||||
ref="AiChatRef"
|
||||
v-model:applicationDetails="applicationDetail"
|
||||
:available="applicationAvailable"
|
||||
:appId="applicationDetail?.id"
|
||||
:record="currentRecordList"
|
||||
:chatId="currentChatId"
|
||||
type="ai-chat"
|
||||
@refresh="refresh"
|
||||
@scroll="handleScroll"
|
||||
class="AiChat-embed"
|
||||
>
|
||||
<template #operateBefore>
|
||||
<div>
|
||||
<el-button type="primary" link class="new-chat-button mb-8" @click="newChat">
|
||||
<el-icon><Plus /></el-icon><span class="ml-4">{{ $t('chat.createChat') }}</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</AiChat>
|
||||
</div>
|
||||
|
||||
<!-- 历史记录弹出层 -->
|
||||
<div
|
||||
v-if="applicationDetail.show_history || !user.isEnterprise()"
|
||||
@click.prevent.stop="show = !show"
|
||||
class="chat-popover-button cursor color-secondary"
|
||||
>
|
||||
<AppIcon
|
||||
iconName="app-history-outlined"
|
||||
:style="{
|
||||
color: applicationDetail?.custom_theme?.header_font_color
|
||||
}"
|
||||
></AppIcon>
|
||||
</div>
|
||||
|
||||
<el-collapse-transition>
|
||||
<div v-show="show" class="chat-popover w-full" v-click-outside="clickoutside">
|
||||
<div class="border-b p-16-24">
|
||||
<span>{{ $t('chat.history') }}</span>
|
||||
</div>
|
||||
|
||||
<el-scrollbar max-height="300">
|
||||
<div class="p-8">
|
||||
<common-list
|
||||
:style="{ '--el-color-primary': applicationDetail?.custom_theme?.theme_color }"
|
||||
:data="chatLogData"
|
||||
v-loading="left_loading"
|
||||
:defaultActive="currentChatId"
|
||||
@click="clickListHandle"
|
||||
@mouseenter="mouseenter"
|
||||
@mouseleave="mouseId = ''"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="flex-between">
|
||||
<ReadWrite
|
||||
@change="editName($event, row)"
|
||||
:data="row.abstract"
|
||||
trigger="manual"
|
||||
:write="row.writeStatus"
|
||||
@close="closeWrite(row)"
|
||||
:maxlength="1024"
|
||||
/>
|
||||
<div
|
||||
@click.stop
|
||||
v-if="mouseId === row.id && row.id !== 'new' && !row.writeStatus"
|
||||
class="flex"
|
||||
>
|
||||
<el-button style="padding: 0" link @click.stop="openWrite(row)">
|
||||
<el-icon><EditPen /></el-icon>
|
||||
</el-button>
|
||||
<el-button style="padding: 0" link @click.stop="deleteLog(row)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #empty>
|
||||
<div class="text-center mt-24">
|
||||
<el-text type="info">{{ $t('chat.noHistory') }}</el-text>
|
||||
</div>
|
||||
</template>
|
||||
</common-list>
|
||||
</div>
|
||||
<div v-if="chatLogData.length" class="gradient-divider lighter mt-8">
|
||||
<span>{{ $t('chat.only20history') }}</span>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</el-collapse-transition>
|
||||
<div class="chat-popover-mask" v-show="show"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, reactive, nextTick, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { isAppIcon } from '@/utils/common'
|
||||
import { hexToRgba } from '@/utils/theme'
|
||||
import { MsgError } from '@/utils/message'
|
||||
import useStore from '@/stores'
|
||||
import { t } from '@/locales'
|
||||
const { user, log } = useStore()
|
||||
const route = useRoute()
|
||||
|
||||
const isPopup = computed(() => {
|
||||
return route.query.popup !== 'no'
|
||||
})
|
||||
const AiChatRef = ref()
|
||||
const loading = ref(false)
|
||||
const left_loading = ref(false)
|
||||
const chatLogData = ref<any[]>([])
|
||||
const show = ref(false)
|
||||
const props = defineProps<{
|
||||
application_profile: any
|
||||
applicationAvailable: boolean
|
||||
}>()
|
||||
const applicationDetail = computed({
|
||||
get: () => {
|
||||
return props.application_profile
|
||||
},
|
||||
set: (v) => {}
|
||||
})
|
||||
const paginationConfig = reactive({
|
||||
current_page: 1,
|
||||
page_size: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const currentRecordList = ref<any>([])
|
||||
const currentChatId = ref('new') // 当前历史记录Id 默认为'new'
|
||||
|
||||
const mouseId = ref('')
|
||||
|
||||
const customStyle = computed(() => {
|
||||
return {
|
||||
background: applicationDetail.value?.custom_theme?.theme_color,
|
||||
color: applicationDetail.value?.custom_theme?.header_font_color
|
||||
}
|
||||
})
|
||||
|
||||
function editName(val: string, item: any) {
|
||||
if (val) {
|
||||
const obj = {
|
||||
abstract: val
|
||||
}
|
||||
log.asyncPutChatClientLog(applicationDetail.value.id, item.id, obj, loading).then(() => {
|
||||
const find = chatLogData.value.find((row: any) => row.id === item.id)
|
||||
if (find) {
|
||||
find.abstract = val
|
||||
}
|
||||
item['writeStatus'] = false
|
||||
})
|
||||
} else {
|
||||
MsgError(t('views.applicationWorkflow.tip.nameMessage'))
|
||||
}
|
||||
}
|
||||
|
||||
function openWrite(item: any) {
|
||||
item['writeStatus'] = true
|
||||
}
|
||||
|
||||
function closeWrite(item: any) {
|
||||
item['writeStatus'] = false
|
||||
}
|
||||
|
||||
function mouseenter(row: any) {
|
||||
mouseId.value = row.id
|
||||
}
|
||||
function deleteLog(row: any) {
|
||||
log.asyncDelChatClientLog(applicationDetail.value.id, row.id, left_loading).then(() => {
|
||||
if (currentChatId.value === row.id) {
|
||||
currentChatId.value = 'new'
|
||||
paginationConfig.current_page = 1
|
||||
paginationConfig.total = 0
|
||||
currentRecordList.value = []
|
||||
}
|
||||
getChatLog(applicationDetail.value.id)
|
||||
})
|
||||
}
|
||||
|
||||
function handleScroll(event: any) {
|
||||
if (
|
||||
currentChatId.value !== 'new' &&
|
||||
event.scrollTop === 0 &&
|
||||
paginationConfig.total > currentRecordList.value.length
|
||||
) {
|
||||
const history_height = event.dialogScrollbar.offsetHeight
|
||||
paginationConfig.current_page += 1
|
||||
getChatRecord().then(() => {
|
||||
event.scrollDiv.setScrollTop(event.dialogScrollbar.offsetHeight - history_height)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function clickoutside() {
|
||||
show.value = false
|
||||
}
|
||||
|
||||
function newChat() {
|
||||
paginationConfig.current_page = 1
|
||||
currentRecordList.value = []
|
||||
currentChatId.value = 'new'
|
||||
}
|
||||
|
||||
function getChatLog(id: string) {
|
||||
const page = {
|
||||
current_page: 1,
|
||||
page_size: 20
|
||||
}
|
||||
|
||||
log.asyncGetChatLogClient(id, page, left_loading).then((res: any) => {
|
||||
chatLogData.value = res.data.records
|
||||
paginationConfig.current_page = 1
|
||||
paginationConfig.total = 0
|
||||
currentRecordList.value = []
|
||||
currentChatId.value = chatLogData.value?.[0]?.id || 'new'
|
||||
if (currentChatId.value !== 'new') {
|
||||
getChatRecord()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getChatRecord() {
|
||||
return log
|
||||
.asyncChatRecordLog(
|
||||
applicationDetail.value.id,
|
||||
currentChatId.value,
|
||||
paginationConfig,
|
||||
loading,
|
||||
false
|
||||
)
|
||||
.then((res: any) => {
|
||||
paginationConfig.total = res.data.total
|
||||
const list = res.data.records
|
||||
list.map((v: any) => {
|
||||
v['write_ed'] = true
|
||||
v['record_id'] = v.id
|
||||
})
|
||||
currentRecordList.value = [...list, ...currentRecordList.value].sort((a, b) =>
|
||||
a.create_time.localeCompare(b.create_time)
|
||||
)
|
||||
if (paginationConfig.current_page === 1) {
|
||||
nextTick(() => {
|
||||
// 将滚动条滚动到最下面
|
||||
AiChatRef.value.setScrollBottom()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const clickListHandle = (item: any) => {
|
||||
if (item.id !== currentChatId.value) {
|
||||
paginationConfig.current_page = 1
|
||||
currentRecordList.value = []
|
||||
currentChatId.value = item.id
|
||||
if (currentChatId.value !== 'new') {
|
||||
getChatRecord()
|
||||
}
|
||||
show.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function refresh(id: string) {
|
||||
getChatLog(applicationDetail.value.id)
|
||||
currentChatId.value = id
|
||||
}
|
||||
/**
|
||||
*初始化历史对话记录
|
||||
*/
|
||||
const init = () => {
|
||||
if (applicationDetail.value.show_history || !user.isEnterprise()) {
|
||||
getChatLog(applicationDetail.value.id)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
init()
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.chat-embed {
|
||||
overflow: hidden;
|
||||
&__header {
|
||||
background: var(--app-header-bg-color);
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
height: var(--app-header-height);
|
||||
line-height: var(--app-header-height);
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
}
|
||||
&__main {
|
||||
padding-top: calc(var(--app-header-height) + 16px);
|
||||
height: calc(100vh - var(--app-header-height) - 16px);
|
||||
overflow: hidden;
|
||||
}
|
||||
.new-chat-button {
|
||||
z-index: 11;
|
||||
}
|
||||
// 历史对话弹出层
|
||||
.chat-popover {
|
||||
position: absolute;
|
||||
top: var(--app-header-height);
|
||||
background: #ffffff;
|
||||
padding-bottom: 24px;
|
||||
z-index: 2009;
|
||||
}
|
||||
.chat-popover-button {
|
||||
z-index: 2009;
|
||||
position: absolute;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
font-size: 22px;
|
||||
}
|
||||
&.chat-embed--popup {
|
||||
.chat-popover-button {
|
||||
right: 85px;
|
||||
}
|
||||
}
|
||||
.chat-popover-mask {
|
||||
background-color: var(--el-overlay-color-lighter);
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
overflow: auto;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: var(--app-header-height);
|
||||
z-index: 2008;
|
||||
}
|
||||
.gradient-divider {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
color: var(--el-color-info);
|
||||
::before {
|
||||
content: '';
|
||||
width: 17%;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, rgba(222, 224, 227, 0) 0%, #dee0e3 100%);
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 50%;
|
||||
}
|
||||
::after {
|
||||
content: '';
|
||||
width: 17%;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, #dee0e3 0%, rgba(222, 224, 227, 0) 100%);
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 50%;
|
||||
}
|
||||
}
|
||||
.AiChat-embed {
|
||||
.ai-chat__operate {
|
||||
padding-top: 12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-overlay) {
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
<template>
|
||||
<component
|
||||
v-if="chat_show && init_data_end"
|
||||
:applicationAvailable="applicationAvailable"
|
||||
:is="currentTemplate"
|
||||
:application_profile="application_profile"
|
||||
:key="route.fullPath"
|
||||
v-loading="loading"
|
||||
/>
|
||||
<Auth
|
||||
v-else
|
||||
:application_profile="application_profile"
|
||||
:auth_type="application_profile.authentication_type"
|
||||
v-model="is_auth"
|
||||
:style="{
|
||||
'--el-color-primary': application_profile?.custom_theme?.theme_color,
|
||||
'--el-color-primary-light-9': hexToRgba(application_profile?.custom_theme?.theme_color, 0.1)
|
||||
}"
|
||||
></Auth>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, onBeforeMount, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import useStore from '@/stores'
|
||||
import Auth from '@/views/chat/auth/index.vue'
|
||||
import { hexToRgba } from '@/utils/theme'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { getBrowserLang } from '@/locales/index'
|
||||
const { locale } = useI18n({ useScope: 'global' })
|
||||
const route = useRoute()
|
||||
const { application, user } = useStore()
|
||||
|
||||
const components: any = import.meta.glob('@/views/chat/**/index.vue', {
|
||||
eager: true
|
||||
})
|
||||
|
||||
const {
|
||||
query: { mode },
|
||||
params: { accessToken }
|
||||
} = route as any
|
||||
const is_auth = ref<boolean>(false)
|
||||
const currentTemplate = computed(() => {
|
||||
let modeName = ''
|
||||
if (!mode || mode === 'pc') {
|
||||
modeName = show_history.value || !user.isEnterprise() ? 'pc' : 'base'
|
||||
} else {
|
||||
modeName = mode
|
||||
}
|
||||
const name = `/src/views/chat/${modeName}/index.vue`
|
||||
return components[name].default
|
||||
})
|
||||
/**
|
||||
* 是否显示对话
|
||||
*/
|
||||
const chat_show = computed(() => {
|
||||
if (init_data_end.value) {
|
||||
if (!applicationAvailable.value) {
|
||||
return true
|
||||
}
|
||||
if (application_profile.value) {
|
||||
if (application_profile.value.authentication && is_auth.value) {
|
||||
return true
|
||||
} else if (!application_profile.value.authentication) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
const loading = ref(false)
|
||||
|
||||
const show_history = ref(false)
|
||||
|
||||
const application_profile = ref<any>({})
|
||||
/**
|
||||
|
||||
* 初始化结束
|
||||
*/
|
||||
const init_data_end = ref<boolean>(false)
|
||||
|
||||
const applicationAvailable = ref<boolean>(true)
|
||||
function getAppProfile() {
|
||||
return application.asyncGetAppProfile(loading).then((res: any) => {
|
||||
locale.value = res.data?.language || getBrowserLang()
|
||||
show_history.value = res.data?.show_history
|
||||
application_profile.value = res.data
|
||||
})
|
||||
}
|
||||
function getAccessToken(token: string) {
|
||||
return application.asyncAppAuthentication(token, loading).then(() => {
|
||||
return getAppProfile()
|
||||
})
|
||||
}
|
||||
onBeforeMount(() => {
|
||||
user.changeUserType(2, accessToken)
|
||||
Promise.all([user.asyncGetProfile(), getAccessToken(accessToken)])
|
||||
.catch(() => {
|
||||
applicationAvailable.value = false
|
||||
})
|
||||
.finally(() => (init_data_end.value = true))
|
||||
})
|
||||
</script>
|
||||
<style lang="scss"></style>
|
||||
|
|
@ -0,0 +1,401 @@
|
|||
<template>
|
||||
<div
|
||||
class="chat-mobile layout-bg"
|
||||
v-loading="loading"
|
||||
:style="{
|
||||
'--el-color-primary': applicationDetail?.custom_theme?.theme_color,
|
||||
'--el-color-primary-light-9': hexToRgba(applicationDetail?.custom_theme?.theme_color, 0.1)
|
||||
}"
|
||||
>
|
||||
<div class="chat-embed__header" :style="customStyle">
|
||||
<div class="flex align-center">
|
||||
<div class="mr-12 ml-24 flex">
|
||||
<AppAvatar
|
||||
v-if="isAppIcon(applicationDetail?.icon)"
|
||||
shape="square"
|
||||
:size="32"
|
||||
style="background: none"
|
||||
>
|
||||
<img :src="applicationDetail?.icon" alt="" />
|
||||
</AppAvatar>
|
||||
<AppAvatar
|
||||
v-else-if="applicationDetail?.name"
|
||||
:name="applicationDetail?.name"
|
||||
pinyinColor
|
||||
shape="square"
|
||||
:size="32"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<h4>{{ applicationDetail?.name }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="chat-embed__main">
|
||||
<AiChat
|
||||
ref="AiChatRef"
|
||||
v-model:applicationDetails="applicationDetail"
|
||||
:available="applicationAvailable"
|
||||
:appId="applicationDetail?.id"
|
||||
:record="currentRecordList"
|
||||
:chatId="currentChatId"
|
||||
type="ai-chat"
|
||||
@refresh="refresh"
|
||||
@scroll="handleScroll"
|
||||
class="AiChat-embed"
|
||||
>
|
||||
<template #operateBefore>
|
||||
<div>
|
||||
<el-button type="primary" link class="new-chat-button mb-8" @click="newChat">
|
||||
<el-icon><Plus /></el-icon><span class="ml-4">{{ $t('chat.createChat') }}</span>
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
</AiChat>
|
||||
</div>
|
||||
|
||||
<!-- 历史记录弹出层 -->
|
||||
<div
|
||||
v-if="applicationDetail.show_history || !user.isEnterprise()"
|
||||
@click.prevent.stop="show = !show"
|
||||
class="chat-popover-button cursor color-secondary"
|
||||
>
|
||||
<AppIcon
|
||||
iconName="app-history-outlined"
|
||||
:style="{
|
||||
color: applicationDetail?.custom_theme?.header_font_color
|
||||
}"
|
||||
></AppIcon>
|
||||
</div>
|
||||
|
||||
<el-collapse-transition>
|
||||
<div v-show="show" class="chat-popover w-full" v-click-outside="clickoutside">
|
||||
<div class="border-b p-16-24">
|
||||
<span>{{ $t('chat.history') }}</span>
|
||||
</div>
|
||||
|
||||
<el-scrollbar max-height="300">
|
||||
<div class="p-8">
|
||||
<common-list
|
||||
:style="{ '--el-color-primary': applicationDetail?.custom_theme?.theme_color }"
|
||||
:data="chatLogData"
|
||||
v-loading="left_loading"
|
||||
:defaultActive="currentChatId"
|
||||
@click="clickListHandle"
|
||||
@mouseenter="mouseenter"
|
||||
@mouseleave="mouseId = ''"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="flex-between">
|
||||
<ReadWrite
|
||||
@change="editName($event, row)"
|
||||
:data="row.abstract"
|
||||
trigger="manual"
|
||||
:write="row.writeStatus"
|
||||
@close="closeWrite(row)"
|
||||
:maxlength="1024"
|
||||
/>
|
||||
<div
|
||||
@click.stop
|
||||
v-if="mouseId === row.id && row.id !== 'new' && !row.writeStatus"
|
||||
class="flex"
|
||||
>
|
||||
<el-button style="padding: 0" link @click.stop="openWrite(row)">
|
||||
<el-icon><EditPen /></el-icon>
|
||||
</el-button>
|
||||
<el-button style="padding: 0" link @click.stop="deleteLog(row)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<template #empty>
|
||||
<div class="text-center mt-24">
|
||||
<el-text type="info">{{ $t('chat.noHistory') }}</el-text>
|
||||
</div>
|
||||
</template>
|
||||
</common-list>
|
||||
</div>
|
||||
<div v-if="chatLogData.length" class="gradient-divider lighter mt-8">
|
||||
<span>{{ $t('chat.only20history') }}</span>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</el-collapse-transition>
|
||||
<div class="chat-popover-mask" v-show="show"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, reactive, nextTick, computed } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { isAppIcon } from '@/utils/common'
|
||||
import { hexToRgba } from '@/utils/theme'
|
||||
import { MsgError } from '@/utils/message'
|
||||
import useStore from '@/stores'
|
||||
import { t } from '@/locales'
|
||||
const { user, log } = useStore()
|
||||
|
||||
const AiChatRef = ref()
|
||||
const loading = ref(false)
|
||||
const left_loading = ref(false)
|
||||
const chatLogData = ref<any[]>([])
|
||||
const show = ref(false)
|
||||
const props = defineProps<{
|
||||
application_profile: any
|
||||
applicationAvailable: boolean
|
||||
}>()
|
||||
const applicationDetail = computed({
|
||||
get: () => {
|
||||
return props.application_profile
|
||||
},
|
||||
set: (v) => {}
|
||||
})
|
||||
const paginationConfig = reactive({
|
||||
current_page: 1,
|
||||
page_size: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const currentRecordList = ref<any>([])
|
||||
const currentChatId = ref('new') // 当前历史记录Id 默认为'new'
|
||||
|
||||
const mouseId = ref('')
|
||||
|
||||
const customStyle = computed(() => {
|
||||
return {
|
||||
background: applicationDetail.value?.custom_theme?.theme_color,
|
||||
color: applicationDetail.value?.custom_theme?.header_font_color
|
||||
}
|
||||
})
|
||||
|
||||
function editName(val: string, item: any) {
|
||||
if (val) {
|
||||
const obj = {
|
||||
abstract: val
|
||||
}
|
||||
|
||||
log.asyncPutChatClientLog(applicationDetail.value.id, item.id, obj, loading).then(() => {
|
||||
const find = chatLogData.value.find((row: any) => row.id === item.id)
|
||||
if (find) {
|
||||
find.abstract = val
|
||||
}
|
||||
item['writeStatus'] = false
|
||||
})
|
||||
} else {
|
||||
MsgError(t('views.applicationWorkflow.tip.nameMessage'))
|
||||
}
|
||||
}
|
||||
|
||||
function openWrite(item: any) {
|
||||
item['writeStatus'] = true
|
||||
}
|
||||
|
||||
function closeWrite(item: any) {
|
||||
item['writeStatus'] = false
|
||||
}
|
||||
|
||||
function mouseenter(row: any) {
|
||||
mouseId.value = row.id
|
||||
}
|
||||
function deleteLog(row: any) {
|
||||
log.asyncDelChatClientLog(applicationDetail.value.id, row.id, left_loading).then(() => {
|
||||
if (currentChatId.value === row.id) {
|
||||
currentChatId.value = 'new'
|
||||
paginationConfig.current_page = 1
|
||||
paginationConfig.total = 0
|
||||
currentRecordList.value = []
|
||||
}
|
||||
getChatLog(applicationDetail.value.id)
|
||||
})
|
||||
}
|
||||
|
||||
function handleScroll(event: any) {
|
||||
if (
|
||||
currentChatId.value !== 'new' &&
|
||||
event.scrollTop === 0 &&
|
||||
paginationConfig.total > currentRecordList.value.length
|
||||
) {
|
||||
const history_height = event.dialogScrollbar.offsetHeight
|
||||
paginationConfig.current_page += 1
|
||||
getChatRecord().then(() => {
|
||||
event.scrollDiv.setScrollTop(event.dialogScrollbar.offsetHeight - history_height)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function clickoutside() {
|
||||
show.value = false
|
||||
}
|
||||
|
||||
function newChat() {
|
||||
paginationConfig.current_page = 1
|
||||
currentRecordList.value = []
|
||||
currentChatId.value = 'new'
|
||||
}
|
||||
|
||||
function getChatLog(id: string) {
|
||||
const page = {
|
||||
current_page: 1,
|
||||
page_size: 20
|
||||
}
|
||||
|
||||
log.asyncGetChatLogClient(id, page, left_loading).then((res: any) => {
|
||||
chatLogData.value = res.data.records
|
||||
paginationConfig.current_page = 1
|
||||
paginationConfig.total = 0
|
||||
currentRecordList.value = []
|
||||
currentChatId.value = chatLogData.value?.[0]?.id || 'new'
|
||||
if (currentChatId.value !== 'new') {
|
||||
getChatRecord()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getChatRecord() {
|
||||
return log
|
||||
.asyncChatRecordLog(
|
||||
applicationDetail.value.id,
|
||||
currentChatId.value,
|
||||
paginationConfig,
|
||||
loading,
|
||||
false
|
||||
)
|
||||
.then((res: any) => {
|
||||
paginationConfig.total = res.data.total
|
||||
const list = res.data.records
|
||||
list.map((v: any) => {
|
||||
v['write_ed'] = true
|
||||
v['record_id'] = v.id
|
||||
})
|
||||
currentRecordList.value = [...list, ...currentRecordList.value].sort((a, b) =>
|
||||
a.create_time.localeCompare(b.create_time)
|
||||
)
|
||||
if (paginationConfig.current_page === 1) {
|
||||
nextTick(() => {
|
||||
// 将滚动条滚动到最下面
|
||||
AiChatRef.value.setScrollBottom()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const clickListHandle = (item: any) => {
|
||||
if (item.id !== currentChatId.value) {
|
||||
paginationConfig.current_page = 1
|
||||
currentRecordList.value = []
|
||||
currentChatId.value = item.id
|
||||
if (currentChatId.value !== 'new') {
|
||||
getChatRecord()
|
||||
}
|
||||
show.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function refresh(id: string) {
|
||||
getChatLog(applicationDetail.value.id)
|
||||
currentChatId.value = id
|
||||
}
|
||||
/**
|
||||
*初始化历史对话记录
|
||||
*/
|
||||
const init = () => {
|
||||
if (applicationDetail.value.show_history || !user.isEnterprise()) {
|
||||
getChatLog(applicationDetail.value.id)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
init()
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.chat-mobile {
|
||||
overflow: hidden;
|
||||
&__header {
|
||||
background: var(--app-header-bg-color);
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
height: var(--app-header-height);
|
||||
line-height: var(--app-header-height);
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
}
|
||||
&__main {
|
||||
padding-top: calc(var(--app-header-height) + 16px);
|
||||
height: calc(100vh - var(--app-header-height) - 16px);
|
||||
overflow: hidden;
|
||||
}
|
||||
.new-chat-button {
|
||||
z-index: 11;
|
||||
font-size: 1rem;
|
||||
}
|
||||
// 历史对话弹出层
|
||||
.chat-popover {
|
||||
position: fixed;
|
||||
top: var(--app-header-height);
|
||||
background: #ffffff;
|
||||
padding-bottom: 24px;
|
||||
z-index: 2009;
|
||||
}
|
||||
.chat-popover-button {
|
||||
z-index: 2009;
|
||||
position: fixed;
|
||||
top: 16px;
|
||||
right: 16px;
|
||||
font-size: 22px;
|
||||
}
|
||||
// &.chat-embed--popup {
|
||||
// .chat-popover-button {
|
||||
// right: 85px;
|
||||
// }
|
||||
// }
|
||||
.chat-popover-mask {
|
||||
background-color: var(--el-overlay-color-lighter);
|
||||
bottom: 0;
|
||||
height: 100%;
|
||||
left: 0;
|
||||
overflow: auto;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
top: var(--app-header-height);
|
||||
z-index: 2008;
|
||||
}
|
||||
.gradient-divider {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
color: var(--el-color-info);
|
||||
::before {
|
||||
content: '';
|
||||
width: 17%;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, rgba(222, 224, 227, 0) 0%, #dee0e3 100%);
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 50%;
|
||||
}
|
||||
::after {
|
||||
content: '';
|
||||
width: 17%;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, #dee0e3 0%, rgba(222, 224, 227, 0) 100%);
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 50%;
|
||||
}
|
||||
}
|
||||
// .AiChat-embed {
|
||||
// .ai-chat__operate {
|
||||
// padding-top: 12px;
|
||||
// }
|
||||
// }
|
||||
}
|
||||
</style>
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-overlay) {
|
||||
background-color: transparent;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
class="responsive-dialog"
|
||||
:title="$t('chat.editTitle')"
|
||||
v-model="dialogVisible"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
:destroy-on-close="true"
|
||||
append-to-body
|
||||
>
|
||||
<el-form
|
||||
label-position="top"
|
||||
ref="fieldFormRef"
|
||||
:model="form"
|
||||
require-asterisk-position="right"
|
||||
>
|
||||
<el-form-item
|
||||
prop="abstract"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: $t('common.inputPlaceholder'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
]"
|
||||
>
|
||||
<el-input
|
||||
v-model="form.abstract"
|
||||
maxlength="1024"
|
||||
show-word-limit
|
||||
type="textarea"
|
||||
@blur="form.abstract = form.abstract.trim()"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click.prevent="dialogVisible = false"> {{ $t('common.cancel') }} </el-button>
|
||||
<el-button type="primary" @click="submit(fieldFormRef)" :loading="loading">
|
||||
{{ $t('common.save') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, watch } from 'vue'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import useStore from '@/stores'
|
||||
import { t } from '@/locales'
|
||||
|
||||
const { log } = useStore()
|
||||
const emit = defineEmits(['refresh'])
|
||||
|
||||
const fieldFormRef = ref()
|
||||
const loading = ref<boolean>(false)
|
||||
const applicationId = ref<string>('')
|
||||
const chatId = ref<string>('')
|
||||
|
||||
const form = ref<any>({
|
||||
abstract: ''
|
||||
})
|
||||
|
||||
const dialogVisible = ref<boolean>(false)
|
||||
|
||||
const open = (row: any, id: string) => {
|
||||
applicationId.value = id
|
||||
chatId.value = row.id
|
||||
form.value.abstract = row.abstract
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const submit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
log.asyncPutChatClientLog(applicationId.value, chatId.value, form.value, loading).then(() => {
|
||||
emit('refresh', chatId.value, form.value.abstract)
|
||||
dialogVisible.value = false
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({ open, close })
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,527 @@
|
|||
<template>
|
||||
<div
|
||||
class="chat-pc layout-bg"
|
||||
:class="classObj"
|
||||
v-loading="loading"
|
||||
:style="{
|
||||
'--el-color-primary': applicationDetail?.custom_theme?.theme_color,
|
||||
'--el-color-primary-light-9': hexToRgba(applicationDetail?.custom_theme?.theme_color, 0.1)
|
||||
}"
|
||||
>
|
||||
<div class="chat-pc__header" :style="customStyle">
|
||||
<div class="flex align-center">
|
||||
<div class="mr-12 ml-24 flex">
|
||||
<AppAvatar
|
||||
v-if="isAppIcon(applicationDetail?.icon)"
|
||||
shape="square"
|
||||
:size="32"
|
||||
style="background: none"
|
||||
>
|
||||
<img :src="applicationDetail?.icon" alt="" />
|
||||
</AppAvatar>
|
||||
<AppAvatar
|
||||
v-else-if="applicationDetail?.name"
|
||||
:name="applicationDetail?.name"
|
||||
pinyinColor
|
||||
shape="square"
|
||||
:size="32"
|
||||
/>
|
||||
</div>
|
||||
<h4>{{ applicationDetail?.name }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="flex">
|
||||
<div class="chat-pc__left border-r">
|
||||
<div class="p-24 pb-0">
|
||||
<el-button class="add-button w-full primary" @click="newChat">
|
||||
<el-icon>
|
||||
<Plus />
|
||||
</el-icon>
|
||||
<span class="ml-4">{{ $t('chat.createChat') }}</span>
|
||||
</el-button>
|
||||
<p class="mt-20 mb-8">{{ $t('chat.history') }}</p>
|
||||
</div>
|
||||
<div class="left-height pt-0">
|
||||
<el-scrollbar>
|
||||
<div class="p-8 pt-0">
|
||||
<common-list
|
||||
:style="{
|
||||
'--el-color-primary': applicationDetail?.custom_theme?.theme_color,
|
||||
'--el-color-primary-light-9': hexToRgba(
|
||||
applicationDetail?.custom_theme?.theme_color,
|
||||
0.1
|
||||
)
|
||||
}"
|
||||
:data="chatLogData"
|
||||
class="mt-8"
|
||||
v-loading="left_loading"
|
||||
:defaultActive="currentChatId"
|
||||
@click="clickListHandle"
|
||||
@mouseenter="mouseenter"
|
||||
@mouseleave="mouseId = ''"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="flex-between">
|
||||
<auto-tooltip :content="row.abstract">
|
||||
{{ row.abstract }}
|
||||
</auto-tooltip>
|
||||
<div @click.stop v-show="mouseId === row.id && row.id !== 'new'">
|
||||
<el-dropdown trigger="click" :teleported="false">
|
||||
<el-icon class="rotate-90 mt-4"><MoreFilled /></el-icon>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click.stop="editLogTitle(row)">
|
||||
<el-icon><EditPen /></el-icon>
|
||||
{{ $t('common.edit') }}
|
||||
</el-dropdown-item>
|
||||
<el-dropdown-item @click.stop="deleteLog(row)">
|
||||
<el-icon><Delete /></el-icon>
|
||||
{{ $t('common.delete') }}
|
||||
</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #empty>
|
||||
<div class="text-center">
|
||||
<el-text type="info">{{ $t('chat.noHistory') }}</el-text>
|
||||
</div>
|
||||
</template>
|
||||
</common-list>
|
||||
</div>
|
||||
<div v-if="chatLogData?.length" class="gradient-divider lighter mt-8">
|
||||
<span>{{ $t('chat.only20history') }}</span>
|
||||
</div>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
<div class="chat-pc__right">
|
||||
<div class="right-header border-b mb-24 p-16-24 flex-between">
|
||||
<h4 class="ellipsis-1" style="width: 66%">
|
||||
{{ currentChatName }}
|
||||
</h4>
|
||||
|
||||
<span class="flex align-center" v-if="currentRecordList.length">
|
||||
<AppIcon
|
||||
v-if="paginationConfig.total"
|
||||
iconName="app-chat-record"
|
||||
class="info mr-8"
|
||||
style="font-size: 16px"
|
||||
></AppIcon>
|
||||
<span v-if="paginationConfig.total" class="lighter">
|
||||
{{ paginationConfig.total }} {{ $t('chat.question_count') }}
|
||||
</span>
|
||||
<el-dropdown class="ml-8">
|
||||
<AppIcon
|
||||
iconName="app-export"
|
||||
class="cursor"
|
||||
:title="$t('chat.exportRecords')"
|
||||
></AppIcon>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click="exportMarkdown"
|
||||
>{{ $t('common.export') }} Markdown</el-dropdown-item
|
||||
>
|
||||
<el-dropdown-item @click="exportHTML"
|
||||
>{{ $t('common.export') }} HTML</el-dropdown-item
|
||||
>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</span>
|
||||
</div>
|
||||
<div class="right-height chat-width">
|
||||
<AiChat
|
||||
ref="AiChatRef"
|
||||
v-model:applicationDetails="applicationDetail"
|
||||
:available="applicationAvailable"
|
||||
type="ai-chat"
|
||||
:appId="applicationDetail?.id"
|
||||
:record="currentRecordList"
|
||||
:chatId="currentChatId"
|
||||
@refresh="refresh"
|
||||
@scroll="handleScroll"
|
||||
>
|
||||
</AiChat>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="collapse">
|
||||
<el-button @click="isCollapse = !isCollapse">
|
||||
<el-icon> <component :is="isCollapse ? 'Fold' : 'Expand'" /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</div>
|
||||
<EditTitleDialog ref="EditTitleDialogRef" @refresh="refreshFieldTitle" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, nextTick, computed } from 'vue'
|
||||
import { marked } from 'marked'
|
||||
import { saveAs } from 'file-saver'
|
||||
import { isAppIcon } from '@/utils/common'
|
||||
import useStore from '@/stores'
|
||||
import useResize from '@/layout/hooks/useResize'
|
||||
import { hexToRgba } from '@/utils/theme'
|
||||
import EditTitleDialog from './EditTitleDialog.vue'
|
||||
import { t } from '@/locales'
|
||||
useResize()
|
||||
|
||||
const { user, log, common } = useStore()
|
||||
|
||||
const EditTitleDialogRef = ref()
|
||||
|
||||
const isCollapse = ref(false)
|
||||
|
||||
const customStyle = computed(() => {
|
||||
return {
|
||||
background: applicationDetail.value?.custom_theme?.theme_color,
|
||||
color: applicationDetail.value?.custom_theme?.header_font_color
|
||||
}
|
||||
})
|
||||
|
||||
const classObj = computed(() => {
|
||||
return {
|
||||
mobile: common.isMobile(),
|
||||
hideLeft: !isCollapse.value,
|
||||
openLeft: isCollapse.value
|
||||
}
|
||||
})
|
||||
|
||||
const newObj = {
|
||||
id: 'new',
|
||||
abstract: t('chat.createChat')
|
||||
}
|
||||
const props = defineProps<{
|
||||
application_profile: any
|
||||
applicationAvailable: boolean
|
||||
}>()
|
||||
const AiChatRef = ref()
|
||||
const loading = ref(false)
|
||||
const left_loading = ref(false)
|
||||
|
||||
const applicationDetail = computed({
|
||||
get: () => {
|
||||
return props.application_profile
|
||||
},
|
||||
set: (v) => {}
|
||||
})
|
||||
|
||||
const chatLogData = ref<any[]>([])
|
||||
|
||||
const paginationConfig = ref({
|
||||
current_page: 1,
|
||||
page_size: 20,
|
||||
total: 0
|
||||
})
|
||||
|
||||
const currentRecordList = ref<any>([])
|
||||
const currentChatId = ref('new') // 当前历史记录Id 默认为'new'
|
||||
const currentChatName = ref(t('chat.createChat'))
|
||||
const mouseId = ref('')
|
||||
|
||||
function mouseenter(row: any) {
|
||||
mouseId.value = row.id
|
||||
}
|
||||
|
||||
function editLogTitle(row: any) {
|
||||
EditTitleDialogRef.value.open(row, applicationDetail.value.id)
|
||||
}
|
||||
function refreshFieldTitle(chatId: string, abstract: string) {
|
||||
const find = chatLogData.value.find((item: any) => item.id == chatId)
|
||||
if (find) {
|
||||
find.abstract = abstract
|
||||
}
|
||||
}
|
||||
function deleteLog(row: any) {
|
||||
log.asyncDelChatClientLog(applicationDetail.value.id, row.id, left_loading).then(() => {
|
||||
if (currentChatId.value === row.id) {
|
||||
currentChatId.value = 'new'
|
||||
currentChatName.value = t('chat.createChat')
|
||||
paginationConfig.value.current_page = 1
|
||||
paginationConfig.value.total = 0
|
||||
currentRecordList.value = []
|
||||
}
|
||||
getChatLog(applicationDetail.value.id)
|
||||
})
|
||||
}
|
||||
|
||||
function handleScroll(event: any) {
|
||||
if (
|
||||
currentChatId.value !== 'new' &&
|
||||
event.scrollTop === 0 &&
|
||||
paginationConfig.value.total > currentRecordList.value.length
|
||||
) {
|
||||
const history_height = event.dialogScrollbar.offsetHeight
|
||||
paginationConfig.value.current_page += 1
|
||||
getChatRecord().then(() => {
|
||||
event.scrollDiv.setScrollTop(event.dialogScrollbar.offsetHeight - history_height)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function newChat() {
|
||||
if (!chatLogData.value.some((v) => v.id === 'new')) {
|
||||
paginationConfig.value.current_page = 1
|
||||
paginationConfig.value.total = 0
|
||||
currentRecordList.value = []
|
||||
chatLogData.value.unshift(newObj)
|
||||
} else {
|
||||
paginationConfig.value.current_page = 1
|
||||
paginationConfig.value.total = 0
|
||||
currentRecordList.value = []
|
||||
}
|
||||
currentChatId.value = 'new'
|
||||
currentChatName.value = t('chat.createChat')
|
||||
if (common.isMobile()) {
|
||||
isCollapse.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function getChatLog(id: string, refresh?: boolean) {
|
||||
const page = {
|
||||
current_page: 1,
|
||||
page_size: 20
|
||||
}
|
||||
|
||||
log.asyncGetChatLogClient(id, page, left_loading).then((res: any) => {
|
||||
chatLogData.value = res.data.records
|
||||
if (refresh) {
|
||||
currentChatName.value = chatLogData.value?.[0]?.abstract
|
||||
} else {
|
||||
paginationConfig.value.current_page = 1
|
||||
paginationConfig.value.total = 0
|
||||
currentRecordList.value = []
|
||||
currentChatId.value = chatLogData.value?.[0]?.id || 'new'
|
||||
currentChatName.value = chatLogData.value?.[0]?.abstract || t('chat.createChat')
|
||||
if (currentChatId.value !== 'new') {
|
||||
getChatRecord()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function getChatRecord() {
|
||||
return log
|
||||
.asyncChatRecordLog(
|
||||
applicationDetail.value.id,
|
||||
currentChatId.value,
|
||||
paginationConfig.value,
|
||||
loading,
|
||||
false
|
||||
)
|
||||
.then((res: any) => {
|
||||
paginationConfig.value.total = res.data.total
|
||||
const list = res.data.records
|
||||
list.map((v: any) => {
|
||||
v['write_ed'] = true
|
||||
v['record_id'] = v.id
|
||||
})
|
||||
currentRecordList.value = [...list, ...currentRecordList.value].sort((a, b) =>
|
||||
a.create_time.localeCompare(b.create_time)
|
||||
)
|
||||
if (paginationConfig.value.current_page === 1) {
|
||||
nextTick(() => {
|
||||
// 将滚动条滚动到最下面
|
||||
AiChatRef.value.setScrollBottom()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const clickListHandle = (item: any) => {
|
||||
if (item.id !== currentChatId.value) {
|
||||
paginationConfig.value.current_page = 1
|
||||
paginationConfig.value.total = 0
|
||||
currentRecordList.value = []
|
||||
currentChatId.value = item.id
|
||||
currentChatName.value = item.abstract
|
||||
if (currentChatId.value !== 'new') {
|
||||
getChatRecord()
|
||||
|
||||
// 切换对话后,取消暂停的浏览器播放
|
||||
if (window.speechSynthesis.paused && window.speechSynthesis.speaking) {
|
||||
window.speechSynthesis.resume()
|
||||
nextTick(() => {
|
||||
window.speechSynthesis.cancel()
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if (common.isMobile()) {
|
||||
isCollapse.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function refresh(id: string) {
|
||||
getChatLog(applicationDetail.value.id, true)
|
||||
currentChatId.value = id
|
||||
}
|
||||
|
||||
async function exportMarkdown(): Promise<void> {
|
||||
const suggestedName: string = `${currentChatId.value}.md`
|
||||
const markdownContent: string = currentRecordList.value
|
||||
.map((record: any) => `# ${record.problem_text}\n\n${record.answer_text}\n\n`)
|
||||
.join('\n')
|
||||
|
||||
const blob: Blob = new Blob([markdownContent], { type: 'text/markdown;charset=utf-8' })
|
||||
saveAs(blob, suggestedName)
|
||||
}
|
||||
|
||||
async function exportHTML(): Promise<void> {
|
||||
const suggestedName: string = `${currentChatId.value}.html`
|
||||
const markdownContent: string = currentRecordList.value
|
||||
.map((record: any) => `# ${record.problem_text}\n\n${record.answer_text}\n\n`)
|
||||
.join('\n')
|
||||
const htmlContent: any = marked(markdownContent)
|
||||
|
||||
const blob: Blob = new Blob([htmlContent], { type: 'text/html;charset=utf-8' })
|
||||
saveAs(blob, suggestedName)
|
||||
}
|
||||
|
||||
/**
|
||||
*初始化历史对话记录
|
||||
*/
|
||||
const init = () => {
|
||||
if (
|
||||
(applicationDetail.value.show_history || !user.isEnterprise()) &&
|
||||
props.applicationAvailable
|
||||
) {
|
||||
getChatLog(applicationDetail.value.id)
|
||||
}
|
||||
}
|
||||
onMounted(() => {
|
||||
init()
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.chat-pc {
|
||||
overflow: hidden;
|
||||
|
||||
&__header {
|
||||
background: var(--app-header-bg-color);
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
height: var(--app-header-height);
|
||||
line-height: var(--app-header-height);
|
||||
box-sizing: border-box;
|
||||
border-bottom: 1px solid var(--el-border-color);
|
||||
}
|
||||
|
||||
&__left {
|
||||
padding-top: calc(var(--app-header-height) - 8px);
|
||||
background: #ffffff;
|
||||
width: 280px;
|
||||
|
||||
.add-button {
|
||||
border: 1px solid var(--el-color-primary);
|
||||
}
|
||||
|
||||
.left-height {
|
||||
height: calc(100vh - var(--app-header-height) - 135px);
|
||||
}
|
||||
}
|
||||
|
||||
&__right {
|
||||
width: calc(100% - 280px);
|
||||
padding-top: calc(var(--app-header-height));
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
|
||||
.right-header {
|
||||
background: #ffffff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.right-height {
|
||||
height: calc(100vh - var(--app-header-height) * 2 - 24px);
|
||||
}
|
||||
}
|
||||
|
||||
.gradient-divider {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
color: var(--el-color-info);
|
||||
|
||||
::before {
|
||||
content: '';
|
||||
width: 17%;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, rgba(222, 224, 227, 0) 0%, #dee0e3 100%);
|
||||
position: absolute;
|
||||
left: 16px;
|
||||
top: 50%;
|
||||
}
|
||||
|
||||
::after {
|
||||
content: '';
|
||||
width: 17%;
|
||||
height: 1px;
|
||||
background: linear-gradient(90deg, #dee0e3 0%, rgba(222, 224, 227, 0) 100%);
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
top: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.collapse {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
// 适配移动端
|
||||
.mobile {
|
||||
.chat-pc {
|
||||
&__right {
|
||||
width: 100%;
|
||||
}
|
||||
&__left {
|
||||
display: none;
|
||||
width: 0;
|
||||
}
|
||||
}
|
||||
.collapse {
|
||||
display: block;
|
||||
position: fixed;
|
||||
bottom: 90px;
|
||||
z-index: 99;
|
||||
}
|
||||
&.openLeft {
|
||||
.chat-pc {
|
||||
&__left {
|
||||
display: block;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
z-index: 99;
|
||||
height: calc(100vh - var(--app-header-height) + 6px);
|
||||
}
|
||||
}
|
||||
.collapse {
|
||||
display: block;
|
||||
position: absolute;
|
||||
bottom: 90px;
|
||||
right: 0;
|
||||
z-index: 99;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chat-width {
|
||||
max-width: 80%;
|
||||
margin: 0 auto;
|
||||
}
|
||||
@media only screen and (max-width: 1000px) {
|
||||
.chat-width {
|
||||
max-width: 100% !important;
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -54,7 +54,7 @@
|
|||
<div class="flex-between">
|
||||
<div class="flex align-center">
|
||||
<AppAvatar class="mr-8 avatar-purple" shape="square" :size="32">
|
||||
<img src="@/assets/icon_web.svg" style="width: 58%" alt="" />
|
||||
<img src="@/assets/knowledge/icon_web.svg" style="width: 58%" alt="" />
|
||||
</AppAvatar>
|
||||
<div>
|
||||
<p>
|
||||
|
|
@ -106,7 +106,7 @@
|
|||
<!-- <div class="flex-between">-->
|
||||
<!-- <div class="flex align-center">-->
|
||||
<!-- <AppAvatar class="mr-8" :size="32">-->
|
||||
<!-- <img src="@/assets/icon_web.svg" style="width: 100%" alt="" />-->
|
||||
<!-- <img src="@/assets/knowledge/icon_web.svg" style="width: 100%" alt="" />-->
|
||||
<!-- </AppAvatar>-->
|
||||
<!-- <div>-->
|
||||
<!-- <p>-->
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
shape="square"
|
||||
:size="24"
|
||||
>
|
||||
<img src="@/assets/icon_web.svg" style="width: 58%" alt="" />
|
||||
<img src="@/assets/knowledge/icon_web.svg" style="width: 58%" alt="" />
|
||||
</AppAvatar>
|
||||
<AppAvatar
|
||||
v-else-if="!item.dataset_id && item.type === '2'"
|
||||
|
|
@ -39,7 +39,7 @@
|
|||
:size="24"
|
||||
style="background: none"
|
||||
>
|
||||
<img src="@/assets/logo_lark.svg" style="width: 100%" alt="" />
|
||||
<img src="@/assets/knowledge/logo_lark.svg" style="width: 100%" alt="" />
|
||||
</AppAvatar>
|
||||
<AppAvatar
|
||||
v-else-if="!item.dataset_id && item.type === '0'"
|
||||
|
|
@ -47,7 +47,7 @@
|
|||
shape="square"
|
||||
:size="24"
|
||||
>
|
||||
<img src="@/assets/icon_document.svg" style="width: 58%" alt="" />
|
||||
<img src="@/assets/knowledge/icon_document.svg" style="width: 58%" alt="" />
|
||||
</AppAvatar>
|
||||
{{ item.name }}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -3,22 +3,15 @@
|
|||
v-model="filterText"
|
||||
:placeholder="$t('common.search')"
|
||||
prefix-icon="Search"
|
||||
class="p-24 pt-0 pb-0 mb-16 mt-4"
|
||||
class="mb-16 mt-4"
|
||||
clearable
|
||||
/>
|
||||
<div class="p-24 pt-0">
|
||||
<div class="pt-0">
|
||||
<el-table :data="filterData" :max-height="tableHeight">
|
||||
<el-table-column
|
||||
prop="name"
|
||||
:label="
|
||||
isApplication
|
||||
? $t('views.application.applicationForm.form.appName.label')
|
||||
: $t('views.dataset.datasetForm.form.datasetName.label')
|
||||
"
|
||||
>
|
||||
<el-table-column prop="name" :label="$t('common.name')">
|
||||
<template #default="{ row }">
|
||||
<div class="flex align-center">
|
||||
<AppAvatar
|
||||
<el-avatar
|
||||
v-if="isApplication && isAppIcon(row?.icon)"
|
||||
style="background: none"
|
||||
class="mr-12"
|
||||
|
|
@ -26,9 +19,9 @@
|
|||
:size="24"
|
||||
>
|
||||
<img :src="row?.icon" alt="" />
|
||||
</AppAvatar>
|
||||
</el-avatar>
|
||||
|
||||
<AppAvatar
|
||||
<el-avatar
|
||||
v-else-if="row?.name && isApplication"
|
||||
:name="row?.name"
|
||||
pinyinColor
|
||||
|
|
@ -36,26 +29,26 @@
|
|||
:size="24"
|
||||
class="mr-12"
|
||||
/>
|
||||
<AppAvatar
|
||||
<el-avatar
|
||||
v-if="row.icon === '1' && isDataset"
|
||||
class="mr-8 avatar-purple"
|
||||
shape="square"
|
||||
:size="24"
|
||||
>
|
||||
<img src="@/assets/icon_web.svg" style="width: 58%" alt="" />
|
||||
</AppAvatar>
|
||||
<AppAvatar
|
||||
<img src="@/assets/knowledge/icon_web.svg" style="width: 58%" alt="" />
|
||||
</el-avatar>
|
||||
<el-avatar
|
||||
v-else-if="row.icon === '2' && isDataset"
|
||||
class="mr-8 avatar-purple"
|
||||
shape="square"
|
||||
:size="24"
|
||||
style="background: none"
|
||||
>
|
||||
<img src="@/assets/logo_lark.svg" style="width: 100%" alt="" />
|
||||
</AppAvatar>
|
||||
<AppAvatar v-else-if="isDataset" class="mr-8 avatar-blue" shape="square" :size="24">
|
||||
<img src="@/assets/icon_document.svg" style="width: 58%" alt="" />
|
||||
</AppAvatar>
|
||||
<img src="@/assets/knowledge/logo_lark.svg" style="width: 100%" alt="" />
|
||||
</el-avatar>
|
||||
<el-avatar v-else-if="isDataset" class="mr-8 avatar-blue" shape="square" :size="24">
|
||||
<img src="@/assets/knowledge/icon_document.svg" style="width: 58%" alt="" />
|
||||
</el-avatar>
|
||||
<auto-tooltip :content="row?.name">
|
||||
{{ row?.name }}
|
||||
</auto-tooltip>
|
||||
|
|
@ -63,7 +56,7 @@
|
|||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('views.team.setting.management')"
|
||||
:label="$t('views.resourceAuthorization.setting.management')"
|
||||
align="center"
|
||||
width="100"
|
||||
fixed="right"
|
||||
|
|
@ -71,21 +64,21 @@
|
|||
<template #header>
|
||||
<el-checkbox
|
||||
:disabled="props.manage"
|
||||
v-model="allChecked[TeamEnum.MANAGE]"
|
||||
:indeterminate="allIndeterminate[TeamEnum.MANAGE]"
|
||||
:label="$t('views.team.setting.management')"
|
||||
v-model="allChecked[AuthorizationEnum.MANAGE]"
|
||||
:indeterminate="allIndeterminate[AuthorizationEnum.MANAGE]"
|
||||
:label="$t('views.resourceAuthorization.setting.management')"
|
||||
/>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<el-checkbox
|
||||
:disabled="props.manage"
|
||||
v-model="row.operate[TeamEnum.MANAGE]"
|
||||
@change="(e: boolean) => checkedOperateChange(TeamEnum.MANAGE, row, e)"
|
||||
v-model="row.operate[AuthorizationEnum.MANAGE]"
|
||||
@change="(e: boolean) => checkedOperateChange(AuthorizationEnum.MANAGE, row, e)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('views.team.setting.check')"
|
||||
:label="$t('views.resourceAuthorization.setting.check')"
|
||||
align="center"
|
||||
width="100"
|
||||
fixed="right"
|
||||
|
|
@ -93,16 +86,16 @@
|
|||
<template #header>
|
||||
<el-checkbox
|
||||
:disabled="props.manage"
|
||||
v-model="allChecked[TeamEnum.USE]"
|
||||
:indeterminate="allIndeterminate[TeamEnum.USE]"
|
||||
:label="$t('views.team.setting.check')"
|
||||
v-model="allChecked[AuthorizationEnum.USE]"
|
||||
:indeterminate="allIndeterminate[AuthorizationEnum.USE]"
|
||||
:label="$t('views.resourceAuthorization.setting.check')"
|
||||
/>
|
||||
</template>
|
||||
<template #default="{ row }">
|
||||
<el-checkbox
|
||||
:disabled="props.manage"
|
||||
v-model="row.operate[TeamEnum.USE]"
|
||||
@change="(e: boolean) => checkedOperateChange(TeamEnum.USE, row, e)"
|
||||
v-model="row.operate[AuthorizationEnum.USE]"
|
||||
@change="(e: boolean) => checkedOperateChange(AuthorizationEnum.USE, row, e)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
|
@ -111,92 +104,96 @@
|
|||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch, computed } from 'vue'
|
||||
import { TeamEnum } from '@/enums/team'
|
||||
import { isAppIcon } from '@/utils/application'
|
||||
import { AuthorizationEnum } from '@/enums/system'
|
||||
import { isAppIcon } from '@/utils/common'
|
||||
|
||||
const props = defineProps({
|
||||
data: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
default: () => [],
|
||||
},
|
||||
id: String,
|
||||
type: String,
|
||||
tableHeight: Number,
|
||||
manage: Boolean
|
||||
manage: Boolean,
|
||||
})
|
||||
|
||||
const isDataset = computed(() => props.type === TeamEnum.DATASET)
|
||||
const isApplication = computed(() => props.type === TeamEnum.APPLICATION)
|
||||
const isDataset = computed(() => props.type === AuthorizationEnum.DATASET)
|
||||
const isApplication = computed(() => props.type === AuthorizationEnum.APPLICATION)
|
||||
|
||||
const emit = defineEmits(['update:data'])
|
||||
const allChecked: any = ref({
|
||||
[TeamEnum.MANAGE]: computed({
|
||||
[AuthorizationEnum.MANAGE]: computed({
|
||||
get: () => {
|
||||
return filterData.value.some((item: any) => item.operate[TeamEnum.MANAGE])
|
||||
return filterData.value.some((item: any) => item.operate[AuthorizationEnum.MANAGE])
|
||||
},
|
||||
set: (val: boolean) => {
|
||||
if (val) {
|
||||
filterData.value.map((item: any) => {
|
||||
item.operate[TeamEnum.MANAGE] = true
|
||||
item.operate[TeamEnum.USE] = true
|
||||
item.operate[AuthorizationEnum.MANAGE] = true
|
||||
item.operate[AuthorizationEnum.USE] = true
|
||||
})
|
||||
} else {
|
||||
filterData.value.map((item: any) => {
|
||||
item.operate[TeamEnum.MANAGE] = false
|
||||
item.operate[AuthorizationEnum.MANAGE] = false
|
||||
})
|
||||
}
|
||||
}
|
||||
},
|
||||
}),
|
||||
[TeamEnum.USE]: computed({
|
||||
[AuthorizationEnum.USE]: computed({
|
||||
get: () => {
|
||||
return filterData.value.some((item: any) => item.operate[TeamEnum.USE])
|
||||
return filterData.value.some((item: any) => item.operate[AuthorizationEnum.USE])
|
||||
},
|
||||
set: (val: boolean) => {
|
||||
if (val) {
|
||||
filterData.value.map((item: any) => {
|
||||
item.operate[TeamEnum.USE] = true
|
||||
item.operate[AuthorizationEnum.USE] = true
|
||||
})
|
||||
} else {
|
||||
filterData.value.map((item: any) => {
|
||||
item.operate[TeamEnum.USE] = false
|
||||
item.operate[TeamEnum.MANAGE] = false
|
||||
item.operate[AuthorizationEnum.USE] = false
|
||||
item.operate[AuthorizationEnum.MANAGE] = false
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
}),
|
||||
})
|
||||
|
||||
const filterText = ref('')
|
||||
|
||||
const filterData = computed(() =>
|
||||
props.data.filter((v: any) => v.name.toLowerCase().includes(filterText.value.toLowerCase()))
|
||||
props.data.filter((v: any) => v.name.toLowerCase().includes(filterText.value.toLowerCase())),
|
||||
)
|
||||
|
||||
const allIndeterminate: any = ref({
|
||||
[TeamEnum.MANAGE]: computed(() => {
|
||||
const all_not_checked = filterData.value.every((item: any) => !item.operate[TeamEnum.MANAGE])
|
||||
[AuthorizationEnum.MANAGE]: computed(() => {
|
||||
const all_not_checked = filterData.value.every(
|
||||
(item: any) => !item.operate[AuthorizationEnum.MANAGE],
|
||||
)
|
||||
if (all_not_checked) {
|
||||
return false
|
||||
}
|
||||
return !filterData.value.every((item: any) => item.operate[TeamEnum.MANAGE])
|
||||
return !filterData.value.every((item: any) => item.operate[AuthorizationEnum.MANAGE])
|
||||
}),
|
||||
[TeamEnum.USE]: computed(() => {
|
||||
const all_not_checked = filterData.value.every((item: any) => !item.operate[TeamEnum.USE])
|
||||
[AuthorizationEnum.USE]: computed(() => {
|
||||
const all_not_checked = filterData.value.every(
|
||||
(item: any) => !item.operate[AuthorizationEnum.USE],
|
||||
)
|
||||
if (all_not_checked) {
|
||||
return false
|
||||
}
|
||||
return !filterData.value.every((item: any) => item.operate[TeamEnum.USE])
|
||||
})
|
||||
return !filterData.value.every((item: any) => item.operate[AuthorizationEnum.USE])
|
||||
}),
|
||||
})
|
||||
|
||||
function checkedOperateChange(Name: string | number, row: any, e: boolean) {
|
||||
props.data.map((item: any) => {
|
||||
if (item.id === row.id) {
|
||||
item.operate[Name] = e
|
||||
if (Name === TeamEnum.MANAGE && e) {
|
||||
item.operate[TeamEnum.USE] = true
|
||||
} else if (Name === TeamEnum.USE && !e) {
|
||||
item.operate[TeamEnum.MANAGE] = false
|
||||
if (Name === AuthorizationEnum.MANAGE && e) {
|
||||
item.operate[AuthorizationEnum.USE] = true
|
||||
} else if (Name === AuthorizationEnum.USE && !e) {
|
||||
item.operate[AuthorizationEnum.MANAGE] = false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,13 +1,11 @@
|
|||
<template>
|
||||
<div class="p-16-24">
|
||||
<div class="resource-authorization p-16-24">
|
||||
<h4 class="mb-16">{{ $t('views.userManage.title') }}</h4>
|
||||
<el-card>
|
||||
<div class="resource-authorization flex main-calc-height">
|
||||
<div class="team-member p-8 border-r">
|
||||
<div class="flex-between p-16">
|
||||
<h4>{{ $t('views.resourceAuthorization.member') }}</h4>
|
||||
</div>
|
||||
<div class="team-member-input">
|
||||
<el-card style="--el-card-padding: 0">
|
||||
<div class="flex main-calc-height">
|
||||
<div class="resource-authorization__left border-r p-8">
|
||||
<div class="p-8">
|
||||
<h4 class="mb-12">{{ $t('views.resourceAuthorization.member') }}</h4>
|
||||
<el-input
|
||||
v-model="filterText"
|
||||
:placeholder="$t('common.search')"
|
||||
|
|
@ -27,48 +25,34 @@
|
|||
<template #default="{ row }">
|
||||
<div class="flex-between">
|
||||
<div>
|
||||
<span class="mr-8">{{ row.username }}</span>
|
||||
<span class="mr-8">{{ row.nick_name }}</span>
|
||||
<el-tag v-if="isManage(row.type)" class="default-tag">{{
|
||||
$t('views.resourceAuthorization.manage')
|
||||
}}</el-tag>
|
||||
</div>
|
||||
<div @click.stop style="margin-top: 5px">
|
||||
<el-dropdown trigger="click" v-if="!isManage(row.type)">
|
||||
<span class="cursor">
|
||||
<el-icon class="rotate-90"><MoreFilled /></el-icon>
|
||||
</span>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu>
|
||||
<el-dropdown-item @click.prevent="deleteMember(row)">{{
|
||||
$t('views.resourceAuthorization.delete.button')
|
||||
}}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</common-list>
|
||||
</el-scrollbar>
|
||||
</div>
|
||||
</div>
|
||||
<div class="permission-setting flex" v-loading="rLoading">
|
||||
<div class="team-manage__table">
|
||||
<h4 class="p-24 pb-0 mb-4">{{ $t('views.resourceAuthorization.permissionSetting') }}</h4>
|
||||
<el-tabs v-model="activeName" class="team-manage__tabs">
|
||||
<div class="permission-setting p-16 flex" v-loading="rLoading">
|
||||
<div class="resource-authorization__table">
|
||||
<h4 class="mb-4">{{ $t('views.resourceAuthorization.permissionSetting') }}</h4>
|
||||
<el-tabs v-model="activeName" class="resource-authorization__tabs">
|
||||
<el-tab-pane
|
||||
v-for="(item, index) in settingTags"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:name="item.value"
|
||||
>
|
||||
<!-- <PermissionSetting
|
||||
<PermissionSetting
|
||||
:key="index"
|
||||
:data="item.data"
|
||||
:type="item.value"
|
||||
:tableHeight="tableHeight"
|
||||
:manage="isManage(currentType)"
|
||||
></PermissionSetting> -->
|
||||
></PermissionSetting>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</div>
|
||||
|
|
@ -79,36 +63,33 @@
|
|||
</div>
|
||||
</div>
|
||||
</el-card>
|
||||
<!-- <CreateMemberDialog ref="CreateMemberRef" @refresh="refresh" /> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref, reactive, watch } from 'vue'
|
||||
import AuthorizationApi from '@/api/user/resource-authorization'
|
||||
import type { TeamMember } from '@/api/type/team'
|
||||
// import CreateMemberDialog from './component/CreateMemberDialog.vue'
|
||||
// import PermissionSetting from './component/PermissionSetting.vue'
|
||||
import PermissionSetting from './component/PermissionSetting.vue'
|
||||
import { MsgSuccess, MsgConfirm } from '@/utils/message'
|
||||
import { AuthorizationEnum } from '@/enums/system'
|
||||
import { t } from '@/locales'
|
||||
// const CreateMemberRef = ref<InstanceType<typeof CreateMemberDialog>>()
|
||||
|
||||
const loading = ref(false)
|
||||
const rLoading = ref(false)
|
||||
const memberList = ref<TeamMember[]>([]) // 全部成员
|
||||
const filterMember = ref<TeamMember[]>([]) // 搜索过滤后列表
|
||||
const memberList = ref<any[]>([]) // 全部成员
|
||||
const filterMember = ref<any[]>([]) // 搜索过滤后列表
|
||||
const currentUser = ref<String>('')
|
||||
const currentType = ref<String>('')
|
||||
|
||||
const filterText = ref('')
|
||||
|
||||
const activeName = ref(AuthorizationEnum.DATASET)
|
||||
const activeName = ref(AuthorizationEnum.KNOWLEDGE)
|
||||
const tableHeight = ref(0)
|
||||
|
||||
const settingTags = reactive([
|
||||
{
|
||||
label: t('views.knowledge.title'),
|
||||
value: AuthorizationEnum.DATASET,
|
||||
value: AuthorizationEnum.KNOWLEDGE,
|
||||
data: [] as any,
|
||||
},
|
||||
{
|
||||
|
|
@ -156,10 +137,40 @@ function submitPermissions() {
|
|||
})
|
||||
}
|
||||
|
||||
function clickMemberHandle(item: any) {
|
||||
currentUser.value = item.id
|
||||
currentType.value = item.type
|
||||
ResourcePermissions(item.id)
|
||||
}
|
||||
|
||||
function getMember(id?: string) {
|
||||
loading.value = true
|
||||
AuthorizationApi.getUserList()
|
||||
.then((res) => {
|
||||
memberList.value = res.data
|
||||
filterMember.value = res.data
|
||||
|
||||
const user = (id && memberList.value.find((p) => p.user_id === id)) || null
|
||||
currentUser.value = user ? user.id : memberList.value[0].id
|
||||
currentType.value = user ? user.type : memberList.value[0].type
|
||||
ResourcePermissions(currentUser.value)
|
||||
loading.value = false
|
||||
})
|
||||
.catch(() => {
|
||||
loading.value = false
|
||||
})
|
||||
}
|
||||
function ResourcePermissions() {
|
||||
rLoading.value = true
|
||||
AuthorizationApi.getResourceAuthorization('default')
|
||||
.then((res) => {
|
||||
if (!res.data || Object.keys(res.data).length > 0) {
|
||||
settingTags.map((item) => {
|
||||
if (Object.keys(res.data).indexOf(item.value) !== -1) {
|
||||
item.data = res.data[item.value]
|
||||
}
|
||||
})
|
||||
}
|
||||
rLoading.value = false
|
||||
})
|
||||
.catch(() => {
|
||||
|
|
@ -176,19 +187,13 @@ onMounted(() => {
|
|||
tableHeight.value = window.innerHeight - 330
|
||||
})()
|
||||
}
|
||||
ResourcePermissions()
|
||||
getMember()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.resource-authorization {
|
||||
.add-user-icon {
|
||||
font-size: 17px;
|
||||
}
|
||||
.team-member-input {
|
||||
padding: 0 calc(var(--app-base-px) * 2);
|
||||
}
|
||||
.team-member {
|
||||
.resource-authorization__left {
|
||||
box-sizing: border-box;
|
||||
width: var(--setting-left-width);
|
||||
min-width: var(--setting-left-width);
|
||||
|
|
@ -196,28 +201,17 @@ onMounted(() => {
|
|||
|
||||
.permission-setting {
|
||||
box-sizing: border-box;
|
||||
width: calc(100% - var(--setting-left-width));
|
||||
width: 100%;
|
||||
flex-direction: column;
|
||||
position: relative;
|
||||
.submit-button {
|
||||
position: absolute;
|
||||
top: 54px;
|
||||
top: 16px;
|
||||
right: 24px;
|
||||
}
|
||||
}
|
||||
.list-height-left {
|
||||
height: calc(var(--create-dataset-height) - 60px);
|
||||
}
|
||||
|
||||
&__tabs {
|
||||
margin-top: 10px;
|
||||
|
||||
:deep(.el-tabs__nav-scroll) {
|
||||
padding: 0 24px;
|
||||
}
|
||||
}
|
||||
&__table {
|
||||
flex: 1;
|
||||
height: calc(100vh - 240px);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,53 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
:title="title"
|
||||
v-model="dialogVisible"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
:destroy-on-close="true"
|
||||
:before-close="close"
|
||||
append-to-body
|
||||
>
|
||||
<DynamicsFormConstructor
|
||||
v-model="dynamicsFormData"
|
||||
label-position="top"
|
||||
require-asterisk-position="right"
|
||||
ref="dynamicsFormConstructorRef"
|
||||
></DynamicsFormConstructor>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click.prevent="dialogVisible = false"> {{$t('common.cancel')}} </el-button>
|
||||
<el-button type="primary" @click="submit()" :loading="loading"> {{$t('common.add')}} </el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import DynamicsFormConstructor from '@/components/dynamics-form/constructor/index.vue'
|
||||
import { t } from '@/locales'
|
||||
const props = withDefaults(
|
||||
defineProps<{ title?: string; addFormField: (form_data: any) => void }>(),
|
||||
{ title: t('views.template.templateForm.title.addParam') }
|
||||
)
|
||||
const dialogVisible = ref<boolean>(false)
|
||||
const dynamicsFormConstructorRef = ref<InstanceType<typeof DynamicsFormConstructor>>()
|
||||
const emit = defineEmits(['submit'])
|
||||
const dynamicsFormData = ref<any>({})
|
||||
const loading = ref<boolean>(false)
|
||||
const open = () => {
|
||||
dialogVisible.value = true
|
||||
}
|
||||
const close = () => {
|
||||
dialogVisible.value = false
|
||||
dynamicsFormData.value = {}
|
||||
}
|
||||
const submit = () => {
|
||||
dynamicsFormConstructorRef.value?.validate().then(() => {
|
||||
props.addFormField(dynamicsFormConstructorRef.value?.getData())
|
||||
close()
|
||||
})
|
||||
}
|
||||
defineExpose({ close, open })
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<template>
|
||||
<div class="custom-edge cursor" @mouseup.stop @click.stop v-show="props.model.isHovered">
|
||||
<svg
|
||||
@click="deleteEdge"
|
||||
width="22"
|
||||
height="22"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 23.0001C5.925 23.0001 1 18.0751 1 12.0001C1 5.92512 5.925 1.00012 12 1.00012C18.075 1.00012 23 5.92512 23 12.0001C23 18.0751 18.075 23.0001 12 23.0001Z"
|
||||
fill="#3370FF"
|
||||
/>
|
||||
<path
|
||||
d="M9.02524 7.61124L12.0002 10.5862L14.9752 7.61124C15.069 7.5175 15.1962 7.46484 15.3287 7.46484C15.4613 7.46484 15.5885 7.5175 15.6822 7.61124L16.3892 8.31824C16.483 8.412 16.5356 8.53915 16.5356 8.67174C16.5356 8.80432 16.483 8.93147 16.3892 9.02524L13.4142 12.0002L16.3892 14.9752C16.483 15.069 16.5356 15.1962 16.5356 15.3287C16.5356 15.4613 16.483 15.5885 16.3892 15.6822L15.6822 16.3892C15.5885 16.483 15.4613 16.5356 15.3287 16.5356C15.1962 16.5356 15.069 16.483 14.9752 16.3892L12.0002 13.4142L9.02524 16.3892C8.93147 16.483 8.80432 16.5356 8.67174 16.5356C8.53916 16.5356 8.412 16.483 8.31824 16.3892L7.61124 15.6822C7.5175 15.5885 7.46484 15.4613 7.46484 15.3287C7.46484 15.1962 7.5175 15.069 7.61124 14.9752L10.5862 12.0002L7.61124 9.02524C7.5175 8.93147 7.46484 8.80432 7.46484 8.67174C7.46484 8.53915 7.5175 8.412 7.61124 8.31824L8.31824 7.61124C8.412 7.5175 8.53916 7.46484 8.67174 7.46484C8.80432 7.46484 8.93147 7.5175 9.02524 7.61124Z"
|
||||
fill="white"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
const props = defineProps<{ model: any }>()
|
||||
const deleteEdge = () => {
|
||||
props.model.graphModel.deleteEdgeById(props.model.id)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.custom-edge {
|
||||
color: var(--el-color-primary);
|
||||
stroke: none;
|
||||
z-index: 100000;
|
||||
position: absolute;
|
||||
pointer-events: all;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
:title="title"
|
||||
v-model="dialogVisible"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
:destroy-on-close="true"
|
||||
:before-close="close"
|
||||
append-to-body
|
||||
>
|
||||
<DynamicsFormConstructor
|
||||
v-model="dynamicsFormData"
|
||||
label-position="top"
|
||||
require-asterisk-position="right"
|
||||
ref="dynamicsFormConstructorRef"
|
||||
></DynamicsFormConstructor>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click.prevent="dialogVisible = false"> {{ $t('common.cancel') }} </el-button>
|
||||
<el-button type="primary" @click="submit()" :loading="loading">
|
||||
{{ $t('common.modify') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import DynamicsFormConstructor from '@/components/dynamics-form/constructor/index.vue'
|
||||
import { t } from '@/locales'
|
||||
const props = withDefaults(
|
||||
defineProps<{ title?: string; editFormField: (form_data: any, index: number) => void }>(),
|
||||
{ title: t('views.template.templateForm.title.editParam') }
|
||||
)
|
||||
const dialogVisible = ref<boolean>(false)
|
||||
const dynamicsFormConstructorRef = ref<InstanceType<typeof DynamicsFormConstructor>>()
|
||||
const emit = defineEmits(['submit'])
|
||||
const dynamicsFormData = ref<any>({})
|
||||
const currentIndex = ref<number>(0)
|
||||
const loading = ref<boolean>(false)
|
||||
const open = (form_data: any, index: number) => {
|
||||
dialogVisible.value = true
|
||||
dynamicsFormData.value = form_data
|
||||
currentIndex.value = index
|
||||
}
|
||||
const close = () => {
|
||||
dialogVisible.value = false
|
||||
dynamicsFormData.value = {}
|
||||
}
|
||||
const submit = () => {
|
||||
dynamicsFormConstructorRef.value?.validate().then(() => {
|
||||
props.editFormField(dynamicsFormConstructorRef.value?.getData(), currentIndex.value)
|
||||
close()
|
||||
})
|
||||
}
|
||||
defineExpose({ close, open })
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
<template>
|
||||
<el-cascader
|
||||
@wheel="wheel"
|
||||
:teleported="false"
|
||||
:options="options"
|
||||
@visible-change="visibleChange"
|
||||
v-bind="$attrs"
|
||||
v-model="data"
|
||||
separator=" > "
|
||||
>
|
||||
<template #default="{ node, data }">
|
||||
<span class="flex align-center" @wheel="wheel">
|
||||
<component :is="iconComponent(`${data.type}-icon`)" class="mr-8" :size="18" />{{
|
||||
data.label
|
||||
}}</span
|
||||
>
|
||||
</template>
|
||||
</el-cascader>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { iconComponent } from '../icons/utils'
|
||||
import { t } from '@/locales'
|
||||
const props = defineProps<{
|
||||
nodeModel: any
|
||||
modelValue: Array<any>
|
||||
global?: Boolean
|
||||
}>()
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const data = computed({
|
||||
set: (value) => {
|
||||
emit('update:modelValue', value)
|
||||
},
|
||||
get: () => {
|
||||
return props.modelValue
|
||||
}
|
||||
})
|
||||
const options = ref<Array<any>>([])
|
||||
|
||||
const wheel = (e: any) => {
|
||||
if (e.ctrlKey === true) {
|
||||
e.preventDefault()
|
||||
return true
|
||||
} else {
|
||||
e.stopPropagation()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function visibleChange(bool: boolean) {
|
||||
if (bool) {
|
||||
options.value = props.global
|
||||
? props.nodeModel.get_up_node_field_list(false, true).filter((v: any) => v.value === 'global')
|
||||
: props.nodeModel.get_up_node_field_list(false, true)
|
||||
}
|
||||
}
|
||||
|
||||
const validate = () => {
|
||||
const incomingNodeValue = props.nodeModel.get_up_node_field_list(false, true)
|
||||
if (!data.value || data.value.length === 0) {
|
||||
return Promise.reject(t('views.applicationWorkflow.variable.ReferencingRequired'))
|
||||
}
|
||||
if (data.value.length < 2) {
|
||||
return Promise.reject(t('views.applicationWorkflow.variable.ReferencingError'))
|
||||
}
|
||||
const node_id = data.value[0]
|
||||
const node_field = data.value[1]
|
||||
const nodeParent = incomingNodeValue.find((item: any) => item.value === node_id)
|
||||
if (!nodeParent) {
|
||||
data.value = []
|
||||
return Promise.reject(t('views.applicationWorkflow.variable.NoReferencing'))
|
||||
}
|
||||
if (!nodeParent.children.some((item: any) => item.value === node_field)) {
|
||||
data.value = []
|
||||
return Promise.reject(t('views.applicationWorkflow.variable.NoReferencing'))
|
||||
}
|
||||
return Promise.resolve('')
|
||||
}
|
||||
defineExpose({ validate })
|
||||
onMounted(() => {
|
||||
options.value = props.global
|
||||
? props.nodeModel.get_up_node_field_list(false, true).filter((v: any) => v.value === 'global')
|
||||
: props.nodeModel.get_up_node_field_list(false, true)
|
||||
})
|
||||
</script>
|
||||
<style scoped></style>
|
||||
|
|
@ -0,0 +1,367 @@
|
|||
<template>
|
||||
<div @mousedown="mousedown" class="workflow-node-container p-16" style="overflow: visible">
|
||||
<div
|
||||
class="step-container app-card p-16"
|
||||
:class="{ isSelected: props.nodeModel.isSelected, error: node_status !== 200 }"
|
||||
style="overflow: visible"
|
||||
>
|
||||
<div v-resize="resizeStepContainer">
|
||||
<div class="flex-between">
|
||||
<div class="flex align-center" style="width: 70%">
|
||||
<component
|
||||
:is="iconComponent(`${nodeModel.type}-icon`)"
|
||||
class="mr-8"
|
||||
:size="24"
|
||||
:item="nodeModel?.properties.node_data"
|
||||
/>
|
||||
<h4 class="ellipsis-1 break-all">{{ nodeModel.properties.stepName }}</h4>
|
||||
</div>
|
||||
|
||||
<div @mousemove.stop @mousedown.stop @keydown.stop @click.stop>
|
||||
<el-button text @click="showNode = !showNode">
|
||||
<el-icon class="arrow-icon color-secondary" :class="showNode ? 'rotate-180' : ''"
|
||||
><ArrowDownBold />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-dropdown
|
||||
v-if="showOperate(nodeModel.type)"
|
||||
:teleported="false"
|
||||
trigger="click"
|
||||
placement="bottom-start"
|
||||
>
|
||||
<el-button text>
|
||||
<img src="@/assets/icon_or.svg" alt="" v-if="condition === 'OR'" />
|
||||
<img src="@/assets/icon_and.svg" alt="" v-if="condition === 'AND'" />
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<div style="width: 280px" class="p-12-16">
|
||||
<h5>{{ $t('views.applicationWorkflow.condition.title') }}</h5>
|
||||
<p class="mt-8 lighter">
|
||||
<span>{{ $t('views.applicationWorkflow.condition.front') }}</span>
|
||||
<el-select v-model="condition" size="small" style="width: 60px; margin: 0 8px">
|
||||
<el-option
|
||||
:label="$t('views.applicationWorkflow.condition.AND')"
|
||||
value="AND"
|
||||
/>
|
||||
<el-option :label="$t('views.applicationWorkflow.condition.OR')" value="OR" />
|
||||
</el-select>
|
||||
<span>{{ $t('views.applicationWorkflow.condition.text') }}</span>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
<el-dropdown v-if="showOperate(nodeModel.type)" :teleported="false" trigger="click">
|
||||
<el-button text>
|
||||
<el-icon class="color-secondary"><MoreFilled /></el-icon>
|
||||
</el-button>
|
||||
<template #dropdown>
|
||||
<el-dropdown-menu style="min-width: 80px">
|
||||
<el-dropdown-item @click="renameNode" class="p-8">{{
|
||||
$t('common.rename')
|
||||
}}</el-dropdown-item>
|
||||
<el-dropdown-item @click="copyNode" class="p-8">{{
|
||||
$t('common.copy')
|
||||
}}</el-dropdown-item>
|
||||
<el-dropdown-item @click="deleteNode" class="border-t p-8">{{
|
||||
$t('common.delete')
|
||||
}}</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</template>
|
||||
</el-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<el-collapse-transition>
|
||||
<div @mousedown.stop @keydown.stop @click.stop v-show="showNode" class="mt-16">
|
||||
<el-alert
|
||||
v-if="node_status != 200"
|
||||
class="mb-16"
|
||||
:title="
|
||||
props.nodeModel.type === 'application-node'
|
||||
? $t('views.applicationWorkflow.tip.applicationNodeError')
|
||||
: $t('views.applicationWorkflow.tip.functionNodeError')
|
||||
"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="false"
|
||||
/>
|
||||
<slot></slot>
|
||||
<template v-if="nodeFields.length > 0">
|
||||
<h5 class="title-decoration-1 mb-8 mt-8">
|
||||
{{ $t('common.param.outputParam') }}
|
||||
</h5>
|
||||
<template v-for="(item, index) in nodeFields" :key="index">
|
||||
<div
|
||||
class="flex-between border-r-4 p-8-12 mb-8 layout-bg lighter"
|
||||
@mouseenter="showicon = index"
|
||||
@mouseleave="showicon = null"
|
||||
>
|
||||
<span style="max-width: 92%">{{ item.label }} {{ '{' + item.value + '}' }}</span>
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
:content="$t('views.applicationWorkflow.setting.copyParam')"
|
||||
placement="top"
|
||||
v-if="showicon === index"
|
||||
>
|
||||
<el-button link @click="copyClick(item.globeLabel)" style="padding: 0">
|
||||
<AppIcon iconName="app-copy"></AppIcon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</div>
|
||||
</el-collapse-transition>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-collapse-transition>
|
||||
<DropdownMenu
|
||||
v-if="showAnchor"
|
||||
@mousemove.stop
|
||||
@mousedown.stop
|
||||
@click.stop
|
||||
@wheel="handleWheel"
|
||||
:show="showAnchor"
|
||||
:id="id"
|
||||
style="left: 100%; top: 50%; transform: translate(0, -50%)"
|
||||
@clickNodes="clickNodes"
|
||||
/>
|
||||
</el-collapse-transition>
|
||||
|
||||
<el-dialog
|
||||
:title="$t('views.applicationWorkflow.nodeName')"
|
||||
v-model="nodeNameDialogVisible"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
:destroy-on-close="true"
|
||||
append-to-body
|
||||
@submit.prevent
|
||||
>
|
||||
<el-form label-position="top" ref="titleFormRef" :model="form">
|
||||
<el-form-item
|
||||
prop="title"
|
||||
:rules="[
|
||||
{
|
||||
required: true,
|
||||
message: $t('common.inputPlaceholder'),
|
||||
trigger: 'blur'
|
||||
}
|
||||
]"
|
||||
>
|
||||
<el-input v-model="form.title" @blur="form.title = form.title.trim()" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click.prevent="nodeNameDialogVisible = false">
|
||||
{{ $t('common.cancel') }}
|
||||
</el-button>
|
||||
<el-button type="primary" @click="editName(titleFormRef)">
|
||||
{{ $t('common.save') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { app } from '@/main'
|
||||
import DropdownMenu from '@/views/application-workflow/component/DropdownMenu.vue'
|
||||
import { set } from 'lodash'
|
||||
import { iconComponent } from '../icons/utils'
|
||||
import { copyClick } from '@/utils/clipboard'
|
||||
import { WorkflowType } from '@/enums/workflow'
|
||||
import { MsgError, MsgConfirm } from '@/utils/message'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { t } from '@/locales'
|
||||
const {
|
||||
params: { id }
|
||||
} = app.config.globalProperties.$route as any
|
||||
|
||||
const height = ref<{
|
||||
stepContainerHeight: number
|
||||
inputContainerHeight: number
|
||||
outputContainerHeight: number
|
||||
}>({
|
||||
stepContainerHeight: 0,
|
||||
inputContainerHeight: 0,
|
||||
outputContainerHeight: 0
|
||||
})
|
||||
const showAnchor = ref<boolean>(false)
|
||||
const anchorData = ref<any>()
|
||||
const titleFormRef = ref()
|
||||
const nodeNameDialogVisible = ref<boolean>(false)
|
||||
const form = ref<any>({
|
||||
title: ''
|
||||
})
|
||||
|
||||
const condition = computed({
|
||||
set: (v) => {
|
||||
set(props.nodeModel.properties, 'condition', v)
|
||||
},
|
||||
get: () => {
|
||||
if (props.nodeModel.properties.condition) {
|
||||
return props.nodeModel.properties.condition
|
||||
}
|
||||
set(props.nodeModel.properties, 'condition', 'AND')
|
||||
return true
|
||||
}
|
||||
})
|
||||
const showNode = computed({
|
||||
set: (v) => {
|
||||
set(props.nodeModel.properties, 'showNode', v)
|
||||
},
|
||||
get: () => {
|
||||
if (props.nodeModel.properties.showNode !== undefined) {
|
||||
return props.nodeModel.properties.showNode
|
||||
}
|
||||
set(props.nodeModel.properties, 'showNode', true)
|
||||
return true
|
||||
}
|
||||
})
|
||||
|
||||
const handleWheel = (event: any) => {
|
||||
const isCombinationKeyPressed = event.ctrlKey || event.metaKey
|
||||
if (!isCombinationKeyPressed) {
|
||||
event.stopPropagation()
|
||||
}
|
||||
}
|
||||
const node_status = computed(() => {
|
||||
if (props.nodeModel.properties.status) {
|
||||
return props.nodeModel.properties.status
|
||||
}
|
||||
return 200
|
||||
})
|
||||
|
||||
function renameNode() {
|
||||
form.value.title = props.nodeModel.properties.stepName
|
||||
nodeNameDialogVisible.value = true
|
||||
}
|
||||
const editName = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
if (
|
||||
!props.nodeModel.graphModel.nodes?.some(
|
||||
(node: any) => node.properties.stepName === form.value.title
|
||||
)
|
||||
) {
|
||||
set(props.nodeModel.properties, 'stepName', form.value.title)
|
||||
nodeNameDialogVisible.value = false
|
||||
formEl.resetFields()
|
||||
} else {
|
||||
MsgError(t('views.applicationWorkflow.tip.repeatedNodeError'))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const mousedown = () => {
|
||||
props.nodeModel.graphModel.clearSelectElements()
|
||||
set(props.nodeModel, 'isSelected', true)
|
||||
set(props.nodeModel, 'isHovered', true)
|
||||
props.nodeModel.graphModel.toFront(props.nodeModel.id)
|
||||
}
|
||||
const showicon = ref<number | null>(null)
|
||||
const copyNode = () => {
|
||||
props.nodeModel.graphModel.clearSelectElements()
|
||||
const cloneNode = props.nodeModel.graphModel.cloneNode(props.nodeModel.id)
|
||||
set(cloneNode, 'isSelected', true)
|
||||
set(cloneNode, 'isHovered', true)
|
||||
props.nodeModel.graphModel.toFront(cloneNode.id)
|
||||
}
|
||||
const deleteNode = () => {
|
||||
MsgConfirm(t('common.tip'), t('views.applicationWorkflow.delete.confirmTitle'), {
|
||||
confirmButtonText: t('common.confirm'),
|
||||
confirmButtonClass: 'danger'
|
||||
}).then(() => {
|
||||
props.nodeModel.graphModel.deleteNode(props.nodeModel.id)
|
||||
})
|
||||
props.nodeModel.graphModel.eventCenter.emit('delete_node')
|
||||
}
|
||||
const resizeStepContainer = (wh: any) => {
|
||||
if (wh.height) {
|
||||
if (!props.nodeModel.virtual) {
|
||||
height.value.stepContainerHeight = wh.height
|
||||
props.nodeModel.setHeight(height.value.stepContainerHeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clickNodes(item: any) {
|
||||
const width = item.properties.width ? item.properties.width : 214
|
||||
const nodeModel = props.nodeModel.graphModel.addNode({
|
||||
type: item.type,
|
||||
properties: item.properties,
|
||||
x: anchorData.value?.x + width / 2 + 200,
|
||||
y: anchorData.value?.y - item.height
|
||||
})
|
||||
props.nodeModel.graphModel.addEdge({
|
||||
type: 'app-edge',
|
||||
sourceNodeId: props.nodeModel.id,
|
||||
sourceAnchorId: anchorData.value?.id,
|
||||
targetNodeId: nodeModel.id
|
||||
})
|
||||
|
||||
closeNodeMenu()
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
nodeModel: any
|
||||
}>()
|
||||
const nodeFields = computed(() => {
|
||||
if (props.nodeModel.properties.config.fields) {
|
||||
const fields = props.nodeModel.properties.config.fields?.map((field: any) => {
|
||||
return {
|
||||
label: field.label,
|
||||
value: field.value,
|
||||
globeLabel: `{{${props.nodeModel.properties.stepName}.${field.value}}}`,
|
||||
globeValue: `{{context['${props.nodeModel.id}'].${field.value}}}`
|
||||
}
|
||||
})
|
||||
return fields
|
||||
}
|
||||
return []
|
||||
})
|
||||
|
||||
function showOperate(type: string) {
|
||||
return type !== WorkflowType.Base && type !== WorkflowType.Start
|
||||
}
|
||||
const openNodeMenu = (anchorValue: any) => {
|
||||
showAnchor.value = true
|
||||
anchorData.value = anchorValue
|
||||
}
|
||||
const closeNodeMenu = () => {
|
||||
showAnchor.value = false
|
||||
anchorData.value = undefined
|
||||
}
|
||||
onMounted(() => {
|
||||
set(props.nodeModel, 'openNodeMenu', (anchorData: any) => {
|
||||
showAnchor.value ? closeNodeMenu() : openNodeMenu(anchorData)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.workflow-node-container {
|
||||
.step-container {
|
||||
border: 2px solid #ffffff !important;
|
||||
box-sizing: border-box;
|
||||
&:hover {
|
||||
box-shadow: 0px 6px 24px 0px rgba(31, 35, 41, 0.08);
|
||||
}
|
||||
&.isSelected {
|
||||
border: 2px solid var(--el-color-primary) !important;
|
||||
}
|
||||
&.error {
|
||||
border: 1px solid #f54a45 !important;
|
||||
}
|
||||
}
|
||||
.arrow-icon {
|
||||
transition: 0.2s;
|
||||
}
|
||||
}
|
||||
:deep(.el-card) {
|
||||
overflow: visible;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,110 @@
|
|||
<template>
|
||||
<el-card shadow="always" style="--el-card-padding: 8px 12px; --el-card-border-radius: 8px">
|
||||
<el-button link @click="zoomOut">
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
:content="$t('views.applicationWorkflow.control.zoomOut')"
|
||||
placement="top"
|
||||
>
|
||||
<el-icon :size="16" :title="$t('views.applicationWorkflow.control.zoomOut')"
|
||||
><ZoomOut
|
||||
/></el-icon>
|
||||
</el-tooltip>
|
||||
</el-button>
|
||||
<el-button link @click="zoomIn">
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
:content="$t('views.applicationWorkflow.control.zoomIn')"
|
||||
placement="top"
|
||||
>
|
||||
<el-icon :size="16" :title="$t('views.applicationWorkflow.control.zoomIn')"
|
||||
><ZoomIn
|
||||
/></el-icon>
|
||||
</el-tooltip>
|
||||
</el-button>
|
||||
<el-button link @click="fitView">
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
:content="$t('views.applicationWorkflow.control.fitView')"
|
||||
placement="top"
|
||||
>
|
||||
<AppIcon
|
||||
iconName="app-fitview"
|
||||
:title="$t('views.applicationWorkflow.control.fitView')"
|
||||
></AppIcon>
|
||||
</el-tooltip>
|
||||
</el-button>
|
||||
<el-divider direction="vertical" />
|
||||
<el-button link @click="retract">
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
:content="$t('views.applicationWorkflow.control.retract')"
|
||||
placement="top"
|
||||
>
|
||||
<AppIcon
|
||||
style="font-size: 16px"
|
||||
iconName="app-retract"
|
||||
:title="$t('views.applicationWorkflow.control.retract')"
|
||||
></AppIcon>
|
||||
</el-tooltip>
|
||||
</el-button>
|
||||
<el-button link @click="extend">
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
:content="$t('views.applicationWorkflow.control.extend')"
|
||||
placement="top"
|
||||
>
|
||||
<AppIcon
|
||||
style="font-size: 16px"
|
||||
iconName="app-extend"
|
||||
:title="$t('views.applicationWorkflow.control.extend')"
|
||||
></AppIcon>
|
||||
</el-tooltip>
|
||||
</el-button>
|
||||
<el-button link @click="layout">
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
:content="$t('views.applicationWorkflow.control.beautify')"
|
||||
placement="top"
|
||||
>
|
||||
<AppIcon
|
||||
style="font-size: 16px"
|
||||
iconName="app-beautify"
|
||||
:title="$t('views.applicationWorkflow.control.beautify')"
|
||||
></AppIcon>
|
||||
</el-tooltip>
|
||||
</el-button>
|
||||
</el-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
const props = defineProps({
|
||||
lf: Object || String || null
|
||||
})
|
||||
|
||||
function zoomIn() {
|
||||
props.lf?.zoom(true, [0, 0])
|
||||
}
|
||||
function zoomOut() {
|
||||
props.lf?.zoom(false, [0, 0])
|
||||
}
|
||||
function fitView() {
|
||||
props.lf?.resetZoom()
|
||||
props.lf?.resetTranslate()
|
||||
props.lf?.fitView()
|
||||
}
|
||||
const layout = () => {
|
||||
props.lf?.extension.dagre.layout()
|
||||
}
|
||||
const retract = () => {
|
||||
props.lf?.graphModel.nodes.forEach((element: any) => {
|
||||
element.properties.showNode = false
|
||||
})
|
||||
}
|
||||
const extend = () => {
|
||||
props.lf?.graphModel.nodes.forEach((element: any) => {
|
||||
element.properties.showNode = true
|
||||
})
|
||||
}
|
||||
</script>
|
||||
<style scoped></style>
|
||||
|
|
@ -0,0 +1,407 @@
|
|||
import Components from '@/components'
|
||||
import ElementPlus from 'element-plus'
|
||||
import * as ElementPlusIcons from '@element-plus/icons-vue'
|
||||
import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
|
||||
import { HtmlResize } from '@logicflow/extension'
|
||||
import { h as lh } from '@logicflow/core'
|
||||
import { createApp, h } from 'vue'
|
||||
import directives from '@/directives'
|
||||
import i18n from '@/locales'
|
||||
import { WorkflowType } from '@/enums/workflow'
|
||||
import { nodeDict } from '@/workflow/common/data'
|
||||
import { isActive, connect, disconnect } from './teleport'
|
||||
import { t } from '@/locales'
|
||||
import { type Dict } from '@/api/type/common'
|
||||
class AppNode extends HtmlResize.view {
|
||||
isMounted
|
||||
r?: any
|
||||
component: any
|
||||
app: any
|
||||
root?: any
|
||||
VueNode: any
|
||||
up_node_field_dict?: Dict<Array<any>>
|
||||
constructor(props: any, VueNode: any) {
|
||||
super(props)
|
||||
this.component = VueNode
|
||||
this.isMounted = false
|
||||
props.model.clear_next_node_field = this.clear_next_node_field.bind(this)
|
||||
props.model.get_up_node_field_dict = this.get_up_node_field_dict.bind(this)
|
||||
props.model.get_node_field_list = this.get_node_field_list.bind(this)
|
||||
props.model.get_up_node_field_list = this.get_up_node_field_list.bind(this)
|
||||
|
||||
if (props.model.properties.noRender) {
|
||||
delete props.model.properties.noRender
|
||||
} else {
|
||||
const filterNodes = props.graphModel.nodes.filter((v: any) => v.type === props.model.type)
|
||||
const filterNameSameNodes = filterNodes.filter(
|
||||
(v: any) => v.properties.stepName === props.model.properties.stepName
|
||||
)
|
||||
if (filterNameSameNodes.length - 1 > 0) {
|
||||
getNodesName(filterNameSameNodes.length - 1)
|
||||
}
|
||||
}
|
||||
function getNodesName(num: number) {
|
||||
const number = num
|
||||
const name = props.model.properties.stepName + number
|
||||
if (!props.graphModel.nodes?.some((node: any) => node.properties.stepName === name.trim())) {
|
||||
props.model.properties.stepName = name
|
||||
} else {
|
||||
getNodesName(number + 1)
|
||||
}
|
||||
}
|
||||
props.model.properties.config = nodeDict[props.model.type].properties.config
|
||||
if (props.model.properties.height) {
|
||||
props.model.height = props.model.properties.height
|
||||
}
|
||||
}
|
||||
get_node_field_list() {
|
||||
const result = []
|
||||
if (this.props.model.type === 'start-node') {
|
||||
result.push({
|
||||
value: 'global',
|
||||
label: t('views.applicationWorkflow.variable.global'),
|
||||
type: 'global',
|
||||
children: this.props.model.properties?.config?.globalFields || []
|
||||
})
|
||||
}
|
||||
result.push({
|
||||
value: this.props.model.id,
|
||||
label: this.props.model.properties.stepName,
|
||||
type: this.props.model.type,
|
||||
children: this.props.model.properties?.config?.fields || []
|
||||
})
|
||||
return result
|
||||
}
|
||||
get_up_node_field_dict(contain_self: boolean, use_cache: boolean) {
|
||||
if (!this.up_node_field_dict || !use_cache) {
|
||||
const up_node_list = this.props.graphModel.getNodeIncomingNode(this.props.model.id)
|
||||
this.up_node_field_dict = up_node_list
|
||||
.filter((node) => node.id != 'start-node')
|
||||
.map((node) => node.get_up_node_field_dict(true, use_cache))
|
||||
.reduce((pre, next) => ({ ...pre, ...next }), {})
|
||||
}
|
||||
if (contain_self) {
|
||||
return {
|
||||
...this.up_node_field_dict,
|
||||
[this.props.model.id]: this.get_node_field_list()
|
||||
}
|
||||
}
|
||||
return this.up_node_field_dict ? this.up_node_field_dict : {}
|
||||
}
|
||||
|
||||
get_up_node_field_list(contain_self: boolean, use_cache: boolean) {
|
||||
const result = Object.values(this.get_up_node_field_dict(contain_self, use_cache)).reduce(
|
||||
(pre, next) => [...pre, ...next],
|
||||
[]
|
||||
)
|
||||
const start_node_field_list = this.props.graphModel
|
||||
.getNodeModelById('start-node')
|
||||
.get_node_field_list()
|
||||
return [...start_node_field_list, ...result]
|
||||
}
|
||||
|
||||
clear_next_node_field(contain_self: boolean) {
|
||||
const next_node_list = this.props.graphModel.getNodeOutgoingNode(this.props.model.id)
|
||||
next_node_list.forEach((node) => {
|
||||
node.clear_next_node_field(true)
|
||||
})
|
||||
if (contain_self) {
|
||||
this.up_node_field_dict = undefined
|
||||
}
|
||||
}
|
||||
getAnchorShape(anchorData: any) {
|
||||
const { x, y, type } = anchorData
|
||||
let isConnect = false
|
||||
|
||||
if (type == 'left') {
|
||||
isConnect = this.props.graphModel.edges.some((edge) => edge.targetAnchorId == anchorData.id)
|
||||
} else {
|
||||
isConnect = this.props.graphModel.edges.some((edge) => edge.sourceAnchorId == anchorData.id)
|
||||
}
|
||||
|
||||
return lh(
|
||||
'foreignObject',
|
||||
{
|
||||
...anchorData,
|
||||
x: x - 10,
|
||||
y: y - 12,
|
||||
width: 30,
|
||||
height: 30
|
||||
},
|
||||
[
|
||||
lh('div', {
|
||||
style: { zindex: 0 },
|
||||
onClick: () => {
|
||||
if (type == 'right') {
|
||||
this.props.model.openNodeMenu(anchorData)
|
||||
}
|
||||
},
|
||||
dangerouslySetInnerHTML: {
|
||||
__html: isConnect
|
||||
? `<svg width="100%" height="100%" viewBox="0 0 42 42" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_d_5119_232585)">
|
||||
<path d="M20.9998 29.8333C28.0875 29.8333 33.8332 24.0876 33.8332 17C33.8332 9.91231 28.0875 4.16663 20.9998 4.16663C13.9122 4.16663 8.1665 9.91231 8.1665 17C8.1665 24.0876 13.9122 29.8333 20.9998 29.8333Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M20.9998 27.5C26.7988 27.5 31.4998 22.799 31.4998 17C31.4998 11.201 26.7988 6.49996 20.9998 6.49996C15.2008 6.49996 10.4998 11.201 10.4998 17C10.4998 22.799 15.2008 27.5 20.9998 27.5ZM33.8332 17C33.8332 24.0876 28.0875 29.8333 20.9998 29.8333C13.9122 29.8333 8.1665 24.0876 8.1665 17C8.1665 9.91231 13.9122 4.16663 20.9998 4.16663C28.0875 4.16663 33.8332 9.91231 33.8332 17Z" fill="#3370FF"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_5119_232585" x="-1" y="-1" width="44" height="44" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="4"/>
|
||||
<feGaussianBlur stdDeviation="4"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.2 0 0 0 0 0.439216 0 0 0 0 1 0 0 0 0.1 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_5119_232585"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_5119_232585" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
||||
`
|
||||
: `<svg width="100%" height="100%" viewBox="0 0 42 42" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_d_5199_166905)">
|
||||
<path d="M20.9998 29.8333C28.0875 29.8333 33.8332 24.0876 33.8332 17C33.8332 9.91231 28.0875 4.16663 20.9998 4.16663C13.9122 4.16663 8.1665 9.91231 8.1665 17C8.1665 24.0876 13.9122 29.8333 20.9998 29.8333Z" fill="#3370FF"/>
|
||||
<path d="M19.8332 11.75C19.8332 11.4278 20.0943 11.1666 20.4165 11.1666H21.5832C21.9053 11.1666 22.1665 11.4278 22.1665 11.75V15.8333H26.2498C26.572 15.8333 26.8332 16.0945 26.8332 16.4166V17.5833C26.8332 17.9055 26.572 18.1666 26.2498 18.1666H22.1665V22.25C22.1665 22.5721 21.9053 22.8333 21.5832 22.8333H20.4165C20.0943 22.8333 19.8332 22.5721 19.8332 22.25V18.1666H15.7498C15.4277 18.1666 15.1665 17.9055 15.1665 17.5833V16.4166C15.1665 16.0945 15.4277 15.8333 15.7498 15.8333H19.8332V11.75Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_d_5199_166905" x="-1" y="-1" width="44" height="44" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="4"/>
|
||||
<feGaussianBlur stdDeviation="4"/>
|
||||
<feComposite in2="hardAlpha" operator="out"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0.2 0 0 0 0 0.439216 0 0 0 0 1 0 0 0 0.1 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_5199_166905"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_5199_166905" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>`
|
||||
}
|
||||
})
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
setHtml(rootEl: HTMLElement) {
|
||||
if (!this.isMounted) {
|
||||
this.isMounted = true
|
||||
const node = document.createElement('div')
|
||||
rootEl.appendChild(node)
|
||||
this.renderVueComponent(node)
|
||||
} else {
|
||||
if (this.r && this.r.component) {
|
||||
this.r.component.props.properties = this.props.model.getProperties()
|
||||
}
|
||||
}
|
||||
}
|
||||
componentWillUnmount() {
|
||||
super.componentWillUnmount()
|
||||
this.unmount()
|
||||
}
|
||||
getComponentContainer() {
|
||||
return this.root
|
||||
}
|
||||
protected targetId() {
|
||||
return `${this.props.graphModel.flowId}:${this.props.model.id}`
|
||||
}
|
||||
protected renderVueComponent(root: any) {
|
||||
this.unmountVueComponent()
|
||||
this.root = root
|
||||
const { model, graphModel } = this.props
|
||||
|
||||
if (root) {
|
||||
if (isActive()) {
|
||||
connect(this.targetId(), this.component, root, model, graphModel)
|
||||
} else {
|
||||
this.r = h(this.component, {
|
||||
properties: this.props.model.properties,
|
||||
nodeModel: this.props.model
|
||||
})
|
||||
this.app = createApp({
|
||||
render() {
|
||||
return this.r
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
getNode: () => model,
|
||||
getGraph: () => graphModel
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
this.app.use(ElementPlus, {
|
||||
locale: zhCn
|
||||
})
|
||||
this.app.use(Components)
|
||||
this.app.use(directives)
|
||||
this.app.use(i18n)
|
||||
for (const [key, component] of Object.entries(ElementPlusIcons)) {
|
||||
this.app.component(key, component)
|
||||
}
|
||||
this.app?.mount(root)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected unmountVueComponent() {
|
||||
if (this.app) {
|
||||
this.app.unmount()
|
||||
this.app = null
|
||||
}
|
||||
if (this.root) {
|
||||
this.root.innerHTML = ''
|
||||
}
|
||||
return this.root
|
||||
}
|
||||
|
||||
unmount() {
|
||||
if (isActive()) {
|
||||
disconnect(this.targetId())
|
||||
}
|
||||
this.unmountVueComponent()
|
||||
}
|
||||
}
|
||||
|
||||
class AppNodeModel extends HtmlResize.model {
|
||||
refreshDeges() {
|
||||
// 更新节点连接边的path
|
||||
this.incoming.edges.forEach((edge: any) => {
|
||||
// 调用自定义的更新方案
|
||||
edge.updatePathByAnchor()
|
||||
})
|
||||
this.outgoing.edges.forEach((edge: any) => {
|
||||
edge.updatePathByAnchor()
|
||||
})
|
||||
}
|
||||
set_position(position: { x?: number; y?: number }) {
|
||||
const { x, y } = position
|
||||
if (x) {
|
||||
this.x = x
|
||||
}
|
||||
if (y) {
|
||||
this.y = y
|
||||
}
|
||||
this.refreshDeges()
|
||||
}
|
||||
getResizeOutlineStyle() {
|
||||
const style = super.getResizeOutlineStyle()
|
||||
style.stroke = 'none'
|
||||
return style
|
||||
}
|
||||
getControlPointStyle() {
|
||||
const style = super.getControlPointStyle()
|
||||
style.stroke = 'none'
|
||||
style.fill = 'none'
|
||||
return style
|
||||
}
|
||||
getNodeStyle() {
|
||||
return {
|
||||
overflow: 'visible'
|
||||
}
|
||||
}
|
||||
getOutlineStyle() {
|
||||
const style = super.getOutlineStyle()
|
||||
style.stroke = 'none'
|
||||
if (style.hover) {
|
||||
style.hover.stroke = 'none'
|
||||
}
|
||||
return style
|
||||
}
|
||||
// 如果不用修改锚地形状,可以重写颜色相关样式
|
||||
getAnchorStyle(anchorInfo: any) {
|
||||
const style = super.getAnchorStyle(anchorInfo)
|
||||
if (anchorInfo.type === 'left') {
|
||||
style.fill = 'red'
|
||||
style.hover.fill = 'transparent'
|
||||
style.hover.stroke = 'transpanrent'
|
||||
style.className = 'lf-hide-default'
|
||||
} else {
|
||||
style.fill = 'green'
|
||||
}
|
||||
return style
|
||||
}
|
||||
|
||||
setHeight(height: number) {
|
||||
const sourceHeight = this.height
|
||||
const targetHeight = height + 100
|
||||
this.height = targetHeight
|
||||
this.properties['height'] = targetHeight
|
||||
this.move(0, (targetHeight - sourceHeight) / 2)
|
||||
this.outgoing.edges.forEach((edge: any) => {
|
||||
// 调用自定义的更新方案
|
||||
edge.updatePathByAnchor()
|
||||
})
|
||||
this.incoming.edges.forEach((edge: any) => {
|
||||
// 调用自定义的更新方案
|
||||
edge.updatePathByAnchor()
|
||||
})
|
||||
}
|
||||
get_width() {
|
||||
return this.properties?.width || 340
|
||||
}
|
||||
|
||||
setAttributes() {
|
||||
const { t } = i18n.global
|
||||
this.width = this.get_width()
|
||||
const isLoop = (node_id: string, target_node_id: string) => {
|
||||
const up_node_list = this.graphModel.getNodeIncomingNode(node_id)
|
||||
for (const index in up_node_list) {
|
||||
const item = up_node_list[index]
|
||||
if (item.id === target_node_id) {
|
||||
return true
|
||||
} else {
|
||||
const result = isLoop(item.id, target_node_id)
|
||||
if (result) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
const circleOnlyAsTarget = {
|
||||
message: t('views.applicationWorkflow.tip.onlyRight'),
|
||||
validate: (sourceNode: any, targetNode: any, sourceAnchor: any) => {
|
||||
return sourceAnchor.type === 'right'
|
||||
}
|
||||
}
|
||||
this.sourceRules.push({
|
||||
message: t('views.applicationWorkflow.tip.notRecyclable'),
|
||||
validate: (sourceNode: any, targetNode: any, sourceAnchor: any, targetAnchor: any) => {
|
||||
return !isLoop(sourceNode.id, targetNode.id)
|
||||
}
|
||||
})
|
||||
|
||||
this.sourceRules.push(circleOnlyAsTarget)
|
||||
this.targetRules.push({
|
||||
message: t('views.applicationWorkflow.tip.onlyLeft'),
|
||||
validate: (sourceNode: any, targetNode: any, sourceAnchor: any, targetAnchor: any) => {
|
||||
return targetAnchor.type === 'left'
|
||||
}
|
||||
})
|
||||
}
|
||||
getDefaultAnchor() {
|
||||
const { id, x, y, width } = this
|
||||
const showNode = this.properties.showNode === undefined ? true : this.properties.showNode
|
||||
const anchors: any = []
|
||||
|
||||
if (this.type !== WorkflowType.Base) {
|
||||
if (this.type !== WorkflowType.Start) {
|
||||
anchors.push({
|
||||
x: x - width / 2 + 10,
|
||||
y: showNode ? y : y - 15,
|
||||
id: `${id}_left`,
|
||||
edgeAddable: false,
|
||||
type: 'left'
|
||||
})
|
||||
}
|
||||
anchors.push({
|
||||
x: x + width / 2 - 10,
|
||||
y: showNode ? y : y - 15,
|
||||
id: `${id}_right`,
|
||||
type: 'right'
|
||||
})
|
||||
}
|
||||
|
||||
return anchors
|
||||
}
|
||||
}
|
||||
export { AppNodeModel, AppNode }
|
||||
|
|
@ -0,0 +1,465 @@
|
|||
import { WorkflowType } from '@/enums/workflow'
|
||||
import { t } from '@/locales'
|
||||
|
||||
export const startNode = {
|
||||
id: WorkflowType.Start,
|
||||
type: WorkflowType.Start,
|
||||
x: 480,
|
||||
y: 3340,
|
||||
properties: {
|
||||
height: 364,
|
||||
stepName: t('views.applicationWorkflow.nodes.startNode.label'),
|
||||
config: {
|
||||
fields: [
|
||||
{
|
||||
label: t('views.applicationWorkflow.nodes.startNode.question'),
|
||||
value: 'question'
|
||||
}
|
||||
],
|
||||
globalFields: [
|
||||
{ label: t('views.applicationWorkflow.nodes.startNode.currentTime'), value: 'time' },
|
||||
{
|
||||
label: t('views.application.applicationForm.form.historyRecord.label'),
|
||||
value: 'history_context'
|
||||
},
|
||||
{
|
||||
label: t('chat.chatId'),
|
||||
value: 'chat_id'
|
||||
}
|
||||
]
|
||||
},
|
||||
fields: [{ label: t('views.applicationWorkflow.nodes.startNode.question'), value: 'question' }],
|
||||
globalFields: [
|
||||
{ label: t('views.applicationWorkflow.nodes.startNode.currentTime'), value: 'time' }
|
||||
],
|
||||
showNode: true
|
||||
}
|
||||
}
|
||||
export const baseNode = {
|
||||
id: WorkflowType.Base,
|
||||
type: WorkflowType.Base,
|
||||
x: 360,
|
||||
y: 2761.3875,
|
||||
text: '',
|
||||
properties: {
|
||||
height: 728.375,
|
||||
stepName: t('views.applicationWorkflow.nodes.baseNode.label'),
|
||||
input_field_list: [],
|
||||
node_data: {
|
||||
name: '',
|
||||
desc: '',
|
||||
// @ts-ignore
|
||||
prologue: t('views.application.applicationForm.form.defaultPrologue'),
|
||||
tts_type: 'BROWSER'
|
||||
},
|
||||
config: {},
|
||||
showNode: true,
|
||||
user_input_config: { title: t('chat.userInput') },
|
||||
user_input_field_list: []
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 说明
|
||||
* type 与 nodes 文件对应
|
||||
*/
|
||||
export const baseNodes = [baseNode, startNode]
|
||||
/**
|
||||
* ai对话节点配置数据
|
||||
*/
|
||||
export const aiChatNode = {
|
||||
type: WorkflowType.AiChat,
|
||||
text: t('views.applicationWorkflow.nodes.aiChatNode.text'),
|
||||
label: t('views.applicationWorkflow.nodes.aiChatNode.label'),
|
||||
height: 340,
|
||||
properties: {
|
||||
stepName: t('views.applicationWorkflow.nodes.aiChatNode.label'),
|
||||
config: {
|
||||
fields: [
|
||||
{
|
||||
label: t('views.applicationWorkflow.nodes.aiChatNode.answer'),
|
||||
value: 'answer'
|
||||
},
|
||||
{
|
||||
label: t('views.applicationWorkflow.nodes.aiChatNode.think'),
|
||||
value: 'reasoning_content'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 知识库检索配置数据
|
||||
*/
|
||||
export const searchDatasetNode = {
|
||||
type: WorkflowType.SearchDataset,
|
||||
text: t('views.applicationWorkflow.nodes.searchDatasetNode.text'),
|
||||
label: t('views.applicationWorkflow.nodes.searchDatasetNode.label'),
|
||||
height: 355,
|
||||
properties: {
|
||||
stepName: t('views.applicationWorkflow.nodes.searchDatasetNode.label'),
|
||||
config: {
|
||||
fields: [
|
||||
{
|
||||
label: t('views.applicationWorkflow.nodes.searchDatasetNode.paragraph_list'),
|
||||
value: 'paragraph_list'
|
||||
},
|
||||
{
|
||||
label: t('views.applicationWorkflow.nodes.searchDatasetNode.is_hit_handling_method_list'),
|
||||
value: 'is_hit_handling_method_list'
|
||||
},
|
||||
{
|
||||
label: t('views.applicationWorkflow.nodes.searchDatasetNode.result'),
|
||||
value: 'data'
|
||||
},
|
||||
{
|
||||
label: t('views.applicationWorkflow.nodes.searchDatasetNode.directly_return'),
|
||||
value: 'directly_return'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
export const questionNode = {
|
||||
type: WorkflowType.Question,
|
||||
text: t('views.applicationWorkflow.nodes.questionNode.text'),
|
||||
label: t('views.applicationWorkflow.nodes.questionNode.label'),
|
||||
height: 345,
|
||||
properties: {
|
||||
stepName: t('views.applicationWorkflow.nodes.questionNode.label'),
|
||||
config: {
|
||||
fields: [
|
||||
{
|
||||
label: t('views.applicationWorkflow.nodes.questionNode.result'),
|
||||
value: 'answer'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
export const conditionNode = {
|
||||
type: WorkflowType.Condition,
|
||||
text: t('views.applicationWorkflow.nodes.conditionNode.text'),
|
||||
label: t('views.applicationWorkflow.nodes.conditionNode.label'),
|
||||
height: 175,
|
||||
properties: {
|
||||
width: 600,
|
||||
stepName: t('views.applicationWorkflow.nodes.conditionNode.label'),
|
||||
config: {
|
||||
fields: [
|
||||
{
|
||||
label: t('views.applicationWorkflow.nodes.conditionNode.branch_name'),
|
||||
value: 'branch_name'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
export const replyNode = {
|
||||
type: WorkflowType.Reply,
|
||||
text: t('views.applicationWorkflow.nodes.replyNode.text'),
|
||||
label: t('views.applicationWorkflow.nodes.replyNode.label'),
|
||||
height: 210,
|
||||
properties: {
|
||||
stepName: t('views.applicationWorkflow.nodes.replyNode.label'),
|
||||
config: {
|
||||
fields: [
|
||||
{
|
||||
label: t('views.applicationWorkflow.nodes.replyNode.content'),
|
||||
value: 'answer'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
export const rerankerNode = {
|
||||
type: WorkflowType.RrerankerNode,
|
||||
text: t('views.applicationWorkflow.nodes.rerankerNode.text'),
|
||||
label: t('views.applicationWorkflow.nodes.rerankerNode.label'),
|
||||
height: 252,
|
||||
properties: {
|
||||
stepName: t('views.applicationWorkflow.nodes.rerankerNode.label'),
|
||||
config: {
|
||||
fields: [
|
||||
{
|
||||
label: t('views.applicationWorkflow.nodes.rerankerNode.result_list'),
|
||||
value: 'result_list'
|
||||
},
|
||||
{
|
||||
label: t('views.applicationWorkflow.nodes.rerankerNode.result'),
|
||||
value: 'result'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
export const formNode = {
|
||||
type: WorkflowType.FormNode,
|
||||
text: t('views.applicationWorkflow.nodes.formNode.text'),
|
||||
label: t('views.applicationWorkflow.nodes.formNode.label'),
|
||||
height: 252,
|
||||
properties: {
|
||||
width: 600,
|
||||
stepName: t('views.applicationWorkflow.nodes.formNode.label'),
|
||||
node_data: {
|
||||
is_result: true,
|
||||
form_field_list: [],
|
||||
form_content_format: `${t('views.applicationWorkflow.nodes.formNode.form_content_format1')}
|
||||
{{form}}
|
||||
${t('views.applicationWorkflow.nodes.formNode.form_content_format2')}`
|
||||
},
|
||||
config: {
|
||||
fields: [
|
||||
{
|
||||
label: t('views.applicationWorkflow.nodes.formNode.form_data'),
|
||||
value: 'form_data'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
export const documentExtractNode = {
|
||||
type: WorkflowType.DocumentExtractNode,
|
||||
text: t('views.applicationWorkflow.nodes.documentExtractNode.text'),
|
||||
label: t('views.applicationWorkflow.nodes.documentExtractNode.label'),
|
||||
height: 252,
|
||||
properties: {
|
||||
stepName: t('views.applicationWorkflow.nodes.documentExtractNode.label'),
|
||||
config: {
|
||||
fields: [
|
||||
{
|
||||
label: t('views.applicationWorkflow.nodes.documentExtractNode.content'),
|
||||
value: 'content'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
export const imageUnderstandNode = {
|
||||
type: WorkflowType.ImageUnderstandNode,
|
||||
text: t('views.applicationWorkflow.nodes.imageUnderstandNode.text'),
|
||||
label: t('views.applicationWorkflow.nodes.imageUnderstandNode.label'),
|
||||
height: 252,
|
||||
properties: {
|
||||
stepName: t('views.applicationWorkflow.nodes.imageUnderstandNode.label'),
|
||||
config: {
|
||||
fields: [
|
||||
{
|
||||
label: t('views.applicationWorkflow.nodes.imageUnderstandNode.answer'),
|
||||
value: 'answer'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const variableAssignNode = {
|
||||
type: WorkflowType.VariableAssignNode,
|
||||
text: t('views.applicationWorkflow.nodes.variableAssignNode.text'),
|
||||
label: t('views.applicationWorkflow.nodes.variableAssignNode.label'),
|
||||
height: 252,
|
||||
properties: {
|
||||
stepName: t('views.applicationWorkflow.nodes.variableAssignNode.label'),
|
||||
config: {}
|
||||
}
|
||||
}
|
||||
|
||||
export const mcpNode = {
|
||||
type: WorkflowType.McpNode,
|
||||
text: t('views.applicationWorkflow.nodes.mcpNode.text'),
|
||||
label: t('views.applicationWorkflow.nodes.mcpNode.label'),
|
||||
height: 252,
|
||||
properties: {
|
||||
stepName: t('views.applicationWorkflow.nodes.mcpNode.label'),
|
||||
config: {
|
||||
fields: [
|
||||
{
|
||||
label: t('common.result'),
|
||||
value: 'result'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const imageGenerateNode = {
|
||||
type: WorkflowType.ImageGenerateNode,
|
||||
text: t('views.applicationWorkflow.nodes.imageGenerateNode.text'),
|
||||
label: t('views.applicationWorkflow.nodes.imageGenerateNode.label'),
|
||||
height: 252,
|
||||
properties: {
|
||||
stepName: t('views.applicationWorkflow.nodes.imageGenerateNode.label'),
|
||||
config: {
|
||||
fields: [
|
||||
{
|
||||
label: t('views.applicationWorkflow.nodes.imageGenerateNode.answer'),
|
||||
value: 'answer'
|
||||
},
|
||||
{
|
||||
label: t('common.fileUpload.image'),
|
||||
value: 'image'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const speechToTextNode = {
|
||||
type: WorkflowType.SpeechToTextNode,
|
||||
text: t('views.applicationWorkflow.nodes.speechToTextNode.text'),
|
||||
label: t('views.applicationWorkflow.nodes.speechToTextNode.label'),
|
||||
height: 252,
|
||||
properties: {
|
||||
stepName: t('views.applicationWorkflow.nodes.speechToTextNode.label'),
|
||||
config: {
|
||||
fields: [
|
||||
{
|
||||
label: t('common.result'),
|
||||
value: 'result'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
export const textToSpeechNode = {
|
||||
type: WorkflowType.TextToSpeechNode,
|
||||
text: t('views.applicationWorkflow.nodes.textToSpeechNode.text'),
|
||||
label: t('views.applicationWorkflow.nodes.textToSpeechNode.label'),
|
||||
height: 252,
|
||||
properties: {
|
||||
stepName: t('views.applicationWorkflow.nodes.textToSpeechNode.label'),
|
||||
config: {
|
||||
fields: [
|
||||
{
|
||||
label: t('common.result'),
|
||||
value: 'result'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
export const menuNodes = [
|
||||
aiChatNode,
|
||||
imageUnderstandNode,
|
||||
imageGenerateNode,
|
||||
searchDatasetNode,
|
||||
rerankerNode,
|
||||
conditionNode,
|
||||
replyNode,
|
||||
formNode,
|
||||
questionNode,
|
||||
documentExtractNode,
|
||||
speechToTextNode,
|
||||
textToSpeechNode,
|
||||
variableAssignNode,
|
||||
mcpNode
|
||||
]
|
||||
|
||||
/**
|
||||
* 自定义函数配置数据
|
||||
*/
|
||||
export const functionNode = {
|
||||
type: WorkflowType.FunctionLibCustom,
|
||||
text: t('views.applicationWorkflow.nodes.functionNode.text'),
|
||||
label: t('views.applicationWorkflow.nodes.functionNode.label'),
|
||||
height: 260,
|
||||
properties: {
|
||||
stepName: t('views.applicationWorkflow.nodes.functionNode.label'),
|
||||
config: {
|
||||
fields: [
|
||||
{
|
||||
label: t('common.result'),
|
||||
value: 'result'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
export const functionLibNode = {
|
||||
type: WorkflowType.FunctionLib,
|
||||
text: t('views.applicationWorkflow.nodes.functionNode.text'),
|
||||
label: t('views.applicationWorkflow.nodes.functionNode.label'),
|
||||
height: 170,
|
||||
properties: {
|
||||
stepName: t('views.applicationWorkflow.nodes.functionNode.label'),
|
||||
config: {
|
||||
fields: [
|
||||
{
|
||||
label: t('common.result'),
|
||||
value: 'result'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const applicationNode = {
|
||||
type: WorkflowType.Application,
|
||||
text: t('views.applicationWorkflow.nodes.applicationNode.label'),
|
||||
label: t('views.applicationWorkflow.nodes.applicationNode.label'),
|
||||
height: 260,
|
||||
properties: {
|
||||
stepName: t('views.applicationWorkflow.nodes.applicationNode.label'),
|
||||
config: {
|
||||
fields: [
|
||||
{
|
||||
label: t('common.result'),
|
||||
value: 'result'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const compareList = [
|
||||
{ value: 'is_null', label: t('views.applicationWorkflow.compare.is_null') },
|
||||
{ value: 'is_not_null', label: t('views.applicationWorkflow.compare.is_not_null') },
|
||||
{ value: 'contain', label: t('views.applicationWorkflow.compare.contain') },
|
||||
{ value: 'not_contain', label: t('views.applicationWorkflow.compare.not_contain') },
|
||||
{ value: 'eq', label: t('views.applicationWorkflow.compare.eq') },
|
||||
{ value: 'ge', label: t('views.applicationWorkflow.compare.ge') },
|
||||
{ value: 'gt', label: t('views.applicationWorkflow.compare.gt') },
|
||||
{ value: 'le', label: t('views.applicationWorkflow.compare.le') },
|
||||
{ value: 'lt', label: t('views.applicationWorkflow.compare.lt') },
|
||||
{ value: 'len_eq', label: t('views.applicationWorkflow.compare.len_eq') },
|
||||
{ value: 'len_ge', label: t('views.applicationWorkflow.compare.len_ge') },
|
||||
{ value: 'len_gt', label: t('views.applicationWorkflow.compare.len_gt') },
|
||||
{ value: 'len_le', label: t('views.applicationWorkflow.compare.len_le') },
|
||||
{ value: 'len_lt', label: t('views.applicationWorkflow.compare.len_lt') },
|
||||
{ value: 'is_true', label: t('views.applicationWorkflow.compare.is_true') },
|
||||
{ value: 'is_not_true', label: t('views.applicationWorkflow.compare.is_not_true') }
|
||||
]
|
||||
|
||||
export const nodeDict: any = {
|
||||
[WorkflowType.AiChat]: aiChatNode,
|
||||
[WorkflowType.SearchDataset]: searchDatasetNode,
|
||||
[WorkflowType.Question]: questionNode,
|
||||
[WorkflowType.Condition]: conditionNode,
|
||||
[WorkflowType.Base]: baseNode,
|
||||
[WorkflowType.Start]: startNode,
|
||||
[WorkflowType.Reply]: replyNode,
|
||||
[WorkflowType.FunctionLib]: functionLibNode,
|
||||
[WorkflowType.FunctionLibCustom]: functionNode,
|
||||
[WorkflowType.RrerankerNode]: rerankerNode,
|
||||
[WorkflowType.FormNode]: formNode,
|
||||
[WorkflowType.Application]: applicationNode,
|
||||
[WorkflowType.DocumentExtractNode]: documentExtractNode,
|
||||
[WorkflowType.ImageUnderstandNode]: imageUnderstandNode,
|
||||
[WorkflowType.TextToSpeechNode]: textToSpeechNode,
|
||||
[WorkflowType.SpeechToTextNode]: speechToTextNode,
|
||||
[WorkflowType.ImageGenerateNode]: imageGenerateNode,
|
||||
[WorkflowType.VariableAssignNode]: variableAssignNode,
|
||||
[WorkflowType.McpNode]: mcpNode
|
||||
}
|
||||
export function isWorkFlow(type: string | undefined) {
|
||||
return type === 'WORK_FLOW'
|
||||
}
|
||||
|
||||
export function isLastNode(nodeModel: any) {
|
||||
const incoming = nodeModel.graphModel.getNodeIncomingNode(nodeModel.id)
|
||||
const outcomming = nodeModel.graphModel.getNodeOutgoingNode(nodeModel.id)
|
||||
if (incoming.length > 0 && outcomming.length === 0) {
|
||||
return true
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,243 @@
|
|||
import { BezierEdge, BezierEdgeModel, h } from '@logicflow/core'
|
||||
import { createApp, h as vh } from 'vue'
|
||||
import { isActive, connect, disconnect } from './teleport'
|
||||
import CustomLine from './CustomLine.vue'
|
||||
function isMouseInElement(element: any, e: any) {
|
||||
const rect = element.getBoundingClientRect()
|
||||
return (
|
||||
e.clientX >= rect.left &&
|
||||
e.clientX <= rect.right &&
|
||||
e.clientY >= rect.top &&
|
||||
e.clientY <= rect.bottom
|
||||
)
|
||||
}
|
||||
const DEFAULT_WIDTH = 32
|
||||
const DEFAULT_HEIGHT = 32
|
||||
class CustomEdge2 extends BezierEdge {
|
||||
isMounted
|
||||
customLineApp?: any
|
||||
root?: any
|
||||
constructor() {
|
||||
super()
|
||||
this.isMounted = false
|
||||
this.handleMouseUp = (e: any) => {
|
||||
this.props.graphModel.clearSelectElements()
|
||||
this.props.model.isSelected = true
|
||||
const element = e.target.parentNode.parentNode.querySelector('.lf-custom-edge-wrapper')
|
||||
if (isMouseInElement(element, e)) {
|
||||
this.props.model.graphModel.deleteEdgeById(this.props.model.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 渲染vue组件
|
||||
* @param root
|
||||
*/
|
||||
protected renderVueComponent(root: any) {
|
||||
this.unmountVueComponent()
|
||||
this.root = root
|
||||
const { graphModel } = this.props
|
||||
if (root) {
|
||||
if (isActive()) {
|
||||
connect(
|
||||
this.targetId(),
|
||||
CustomLine,
|
||||
root,
|
||||
this.props.model,
|
||||
graphModel,
|
||||
(node: any, graph: any) => {
|
||||
return { model: node, graph }
|
||||
}
|
||||
)
|
||||
} else {
|
||||
this.customLineApp = createApp({
|
||||
render: () => vh(CustomLine, { model: this.props.model })
|
||||
})
|
||||
this.customLineApp?.mount(root)
|
||||
}
|
||||
}
|
||||
}
|
||||
protected targetId() {
|
||||
return `${this.props.graphModel.flowId}:${this.props.model.id}`
|
||||
}
|
||||
/**
|
||||
* 组件即将卸载勾子
|
||||
*/
|
||||
componentWillUnmount() {
|
||||
if (super.componentWillUnmount) {
|
||||
super.componentWillUnmount()
|
||||
}
|
||||
if (isActive()) {
|
||||
disconnect(this.targetId())
|
||||
}
|
||||
this.unmountVueComponent()
|
||||
}
|
||||
/**
|
||||
* 卸载vue
|
||||
* @returns
|
||||
*/
|
||||
protected unmountVueComponent() {
|
||||
if (this.customLineApp) {
|
||||
this.customLineApp.unmount()
|
||||
this.customLineApp = null
|
||||
}
|
||||
if (this.root) {
|
||||
this.root.innerHTML = ''
|
||||
}
|
||||
return this.root
|
||||
}
|
||||
|
||||
getEdge() {
|
||||
const { model } = this.props
|
||||
const id = model.id
|
||||
const { customWidth = DEFAULT_WIDTH, customHeight = DEFAULT_HEIGHT } = model.getProperties()
|
||||
const { startPoint, endPoint, path, isAnimation, arrowConfig } = model
|
||||
const animationStyle = model.getEdgeAnimationStyle()
|
||||
const {
|
||||
strokeDasharray,
|
||||
stroke,
|
||||
strokeDashoffset,
|
||||
animationName,
|
||||
animationDuration,
|
||||
animationIterationCount,
|
||||
animationTimingFunction,
|
||||
animationDirection
|
||||
} = animationStyle
|
||||
const positionData = {
|
||||
x: (startPoint.x + endPoint.x - customWidth) / 2,
|
||||
y: (startPoint.y + endPoint.y - customHeight) / 2,
|
||||
width: customWidth,
|
||||
height: customHeight
|
||||
}
|
||||
const style = model.getEdgeStyle()
|
||||
const wrapperStyle = {
|
||||
width: customWidth,
|
||||
height: customHeight
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
const s = document.getElementById(id)
|
||||
if (s && !this.isMounted) {
|
||||
this.isMounted = true
|
||||
this.renderVueComponent(s)
|
||||
}
|
||||
}, 0)
|
||||
|
||||
delete style.stroke
|
||||
|
||||
return h('g', {}, [
|
||||
h(
|
||||
'style' as any,
|
||||
{ type: 'text/css' },
|
||||
'.lf-edge{stroke:#afafaf}.lf-edge:hover{stroke: #3370FF;}'
|
||||
),
|
||||
h('path', {
|
||||
d: path,
|
||||
...style,
|
||||
...arrowConfig,
|
||||
...(isAnimation
|
||||
? {
|
||||
strokeDasharray,
|
||||
stroke,
|
||||
style: {
|
||||
strokeDashoffset,
|
||||
animationName,
|
||||
animationDuration,
|
||||
animationIterationCount,
|
||||
animationTimingFunction,
|
||||
animationDirection
|
||||
}
|
||||
}
|
||||
: {})
|
||||
}),
|
||||
h(
|
||||
'foreignObject',
|
||||
{
|
||||
...positionData,
|
||||
y: positionData.y + 5,
|
||||
x: positionData.x + 5,
|
||||
style: {}
|
||||
},
|
||||
[
|
||||
h('div', {
|
||||
id,
|
||||
style: { ...wrapperStyle },
|
||||
className: 'lf-custom-edge-wrapper'
|
||||
})
|
||||
]
|
||||
)
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
class CustomEdgeModel2 extends BezierEdgeModel {
|
||||
getArrowStyle() {
|
||||
const arrowStyle = super.getArrowStyle()
|
||||
arrowStyle.offset = 1
|
||||
arrowStyle.verticalLength = 0
|
||||
return arrowStyle
|
||||
}
|
||||
|
||||
getEdgeStyle() {
|
||||
const style = super.getEdgeStyle()
|
||||
// svg属性
|
||||
style.strokeWidth = 2
|
||||
style.stroke = '#BBBFC4'
|
||||
style.offset = 0
|
||||
return style
|
||||
}
|
||||
/**
|
||||
* 重写此方法,使保存数据是能带上锚点数据。
|
||||
*/
|
||||
getData() {
|
||||
const data: any = super.getData()
|
||||
if (data) {
|
||||
data.sourceAnchorId = this.sourceAnchorId
|
||||
data.targetAnchorId = this.targetAnchorId
|
||||
}
|
||||
return data
|
||||
}
|
||||
/**
|
||||
* 给边自定义方案,使其支持基于锚点的位置更新边的路径
|
||||
*/
|
||||
updatePathByAnchor() {
|
||||
// TODO
|
||||
const sourceNodeModel = this.graphModel.getNodeModelById(this.sourceNodeId)
|
||||
const sourceAnchor = sourceNodeModel
|
||||
.getDefaultAnchor()
|
||||
.find((anchor: any) => anchor.id === this.sourceAnchorId)
|
||||
|
||||
const targetNodeModel = this.graphModel.getNodeModelById(this.targetNodeId)
|
||||
const targetAnchor = targetNodeModel
|
||||
.getDefaultAnchor()
|
||||
.find((anchor: any) => anchor.id === this.targetAnchorId)
|
||||
if (sourceAnchor && targetAnchor) {
|
||||
const startPoint = {
|
||||
x: sourceAnchor.x,
|
||||
y: sourceAnchor.y
|
||||
}
|
||||
this.updateStartPoint(startPoint)
|
||||
const endPoint = {
|
||||
x: targetAnchor.x,
|
||||
y: targetAnchor.y
|
||||
}
|
||||
|
||||
this.updateEndPoint(endPoint)
|
||||
}
|
||||
|
||||
// 这里需要将原有的pointsList设置为空,才能触发bezier的自动计算control点。
|
||||
this.pointsList = []
|
||||
this.initPoints()
|
||||
}
|
||||
setAttributes(): void {
|
||||
super.setAttributes()
|
||||
this.isHitable = true
|
||||
this.zIndex = 0
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
type: 'app-edge',
|
||||
view: CustomEdge2,
|
||||
model: CustomEdgeModel2
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
import type LogicFlow from '@logicflow/core'
|
||||
import { type GraphModel } from '@logicflow/core'
|
||||
import { MsgSuccess, MsgError, MsgConfirm } from '@/utils/message'
|
||||
import { WorkflowType } from '@/enums/workflow'
|
||||
import { t } from '@/locales'
|
||||
let selected: any | null = null
|
||||
|
||||
function translationNodeData(nodeData: any, distance: any) {
|
||||
nodeData.x += distance
|
||||
nodeData.y += distance
|
||||
if (nodeData.text) {
|
||||
nodeData.text.x += distance
|
||||
nodeData.text.y += distance
|
||||
}
|
||||
return nodeData
|
||||
}
|
||||
|
||||
function translationEdgeData(edgeData: any, distance: any) {
|
||||
if (edgeData.startPoint) {
|
||||
edgeData.startPoint.x += distance
|
||||
edgeData.startPoint.y += distance
|
||||
}
|
||||
if (edgeData.endPoint) {
|
||||
edgeData.endPoint.x += distance
|
||||
edgeData.endPoint.y += distance
|
||||
}
|
||||
if (edgeData.pointsList && edgeData.pointsList.length > 0) {
|
||||
edgeData.pointsList.forEach((point: any) => {
|
||||
point.x += distance
|
||||
point.y += distance
|
||||
})
|
||||
}
|
||||
if (edgeData.text) {
|
||||
edgeData.text.x += distance
|
||||
edgeData.text.y += distance
|
||||
}
|
||||
return edgeData
|
||||
}
|
||||
|
||||
const TRANSLATION_DISTANCE = 40
|
||||
let CHILDREN_TRANSLATION_DISTANCE = 40
|
||||
|
||||
export function initDefaultShortcut(lf: LogicFlow, graph: GraphModel) {
|
||||
const { keyboard } = lf
|
||||
const {
|
||||
options: { keyboard: keyboardOptions }
|
||||
} = keyboard
|
||||
const copy_node = () => {
|
||||
CHILDREN_TRANSLATION_DISTANCE = TRANSLATION_DISTANCE
|
||||
if (!keyboardOptions?.enabled) return true
|
||||
if (graph.textEditElement) return true
|
||||
const { guards } = lf.options
|
||||
const elements = graph.getSelectElements(false)
|
||||
const enabledClone = guards && guards.beforeClone ? guards.beforeClone(elements) : true
|
||||
if (!enabledClone || (elements.nodes.length === 0 && elements.edges.length === 0)) {
|
||||
selected = null
|
||||
return true
|
||||
}
|
||||
const base_nodes = elements.nodes.filter(
|
||||
(node: any) => node.type === WorkflowType.Start || node.type === WorkflowType.Base
|
||||
)
|
||||
if (base_nodes.length > 0) {
|
||||
MsgError(base_nodes[0]?.properties?.stepName + t('views.applicationWorkflow.tip.cannotCopy'))
|
||||
return
|
||||
}
|
||||
selected = elements
|
||||
selected.nodes.forEach((node: any) => translationNodeData(node, TRANSLATION_DISTANCE))
|
||||
selected.edges.forEach((edge: any) => translationEdgeData(edge, TRANSLATION_DISTANCE))
|
||||
MsgSuccess(t('views.applicationWorkflow.tip.copyError'))
|
||||
return false
|
||||
}
|
||||
const paste_node = () => {
|
||||
if (!keyboardOptions?.enabled) return true
|
||||
if (graph.textEditElement) return true
|
||||
if (selected && (selected.nodes || selected.edges)) {
|
||||
lf.clearSelectElements()
|
||||
const addElements = lf.addElements(selected, CHILDREN_TRANSLATION_DISTANCE)
|
||||
if (!addElements) return true
|
||||
addElements.nodes.forEach((node) => lf.selectElementById(node.id, true))
|
||||
addElements.edges.forEach((edge) => lf.selectElementById(edge.id, true))
|
||||
selected.nodes.forEach((node: any) => translationNodeData(node, TRANSLATION_DISTANCE))
|
||||
selected.edges.forEach((edge: any) => translationEdgeData(edge, TRANSLATION_DISTANCE))
|
||||
CHILDREN_TRANSLATION_DISTANCE = CHILDREN_TRANSLATION_DISTANCE + TRANSLATION_DISTANCE
|
||||
}
|
||||
return false
|
||||
}
|
||||
const delete_node = () => {
|
||||
const elements = graph.getSelectElements(true)
|
||||
lf.clearSelectElements()
|
||||
if (elements.nodes.length == 0 && elements.edges.length == 0) {
|
||||
return
|
||||
}
|
||||
if (elements.edges.length > 0 && elements.nodes.length == 0) {
|
||||
elements.edges.forEach((edge: any) => lf.deleteEdge(edge.id))
|
||||
return
|
||||
}
|
||||
const nodes = elements.nodes.filter((node) => ['start-node', 'base-node'].includes(node.type))
|
||||
if (nodes.length > 0) {
|
||||
MsgError(`${nodes[0].properties?.stepName}${t('views.applicationWorkflow.delete.deleteMessage')}`)
|
||||
return
|
||||
}
|
||||
MsgConfirm(t('common.tip'), t('views.applicationWorkflow.delete.confirmTitle'), {
|
||||
confirmButtonText: t('common.confirm'),
|
||||
confirmButtonClass: 'danger'
|
||||
}).then(() => {
|
||||
if (!keyboardOptions?.enabled) return true
|
||||
if (graph.textEditElement) return true
|
||||
|
||||
elements.edges.forEach((edge: any) => lf.deleteEdge(edge.id))
|
||||
elements.nodes.forEach((node: any) => lf.deleteNode(node.id))
|
||||
})
|
||||
|
||||
return false
|
||||
}
|
||||
graph.eventCenter.on('copy_node', copy_node)
|
||||
// 复制
|
||||
keyboard.on(['cmd + c', 'ctrl + c'], copy_node)
|
||||
// 粘贴
|
||||
keyboard.on(['cmd + v', 'ctrl + v'], paste_node)
|
||||
// undo
|
||||
keyboard.on(['cmd + z', 'ctrl + z'], () => {
|
||||
// if (!keyboardOptions?.enabled) return true
|
||||
// if (graph.textEditElement) return true
|
||||
// lf.undo()
|
||||
// return false
|
||||
})
|
||||
// redo
|
||||
keyboard.on(['cmd + y', 'ctrl + y'], () => {
|
||||
if (!keyboardOptions?.enabled) return true
|
||||
if (graph.textEditElement) return true
|
||||
lf.redo()
|
||||
return false
|
||||
})
|
||||
// delete
|
||||
keyboard.on(['backspace'], delete_node)
|
||||
}
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
import { BaseEdgeModel, BaseNodeModel, GraphModel } from '@logicflow/core'
|
||||
import { defineComponent, h, reactive, isVue3, Teleport, markRaw, Fragment } from 'vue-demi'
|
||||
|
||||
let active = false
|
||||
const items = reactive<{ [key: string]: any }>({})
|
||||
|
||||
export function connect(
|
||||
id: string,
|
||||
component: any,
|
||||
container: HTMLDivElement,
|
||||
node: BaseNodeModel | BaseEdgeModel,
|
||||
graph: GraphModel,
|
||||
get_props?: any
|
||||
) {
|
||||
if (!get_props) {
|
||||
get_props = (node: BaseNodeModel | BaseEdgeModel, graph: GraphModel) => {
|
||||
return { nodeModel: node, graph }
|
||||
}
|
||||
}
|
||||
if (active) {
|
||||
items[id] = markRaw(
|
||||
defineComponent({
|
||||
render: () => h(Teleport, { to: container } as any, [h(component, get_props(node, graph))]),
|
||||
provide: () => ({
|
||||
getNode: () => node,
|
||||
getGraph: () => graph
|
||||
})
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function disconnect(id: string) {
|
||||
if (active) {
|
||||
delete items[id]
|
||||
}
|
||||
}
|
||||
|
||||
export function isActive() {
|
||||
return active
|
||||
}
|
||||
|
||||
export function getTeleport(): any {
|
||||
if (!isVue3) {
|
||||
throw new Error('teleport is only available in Vue3')
|
||||
}
|
||||
active = true
|
||||
|
||||
return defineComponent({
|
||||
props: {
|
||||
flowId: {
|
||||
type: String,
|
||||
required: true
|
||||
}
|
||||
},
|
||||
setup(props) {
|
||||
return () => {
|
||||
const children: Record<string, any>[] = []
|
||||
Object.keys(items).forEach((id) => {
|
||||
// https://github.com/didi/LogicFlow/issues/1768
|
||||
// 多个不同的VueNodeView都会connect注册到items中,因此items存储了可能有多个flowId流程图的数据
|
||||
// 当使用多个LogicFlow时,会创建多个flowId + 同时使用KeepAlive
|
||||
// 每一次items改变,会触发不同flowId持有的setup()执行,由于每次setup()执行就是遍历items,因此存在多次重复渲染元素的问题
|
||||
// 即items[0]会在Page1的setup()执行,items[0]也会在Page2的setup()执行,从而生成两个items[0]
|
||||
|
||||
// 比对当前界面显示的flowId,只更新items[当前页面flowId:nodeId]的数据
|
||||
// 比如items[0]属于Page1的数据,那么Page2无论active=true/false,都无法执行items[0]
|
||||
if (id.startsWith(props.flowId)) {
|
||||
children.push(items[id])
|
||||
}
|
||||
})
|
||||
return h(
|
||||
Fragment,
|
||||
{},
|
||||
children.map((item) => h(item))
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,150 @@
|
|||
import { WorkflowType } from '@/enums/workflow'
|
||||
import { t } from '@/locales'
|
||||
|
||||
const end_nodes: Array<string> = [
|
||||
WorkflowType.AiChat,
|
||||
WorkflowType.Reply,
|
||||
WorkflowType.FunctionLib,
|
||||
WorkflowType.FunctionLibCustom,
|
||||
WorkflowType.ImageUnderstandNode,
|
||||
WorkflowType.Application,
|
||||
WorkflowType.SpeechToTextNode,
|
||||
WorkflowType.TextToSpeechNode,
|
||||
WorkflowType.ImageGenerateNode,
|
||||
]
|
||||
export class WorkFlowInstance {
|
||||
nodes
|
||||
edges
|
||||
workFlowNodes: Array<any>
|
||||
constructor(workflow: { nodes: Array<any>; edges: Array<any> }) {
|
||||
this.nodes = workflow.nodes
|
||||
this.edges = workflow.edges
|
||||
this.workFlowNodes = []
|
||||
}
|
||||
/**
|
||||
* 校验开始节点
|
||||
*/
|
||||
private is_valid_start_node() {
|
||||
const start_node_list = this.nodes.filter((item) => item.id === WorkflowType.Start)
|
||||
if (start_node_list.length == 0) {
|
||||
throw t('views.applicationWorkflow.validate.startNodeRequired')
|
||||
} else if (start_node_list.length > 1) {
|
||||
throw t('views.applicationWorkflow.validate.startNodeOnly')
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 校验基本信息节点
|
||||
*/
|
||||
private is_valid_base_node() {
|
||||
const start_node_list = this.nodes.filter((item) => item.id === WorkflowType.Base)
|
||||
if (start_node_list.length == 0) {
|
||||
throw t('views.applicationWorkflow.validate.baseNodeRequired')
|
||||
} else if (start_node_list.length > 1) {
|
||||
throw t('views.applicationWorkflow.validate.baseNodeOnly')
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 校验节点
|
||||
*/
|
||||
is_valid() {
|
||||
this.is_valid_start_node()
|
||||
this.is_valid_base_node()
|
||||
this.is_valid_work_flow()
|
||||
this.is_valid_nodes()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取开始节点
|
||||
* @returns
|
||||
*/
|
||||
get_start_node() {
|
||||
const start_node_list = this.nodes.filter((item) => item.id === WorkflowType.Start)
|
||||
return start_node_list[0]
|
||||
}
|
||||
/**
|
||||
* 获取基本节点
|
||||
* @returns 基本节点
|
||||
*/
|
||||
get_base_node() {
|
||||
const base_node_list = this.nodes.filter((item) => item.id === WorkflowType.Base)
|
||||
return base_node_list[0]
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验工作流
|
||||
* @param up_node 上一个节点
|
||||
*/
|
||||
private _is_valid_work_flow(up_node?: any) {
|
||||
if (!up_node) {
|
||||
up_node = this.get_start_node()
|
||||
}
|
||||
this.workFlowNodes.push(up_node)
|
||||
this.is_valid_node(up_node)
|
||||
const next_nodes = this.get_next_nodes(up_node)
|
||||
for (const next_node of next_nodes) {
|
||||
this._is_valid_work_flow(next_node)
|
||||
}
|
||||
}
|
||||
private is_valid_work_flow() {
|
||||
this.workFlowNodes = []
|
||||
this._is_valid_work_flow()
|
||||
const notInWorkFlowNodes = this.nodes
|
||||
.filter((node: any) => node.id !== WorkflowType.Start && node.id !== WorkflowType.Base)
|
||||
.filter((node) => !this.workFlowNodes.includes(node))
|
||||
if (notInWorkFlowNodes.length > 0) {
|
||||
throw `${t('views.applicationWorkflow.validate.notInWorkFlowNode')}:${notInWorkFlowNodes.map((node) => node.properties.stepName).join(',')}`
|
||||
}
|
||||
this.workFlowNodes = []
|
||||
}
|
||||
/**
|
||||
* 获取流程下一个节点列表
|
||||
* @param node 节点
|
||||
* @returns 节点列表
|
||||
*/
|
||||
private get_next_nodes(node: any) {
|
||||
const edge_list = this.edges.filter((edge) => edge.sourceNodeId == node.id)
|
||||
const node_list = edge_list
|
||||
.map((edge) => this.nodes.filter((node) => node.id == edge.targetNodeId))
|
||||
.reduce((x, y) => [...x, ...y], [])
|
||||
if (node_list.length == 0 && !end_nodes.includes(node.type)) {
|
||||
throw t('views.applicationWorkflow.validate.noNextNode')
|
||||
}
|
||||
return node_list
|
||||
}
|
||||
private is_valid_nodes() {
|
||||
for (const node of this.nodes) {
|
||||
if (node.type !== WorkflowType.Base && node.type !== WorkflowType.Start) {
|
||||
if (!this.edges.some((edge) => edge.targetNodeId === node.id)) {
|
||||
throw `${t('views.applicationWorkflow.validate.notInWorkFlowNode')}:${node.properties.stepName}`
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 校验节点
|
||||
* @param node 节点
|
||||
*/
|
||||
private is_valid_node(node: any) {
|
||||
if (node.properties.status && node.properties.status === 500) {
|
||||
throw `${node.properties.stepName} ${t('views.applicationWorkflow.validate.nodeUnavailable')}`
|
||||
}
|
||||
if (node.type === WorkflowType.Condition) {
|
||||
const branch_list = node.properties.node_data.branch
|
||||
for (const branch of branch_list) {
|
||||
const source_anchor_id = `${node.id}_${branch.id}_right`
|
||||
const edge_list = this.edges.filter((edge) => edge.sourceAnchorId == source_anchor_id)
|
||||
if (edge_list.length == 0) {
|
||||
throw `${node.properties.stepName} ${t('views.applicationWorkflow.validate.needConnect1')}${branch.type}${t('views.applicationWorkflow.validate.needConnect2')}`
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const edge_list = this.edges.filter((edge) => edge.sourceNodeId == node.id)
|
||||
if (edge_list.length == 0 && !end_nodes.includes(node.type)) {
|
||||
throw `${node.properties.stepName} ${t('views.applicationWorkflow.validate.cannotEndNode')}`
|
||||
}
|
||||
}
|
||||
if (node.properties.status && node.properties.status !== 200) {
|
||||
throw `${node.properties.stepName} ${t('views.applicationWorkflow.validate.nodeUnavailable')}`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<AppAvatar class="avatar-gradient" shape="square">
|
||||
<img src="@/assets/icon_robot.svg" style="width: 75%" alt="" />
|
||||
</AppAvatar>
|
||||
</template>
|
||||
<script setup lang="ts"></script>
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
<template>
|
||||
<AppAvatar
|
||||
v-if="isAppIcon(item?.icon)"
|
||||
shape="square"
|
||||
:size="32"
|
||||
style="background: none"
|
||||
class="mr-8"
|
||||
>
|
||||
<img :src="item?.icon" alt="" />
|
||||
</AppAvatar>
|
||||
<AppAvatar
|
||||
v-else-if="item?.name"
|
||||
:name="item?.name"
|
||||
pinyinColor
|
||||
shape="square"
|
||||
:size="32"
|
||||
class="mr-8"
|
||||
/>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { isAppIcon } from '@/utils/common'
|
||||
const props = defineProps<{
|
||||
item: {
|
||||
name: string
|
||||
icon: string
|
||||
}
|
||||
}>()
|
||||
</script>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<AppAvatar shape="square" style="background: #FF8800;">
|
||||
<img src="@/assets/icon_hi.svg" style="width: 75%" alt="" />
|
||||
</AppAvatar>
|
||||
</template>
|
||||
<script setup lang="ts"></script>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<AppAvatar shape="square" style="background: #14C0FF;">
|
||||
<img src="@/assets/icon_condition.svg" style="width: 75%" alt="" />
|
||||
</AppAvatar>
|
||||
</template>
|
||||
<script setup lang="ts"></script>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<AppAvatar shape="square" class="avatar-blue">
|
||||
<img src="@/assets/icon_docs.svg" style="width: 65%" alt="" />
|
||||
</AppAvatar>
|
||||
</template>
|
||||
<script setup lang="ts"></script>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<AppAvatar shape="square" style="background: #34c724">
|
||||
<img src="@/assets/icon_form.svg" style="width: 75%" alt="" />
|
||||
</AppAvatar>
|
||||
</template>
|
||||
<script setup lang="ts"></script>
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<template>
|
||||
<AppAvatar
|
||||
v-if="isAppIcon(item?.icon)"
|
||||
shape="square"
|
||||
:size="32"
|
||||
style="background: none"
|
||||
class="mr-8"
|
||||
>
|
||||
<img :src="item?.icon" alt="" />
|
||||
</AppAvatar>
|
||||
<AppAvatar v-else shape="square" style="background: #34c724">
|
||||
<img src="@/assets/icon_function_outlined.svg" style="width: 75%" alt="" />
|
||||
</AppAvatar>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { isAppIcon } from '@/utils/common'
|
||||
const props = defineProps<{
|
||||
item?: {
|
||||
name: string
|
||||
icon: string
|
||||
}
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<script setup lang="ts"></script>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<AppAvatar shape="square" style="background: #34c724">
|
||||
<img src="@/assets/icon_function_outlined.svg" style="width: 75%" alt="" />
|
||||
</AppAvatar>
|
||||
</template>
|
||||
<script setup lang="ts"></script>
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
<template>
|
||||
<img src="@/assets/icon_globe_color.svg" style="width: 18px" alt="" />
|
||||
</template>
|
||||
<script setup lang="ts"></script>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<AppAvatar shape="square" style="background: #FF8800;">
|
||||
<img src="@/assets/icon_text-image.svg" style="width: 65%" alt="" />
|
||||
</AppAvatar>
|
||||
</template>
|
||||
<script setup lang="ts"></script>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<AppAvatar shape="square" style="background: #14C0FF;">
|
||||
<img src="@/assets/icon_image.svg" style="width: 65%" alt="" />
|
||||
</AppAvatar>
|
||||
</template>
|
||||
<script setup lang="ts"></script>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<AppAvatar shape="square" style="background: #34c724">
|
||||
<img src="@/assets/icon_mcp.svg" style="width: 65%" alt="" />
|
||||
</AppAvatar>
|
||||
</template>
|
||||
<script setup lang="ts"></script>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<AppAvatar shape="square" style="background: #34C724">
|
||||
<img src="@/assets/icon_setting.svg" style="width: 65%" alt="" />
|
||||
</AppAvatar>
|
||||
</template>
|
||||
<script setup lang="ts"></script>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<AppAvatar shape="square" style="background: #FF8800">
|
||||
<img src="@/assets/icon_reply.svg" style="width: 65%" alt="" />
|
||||
</AppAvatar>
|
||||
</template>
|
||||
<script setup lang="ts"></script>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<AppAvatar shape="square" style="background: #7F3BF5">
|
||||
<img src="@/assets/icon_reranker.svg" style="width: 65%" alt="" />
|
||||
</AppAvatar>
|
||||
</template>
|
||||
<script setup lang="ts"></script>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<AppAvatar shape="square" class="avatar-blue">
|
||||
<img src="@/assets/icon_document.svg" style="width: 58%" alt="" />
|
||||
</AppAvatar>
|
||||
</template>
|
||||
<script setup lang="ts"></script>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<AppAvatar shape="square" style="background: #ff8800">
|
||||
<img src="@/assets/icon_speech_to_text.svg" style="width: 100%" alt="" />
|
||||
</AppAvatar>
|
||||
</template>
|
||||
<script setup lang="ts"></script>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<AppAvatar shape="square" style="background: #D136D1;">
|
||||
<img src="@/assets/icon_start.svg" style="width: 75%" alt="" />
|
||||
</AppAvatar>
|
||||
</template>
|
||||
<script setup lang="ts"></script>
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<AppAvatar shape="square" style="background: #14c0ff">
|
||||
<img src="@/assets/icon_text_to_speech.svg" style="width: 100%" alt="" />
|
||||
</AppAvatar>
|
||||
</template>
|
||||
<script setup lang="ts"></script>
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
const icons: any = import.meta.glob('./**.vue', { eager: true })
|
||||
export function iconComponent(name: string) {
|
||||
const url = `./${name}.vue`
|
||||
return icons[url]?.default || null
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<template>
|
||||
<AppAvatar shape="square" class="avatar-blue">
|
||||
<img src="@/assets/icon_assigner.svg" style="width: 65%" alt="" />
|
||||
</AppAvatar>
|
||||
</template>
|
||||
<script setup lang="ts"></script>
|
||||
|
|
@ -0,0 +1,178 @@
|
|||
<template>
|
||||
<div className="workflow-app" id="container"></div>
|
||||
<!-- 辅助工具栏 -->
|
||||
<Control class="workflow-control" v-if="lf" :lf="lf"></Control>
|
||||
<TeleportContainer :flow-id="flowId" />
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import LogicFlow from '@logicflow/core'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import AppEdge from './common/edge'
|
||||
import Control from './common/NodeControl.vue'
|
||||
import { baseNodes } from '@/workflow/common/data'
|
||||
import '@logicflow/extension/lib/style/index.css'
|
||||
import '@logicflow/core/dist/style/index.css'
|
||||
import { initDefaultShortcut } from '@/workflow/common/shortcut'
|
||||
import Dagre from '@/workflow/plugins/dagre'
|
||||
import { getTeleport } from '@/workflow/common/teleport'
|
||||
const nodes: any = import.meta.glob('./nodes/**/index.ts', { eager: true })
|
||||
|
||||
defineOptions({ name: 'WorkFlow' })
|
||||
const TeleportContainer = getTeleport()
|
||||
const flowId = ref('')
|
||||
type ShapeItem = {
|
||||
type?: string
|
||||
text?: string
|
||||
icon?: string
|
||||
label?: string
|
||||
className?: string
|
||||
disabled?: boolean
|
||||
properties?: Record<string, any>
|
||||
callback?: (lf: LogicFlow, container?: HTMLElement) => void
|
||||
}
|
||||
|
||||
const props = defineProps({
|
||||
data: Object || null
|
||||
})
|
||||
|
||||
const defaultData = {
|
||||
nodes: [...baseNodes]
|
||||
}
|
||||
const graphData = computed({
|
||||
get: () => {
|
||||
if (props.data) {
|
||||
return props.data
|
||||
} else {
|
||||
return defaultData
|
||||
}
|
||||
},
|
||||
set: (value) => {
|
||||
return value
|
||||
}
|
||||
})
|
||||
|
||||
const lf = ref()
|
||||
onMounted(() => {
|
||||
renderGraphData()
|
||||
})
|
||||
const render = (data: any) => {
|
||||
lf.value.render(data)
|
||||
}
|
||||
const renderGraphData = (data?: any) => {
|
||||
const container: any = document.querySelector('#container')
|
||||
if (container) {
|
||||
lf.value = new LogicFlow({
|
||||
plugins: [Dagre],
|
||||
textEdit: false,
|
||||
adjustEdge: false,
|
||||
adjustEdgeStartAndEnd: false,
|
||||
background: {
|
||||
backgroundColor: '#f5f6f7'
|
||||
},
|
||||
grid: {
|
||||
size: 10,
|
||||
type: 'dot',
|
||||
config: {
|
||||
color: '#DEE0E3',
|
||||
thickness: 1
|
||||
}
|
||||
},
|
||||
keyboard: {
|
||||
enabled: true
|
||||
},
|
||||
isSilentMode: false,
|
||||
container: container
|
||||
})
|
||||
lf.value.setTheme({
|
||||
bezier: {
|
||||
stroke: '#afafaf',
|
||||
strokeWidth: 1
|
||||
}
|
||||
})
|
||||
lf.value.on('graph:rendered', () => {
|
||||
flowId.value = lf.value.graphModel.flowId
|
||||
})
|
||||
initDefaultShortcut(lf.value, lf.value.graphModel)
|
||||
lf.value.batchRegister([...Object.keys(nodes).map((key) => nodes[key].default), AppEdge])
|
||||
lf.value.setDefaultEdgeType('app-edge')
|
||||
|
||||
lf.value.render(data ? data : {})
|
||||
|
||||
lf.value.graphModel.eventCenter.on('delete_edge', (id_list: Array<string>) => {
|
||||
id_list.forEach((id: string) => {
|
||||
lf.value.deleteEdge(id)
|
||||
})
|
||||
})
|
||||
lf.value.graphModel.eventCenter.on('anchor:drop', (data: any) => {
|
||||
// 清除当前节点下面的子节点的所有缓存
|
||||
data.nodeModel.clear_next_node_field(false)
|
||||
})
|
||||
|
||||
setTimeout(() => {
|
||||
lf.value?.fitView()
|
||||
}, 500)
|
||||
}
|
||||
}
|
||||
const validate = () => {
|
||||
return Promise.all(lf.value.graphModel.nodes.map((element: any) => element?.validate?.()))
|
||||
}
|
||||
const getGraphData = () => {
|
||||
return lf.value.getGraphData()
|
||||
}
|
||||
|
||||
const onmousedown = (shapeItem: ShapeItem) => {
|
||||
if (shapeItem.type) {
|
||||
lf.value.dnd.startDrag({
|
||||
type: shapeItem.type,
|
||||
properties: { ...shapeItem.properties }
|
||||
})
|
||||
}
|
||||
if (shapeItem.callback) {
|
||||
shapeItem.callback(lf.value)
|
||||
}
|
||||
}
|
||||
const addNode = (shapeItem: ShapeItem) => {
|
||||
lf.value.clearSelectElements()
|
||||
const { virtualRectCenterPositionX, virtualRectCenterPositionY } =
|
||||
lf.value.graphModel.getVirtualRectSize()
|
||||
const newNode = lf.value.graphModel.addNode({
|
||||
type: shapeItem.type,
|
||||
properties: shapeItem.properties,
|
||||
x: virtualRectCenterPositionX,
|
||||
y: virtualRectCenterPositionY - lf.value.graphModel.height / 2
|
||||
})
|
||||
newNode.isSelected = true
|
||||
newNode.isHovered = true
|
||||
lf.value.toFront(newNode.id)
|
||||
}
|
||||
|
||||
const clearGraphData = () => {
|
||||
return lf.value.clearData()
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
onmousedown,
|
||||
validate,
|
||||
getGraphData,
|
||||
addNode,
|
||||
clearGraphData,
|
||||
renderGraphData,
|
||||
render
|
||||
})
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.workflow-app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
.workflow-control {
|
||||
position: absolute;
|
||||
bottom: 24px;
|
||||
left: 24px;
|
||||
z-index: 2;
|
||||
}
|
||||
.lf-drag-able {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import ChatNodeVue from './index.vue'
|
||||
import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
|
||||
class ChatNode extends AppNode {
|
||||
constructor(props: any) {
|
||||
super(props, ChatNodeVue)
|
||||
}
|
||||
}
|
||||
export default {
|
||||
type: 'ai-chat-node',
|
||||
model: AppNodeModel,
|
||||
view: ChatNode
|
||||
}
|
||||
|
|
@ -0,0 +1,338 @@
|
|||
<template>
|
||||
<NodeContainer :nodeModel="nodeModel">
|
||||
<h5 class="title-decoration-1 mb-8">{{ $t('views.applicationWorkflow.nodeSetting') }}</h5>
|
||||
<el-card shadow="never" class="card-never" style="--el-card-padding: 12px">
|
||||
<el-form
|
||||
@submit.prevent
|
||||
:model="chat_data"
|
||||
label-position="top"
|
||||
require-asterisk-position="right"
|
||||
label-width="auto"
|
||||
ref="aiChatNodeFormRef"
|
||||
hide-required-asterisk
|
||||
>
|
||||
<el-form-item
|
||||
:label="$t('views.application.applicationForm.form.aiModel.label')"
|
||||
prop="model_id"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: $t('views.application.applicationForm.form.aiModel.placeholder'),
|
||||
trigger: 'change'
|
||||
}"
|
||||
>
|
||||
<template #label>
|
||||
<div class="flex-between w-full">
|
||||
<div>
|
||||
<span
|
||||
>{{ $t('views.application.applicationForm.form.aiModel.label')
|
||||
}}<span class="danger">*</span></span
|
||||
>
|
||||
</div>
|
||||
|
||||
<el-button
|
||||
:disabled="!chat_data.model_id"
|
||||
type="primary"
|
||||
link
|
||||
@click="openAIParamSettingDialog(chat_data.model_id)"
|
||||
@refreshForm="refreshParam"
|
||||
>
|
||||
<el-icon><Setting /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<ModelSelect
|
||||
@change="model_change"
|
||||
@wheel="wheel"
|
||||
:teleported="false"
|
||||
v-model="chat_data.model_id"
|
||||
:placeholder="$t('views.application.applicationForm.form.aiModel.placeholder')"
|
||||
:options="modelOptions"
|
||||
@submitModel="getModel"
|
||||
showFooter
|
||||
:model-type="'LLM'"
|
||||
></ModelSelect>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('views.application.applicationForm.form.roleSettings.label')">
|
||||
<MdEditorMagnify
|
||||
:title="$t('views.application.applicationForm.form.roleSettings.label')"
|
||||
v-model="chat_data.system"
|
||||
style="height: 100px"
|
||||
@submitDialog="submitSystemDialog"
|
||||
:placeholder="$t('views.application.applicationForm.form.roleSettings.label')"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
:label="$t('views.application.applicationForm.form.prompt.label')"
|
||||
prop="prompt"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: $t('views.application.applicationForm.form.prompt.requiredMessage'),
|
||||
trigger: 'blur'
|
||||
}"
|
||||
>
|
||||
<template #label>
|
||||
<div class="flex align-center">
|
||||
<div class="mr-4">
|
||||
<span
|
||||
>{{ $t('views.application.applicationForm.form.prompt.label')
|
||||
}}<span class="danger">*</span></span
|
||||
>
|
||||
</div>
|
||||
<el-tooltip effect="dark" placement="right" popper-class="max-w-200">
|
||||
<template #content
|
||||
>{{ $t('views.application.applicationForm.form.prompt.tooltip') }}
|
||||
</template>
|
||||
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<MdEditorMagnify
|
||||
@wheel="wheel"
|
||||
:title="$t('views.application.applicationForm.form.prompt.label')"
|
||||
v-model="chat_data.prompt"
|
||||
style="height: 150px"
|
||||
@submitDialog="submitDialog"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('views.application.applicationForm.form.historyRecord.label')">
|
||||
<template #label>
|
||||
<div class="flex-between">
|
||||
<div>{{ $t('views.application.applicationForm.form.historyRecord.label') }}</div>
|
||||
<el-select v-model="chat_data.dialogue_type" type="small" style="width: 100px">
|
||||
<el-option :label="$t('views.applicationWorkflow.node')" value="NODE" />
|
||||
<el-option :label="$t('views.applicationWorkflow.workflow')" value="WORKFLOW" />
|
||||
</el-select>
|
||||
</div>
|
||||
</template>
|
||||
<el-input-number
|
||||
v-model="chat_data.dialogue_number"
|
||||
:min="0"
|
||||
:value-on-clear="0"
|
||||
controls-position="right"
|
||||
class="w-full"
|
||||
:step="1"
|
||||
:step-strictly="true"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<div class="flex-between mb-16">
|
||||
<div class="lighter">{{ $t('views.applicationWorkflow.nodes.mcpNode.tool') }}</div>
|
||||
<el-button type="primary" link @click="openMcpServersDialog" @refreshForm="refreshParam">
|
||||
<el-icon><Setting /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<el-form-item @click.prevent>
|
||||
<template #label>
|
||||
<div class="flex-between w-full">
|
||||
<div>
|
||||
<span>{{
|
||||
$t('views.application.applicationForm.form.reasoningContent.label')
|
||||
}}</span>
|
||||
</div>
|
||||
<el-button
|
||||
type="primary"
|
||||
link
|
||||
@click="openReasoningParamSettingDialog"
|
||||
@refreshForm="refreshParam"
|
||||
>
|
||||
<el-icon><Setting /></el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</template>
|
||||
<el-switch size="small" v-model="chat_data.model_setting.reasoning_content_enable" />
|
||||
</el-form-item>
|
||||
<el-form-item @click.prevent>
|
||||
<template #label>
|
||||
<div class="flex align-center">
|
||||
<div class="mr-4">
|
||||
<span>{{
|
||||
$t('views.applicationWorkflow.nodes.aiChatNode.returnContent.label')
|
||||
}}</span>
|
||||
</div>
|
||||
<el-tooltip effect="dark" placement="right" popper-class="max-w-200">
|
||||
<template #content>
|
||||
{{ $t('views.applicationWorkflow.nodes.aiChatNode.returnContent.tooltip') }}
|
||||
</template>
|
||||
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-switch size="small" v-model="chat_data.is_result" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
|
||||
<AIModeParamSettingDialog ref="AIModeParamSettingDialogRef" @refresh="refreshParam" />
|
||||
<ReasoningParamSettingDialog
|
||||
ref="ReasoningParamSettingDialogRef"
|
||||
@refresh="submitReasoningDialog"
|
||||
/>
|
||||
<McpServersDialog ref="mcpServersDialogRef" @refresh="submitMcpServersDialog" />
|
||||
</NodeContainer>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { cloneDeep, set, groupBy } from 'lodash'
|
||||
import { app } from '@/main'
|
||||
import NodeContainer from '@/workflow/common/NodeContainer.vue'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import applicationApi from '@/api/application'
|
||||
import useStore from '@/stores'
|
||||
import { isLastNode } from '@/workflow/common/data'
|
||||
import AIModeParamSettingDialog from '@/views/application/component/AIModeParamSettingDialog.vue'
|
||||
import { t } from '@/locales'
|
||||
import ReasoningParamSettingDialog from '@/views/application/component/ReasoningParamSettingDialog.vue'
|
||||
import McpServersDialog from '@/views/application/component/McpServersDialog.vue'
|
||||
const { model } = useStore()
|
||||
|
||||
const wheel = (e: any) => {
|
||||
if (e.ctrlKey === true) {
|
||||
e.preventDefault()
|
||||
return true
|
||||
} else {
|
||||
e.stopPropagation()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function submitSystemDialog(val: string) {
|
||||
set(props.nodeModel.properties.node_data, 'system', val)
|
||||
}
|
||||
|
||||
function submitDialog(val: string) {
|
||||
set(props.nodeModel.properties.node_data, 'prompt', val)
|
||||
}
|
||||
|
||||
const model_change = (model_id?: string) => {
|
||||
if (model_id) {
|
||||
AIModeParamSettingDialogRef.value?.reset_default(model_id, id)
|
||||
} else {
|
||||
refreshParam({})
|
||||
}
|
||||
}
|
||||
const {
|
||||
params: { id }
|
||||
} = app.config.globalProperties.$route as any
|
||||
|
||||
// @ts-ignore
|
||||
const defaultPrompt = `${t('views.applicationWorkflow.nodes.aiChatNode.defaultPrompt')}:
|
||||
{{${t('views.applicationWorkflow.nodes.searchDatasetNode.label')}.data}}
|
||||
${t('views.problem.title')}:
|
||||
{{${t('views.applicationWorkflow.nodes.startNode.label')}.question}}`
|
||||
|
||||
const form = {
|
||||
model_id: '',
|
||||
system: '',
|
||||
prompt: defaultPrompt,
|
||||
dialogue_number: 1,
|
||||
is_result: false,
|
||||
temperature: null,
|
||||
max_tokens: null,
|
||||
dialogue_type: 'WORKFLOW',
|
||||
model_setting: {
|
||||
reasoning_content_start: '<think>',
|
||||
reasoning_content_end: '</think>',
|
||||
reasoning_content_enable: false
|
||||
}
|
||||
}
|
||||
|
||||
const chat_data = computed({
|
||||
get: () => {
|
||||
if (props.nodeModel.properties.node_data) {
|
||||
if (!props.nodeModel.properties.node_data.model_setting) {
|
||||
set(props.nodeModel.properties.node_data, 'model_setting', {
|
||||
reasoning_content_start: '<think>',
|
||||
reasoning_content_end: '</think>',
|
||||
reasoning_content_enable: false
|
||||
})
|
||||
}
|
||||
return props.nodeModel.properties.node_data
|
||||
} else {
|
||||
set(props.nodeModel.properties, 'node_data', form)
|
||||
}
|
||||
|
||||
return props.nodeModel.properties.node_data
|
||||
},
|
||||
set: (value) => {
|
||||
set(props.nodeModel.properties, 'node_data', value)
|
||||
}
|
||||
})
|
||||
const props = defineProps<{ nodeModel: any }>()
|
||||
|
||||
const aiChatNodeFormRef = ref<FormInstance>()
|
||||
|
||||
const modelOptions = ref<any>(null)
|
||||
const AIModeParamSettingDialogRef = ref<InstanceType<typeof AIModeParamSettingDialog>>()
|
||||
const ReasoningParamSettingDialogRef = ref<InstanceType<typeof ReasoningParamSettingDialog>>()
|
||||
const validate = () => {
|
||||
return aiChatNodeFormRef.value?.validate().catch((err) => {
|
||||
return Promise.reject({ node: props.nodeModel, errMessage: err })
|
||||
})
|
||||
}
|
||||
|
||||
function getModel() {
|
||||
if (id) {
|
||||
applicationApi.getApplicationModel(id).then((res: any) => {
|
||||
modelOptions.value = groupBy(res?.data, 'provider')
|
||||
})
|
||||
} else {
|
||||
model.asyncGetModel().then((res: any) => {
|
||||
modelOptions.value = groupBy(res?.data, 'provider')
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const openAIParamSettingDialog = (modelId: string) => {
|
||||
if (modelId) {
|
||||
AIModeParamSettingDialogRef.value?.open(modelId, id, chat_data.value.model_params_setting)
|
||||
}
|
||||
}
|
||||
|
||||
const openReasoningParamSettingDialog = () => {
|
||||
ReasoningParamSettingDialogRef.value?.open(chat_data.value.model_setting)
|
||||
}
|
||||
|
||||
function refreshParam(data: any) {
|
||||
set(props.nodeModel.properties.node_data, 'model_params_setting', data)
|
||||
}
|
||||
|
||||
function submitReasoningDialog(val: any) {
|
||||
let model_setting = cloneDeep(props.nodeModel.properties.node_data.model_setting)
|
||||
model_setting = {
|
||||
...model_setting,
|
||||
...val
|
||||
}
|
||||
|
||||
set(props.nodeModel.properties.node_data, 'model_setting', model_setting)
|
||||
}
|
||||
|
||||
const mcpServersDialogRef = ref()
|
||||
function openMcpServersDialog() {
|
||||
const config = {
|
||||
mcp_servers: chat_data.value.mcp_servers,
|
||||
mcp_enable: chat_data.value.mcp_enable
|
||||
}
|
||||
mcpServersDialogRef.value.open(config)
|
||||
}
|
||||
|
||||
function submitMcpServersDialog(config: any) {
|
||||
set(props.nodeModel.properties.node_data, 'mcp_servers', config.mcp_servers)
|
||||
set(props.nodeModel.properties.node_data, 'mcp_enable', config.mcp_enable)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
getModel()
|
||||
if (typeof props.nodeModel.properties.node_data?.is_result === 'undefined') {
|
||||
if (isLastNode(props.nodeModel)) {
|
||||
set(props.nodeModel.properties.node_data, 'is_result', true)
|
||||
}
|
||||
}
|
||||
set(props.nodeModel, 'validate', validate)
|
||||
if (!chat_data.value.dialogue_type) {
|
||||
chat_data.value.dialogue_type = 'WORKFLOW'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import ChatNodeVue from './index.vue'
|
||||
import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
|
||||
class ChatNode extends AppNode {
|
||||
constructor(props: any) {
|
||||
super(props, ChatNodeVue)
|
||||
}
|
||||
}
|
||||
export default {
|
||||
type: 'application-node',
|
||||
model: AppNodeModel,
|
||||
view: ChatNode
|
||||
}
|
||||
|
|
@ -0,0 +1,317 @@
|
|||
<template>
|
||||
<NodeContainer :nodeModel="nodeModel">
|
||||
<h5 class="title-decoration-1 mb-8">{{ $t('views.applicationWorkflow.nodeSetting') }}</h5>
|
||||
<el-card shadow="never" class="card-never">
|
||||
<el-form
|
||||
@submit.prevent
|
||||
:model="form_data"
|
||||
label-position="top"
|
||||
require-asterisk-position="right"
|
||||
label-width="auto"
|
||||
ref="applicationNodeFormRef"
|
||||
>
|
||||
<el-form-item
|
||||
:label="$t('views.applicationWorkflow.nodes.startNode.question')"
|
||||
prop="question_reference_address"
|
||||
:rules="{
|
||||
message: $t(
|
||||
'views.applicationWorkflow.nodes.searchDatasetNode.searchQuestion.requiredMessage'
|
||||
),
|
||||
trigger: 'blur',
|
||||
required: true
|
||||
}"
|
||||
>
|
||||
<NodeCascader
|
||||
ref="applicationNodeFormRef"
|
||||
:nodeModel="nodeModel"
|
||||
class="w-full"
|
||||
:placeholder="
|
||||
$t('views.applicationWorkflow.nodes.searchDatasetNode.searchQuestion.placeholder')
|
||||
"
|
||||
v-model="form_data.question_reference_address"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
v-if="form_data.hasOwnProperty('document_list') || 'document_list' in form_data"
|
||||
:label="$t('views.problem.relateParagraph.selectDocument')"
|
||||
prop="document_list"
|
||||
:rules="{
|
||||
message: $t('views.log.documentPlaceholder'),
|
||||
trigger: 'blur',
|
||||
required: false
|
||||
}"
|
||||
>
|
||||
<NodeCascader
|
||||
ref="nodeCascaderRef"
|
||||
:nodeModel="nodeModel"
|
||||
class="w-full"
|
||||
:placeholder="$t('views.log.documentPlaceholder')"
|
||||
v-model="form_data.document_list"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
v-if="form_data.hasOwnProperty('image_list') || 'image_list' in form_data"
|
||||
:label="$t('views.applicationWorkflow.nodes.imageUnderstandNode.image.label')"
|
||||
prop="image_list"
|
||||
:rules="{
|
||||
message: $t(
|
||||
'views.applicationWorkflow.nodes.imageUnderstandNode.image.requiredMessage'
|
||||
),
|
||||
trigger: 'blur',
|
||||
required: false
|
||||
}"
|
||||
>
|
||||
<NodeCascader
|
||||
ref="nodeCascaderRef"
|
||||
:nodeModel="nodeModel"
|
||||
class="w-full"
|
||||
:placeholder="
|
||||
$t('views.applicationWorkflow.nodes.imageUnderstandNode.image.requiredMessage')
|
||||
"
|
||||
v-model="form_data.image_list"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item
|
||||
v-if="form_data.hasOwnProperty('audio_list') || 'audio_list' in form_data"
|
||||
:label="$t('views.applicationWorkflow.nodes.speechToTextNode.audio.label')"
|
||||
prop="audio_list"
|
||||
:rules="{
|
||||
message: $t('views.applicationWorkflow.nodes.speechToTextNode.audio.placeholder'),
|
||||
trigger: 'blur',
|
||||
required: false
|
||||
}"
|
||||
>
|
||||
<NodeCascader
|
||||
ref="nodeCascaderRef"
|
||||
:nodeModel="nodeModel"
|
||||
class="w-full"
|
||||
:placeholder="$t('views.applicationWorkflow.nodes.speechToTextNode.audio.placeholder')"
|
||||
v-model="form_data.audio_list"
|
||||
/>
|
||||
</el-form-item>
|
||||
<div v-for="(field, index) in form_data.api_input_field_list" :key="'api-input-' + index">
|
||||
<el-form-item
|
||||
:label="field.variable"
|
||||
:prop="'api_input_field_list.' + index + '.value'"
|
||||
:rules="[
|
||||
{
|
||||
required: field.is_required,
|
||||
message: `${$t('common.inputPlaceholder')}${field.variable}`,
|
||||
trigger: 'blur'
|
||||
}
|
||||
]"
|
||||
>
|
||||
<NodeCascader
|
||||
ref="nodeCascaderRef"
|
||||
:nodeModel="nodeModel"
|
||||
class="w-full"
|
||||
:placeholder="
|
||||
$t('views.applicationWorkflow.nodes.searchDatasetNode.searchQuestion.placeholder')
|
||||
"
|
||||
v-model="form_data.api_input_field_list[index].value"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
|
||||
<div v-for="(field, index) in form_data.user_input_field_list" :key="'user-input-' + index">
|
||||
<el-form-item
|
||||
:label="field.label"
|
||||
:prop="'user_input_field_list.' + index + '.value'"
|
||||
:rules="[
|
||||
{
|
||||
required: field.required,
|
||||
message: `${$t('common.inputPlaceholder')}${field.label}`,
|
||||
trigger: 'blur'
|
||||
}
|
||||
]"
|
||||
>
|
||||
<NodeCascader
|
||||
ref="nodeCascaderRef"
|
||||
:nodeModel="nodeModel"
|
||||
class="w-full"
|
||||
:placeholder="
|
||||
$t('views.applicationWorkflow.nodes.searchDatasetNode.searchQuestion.placeholder')
|
||||
"
|
||||
v-model="form_data.user_input_field_list[index].value"
|
||||
/>
|
||||
</el-form-item>
|
||||
</div>
|
||||
<el-form-item
|
||||
:label="$t('views.applicationWorkflow.nodes.aiChatNode.returnContent.label')"
|
||||
@click.prevent
|
||||
>
|
||||
<template #label>
|
||||
<div class="flex align-center">
|
||||
<div class="mr-4">
|
||||
<span>{{
|
||||
$t('views.applicationWorkflow.nodes.aiChatNode.returnContent.label')
|
||||
}}</span>
|
||||
</div>
|
||||
<el-tooltip effect="dark" placement="right" popper-class="max-w-200">
|
||||
<template #content>
|
||||
{{ $t('views.applicationWorkflow.nodes.aiChatNode.returnContent.tooltip') }}
|
||||
</template>
|
||||
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<el-switch size="small" v-model="form_data.is_result" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</NodeContainer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { set, groupBy, create, cloneDeep } from 'lodash'
|
||||
import { app } from '@/main'
|
||||
import NodeContainer from '@/workflow/common/NodeContainer.vue'
|
||||
import { ref, computed, onMounted, onActivated } from 'vue'
|
||||
import NodeCascader from '@/workflow/common/NodeCascader.vue'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import applicationApi from '@/api/application'
|
||||
import { isWorkFlow } from '@/utils/application'
|
||||
|
||||
const form = {
|
||||
question_reference_address: ['start-node', 'question'],
|
||||
api_input_field_list: [],
|
||||
user_input_field_list: [],
|
||||
document_list: ['start-node', 'document'],
|
||||
image_list: ['start-node', 'image'],
|
||||
audio_list: ['start-node', 'audio']
|
||||
}
|
||||
|
||||
const {
|
||||
params: { id }
|
||||
} = app.config.globalProperties.$route as any
|
||||
|
||||
const applicationNodeFormRef = ref<FormInstance>()
|
||||
|
||||
const form_data = computed({
|
||||
get: () => {
|
||||
if (props.nodeModel.properties.node_data) {
|
||||
return props.nodeModel.properties.node_data
|
||||
} else {
|
||||
set(props.nodeModel.properties, 'node_data', form)
|
||||
}
|
||||
return props.nodeModel.properties.node_data
|
||||
},
|
||||
set: (value) => {
|
||||
set(props.nodeModel.properties, 'node_data', value)
|
||||
}
|
||||
})
|
||||
|
||||
function handleFileUpload(type: string, isEnabled: boolean) {
|
||||
const listKey = `${type}_list`
|
||||
if (isEnabled) {
|
||||
if (!props.nodeModel.properties.node_data[listKey]) {
|
||||
set(props.nodeModel.properties.node_data, listKey, [])
|
||||
}
|
||||
} else {
|
||||
// eslint-disable-next-line vue/no-mutating-props
|
||||
delete props.nodeModel.properties.node_data[listKey]
|
||||
}
|
||||
}
|
||||
|
||||
const update_field = () => {
|
||||
if (!props.nodeModel.properties.node_data.application_id) {
|
||||
set(props.nodeModel.properties, 'status', 500)
|
||||
return
|
||||
}
|
||||
applicationApi
|
||||
.getApplicationById(id, props.nodeModel.properties.node_data.application_id)
|
||||
.then((ok) => {
|
||||
const old_api_input_field_list = cloneDeep(
|
||||
props.nodeModel.properties.node_data.api_input_field_list
|
||||
)
|
||||
const old_user_input_field_list = cloneDeep(
|
||||
props.nodeModel.properties.node_data.user_input_field_list
|
||||
)
|
||||
if (isWorkFlow(ok.data.type)) {
|
||||
const nodeData = ok.data.work_flow.nodes[0].properties.node_data
|
||||
const new_api_input_field_list = cloneDeep(
|
||||
ok.data.work_flow.nodes[0].properties.api_input_field_list
|
||||
)
|
||||
const new_user_input_field_list = cloneDeep(
|
||||
ok.data.work_flow.nodes[0].properties.user_input_field_list
|
||||
)
|
||||
|
||||
const merge_api_input_field_list = (new_api_input_field_list || []).map((item: any) => {
|
||||
const find_field = old_api_input_field_list.find(
|
||||
(old_item: any) => old_item.variable == item.variable
|
||||
)
|
||||
if (find_field) {
|
||||
return {
|
||||
...item,
|
||||
value: find_field.value,
|
||||
label:
|
||||
typeof item.label === 'object' && item.label != null ? item.label.label : item.label
|
||||
}
|
||||
} else {
|
||||
return item
|
||||
}
|
||||
})
|
||||
set(
|
||||
props.nodeModel.properties.node_data,
|
||||
'api_input_field_list',
|
||||
merge_api_input_field_list
|
||||
)
|
||||
const merge_user_input_field_list = (new_user_input_field_list || []).map((item: any) => {
|
||||
const find_field = old_user_input_field_list.find(
|
||||
(old_item: any) => old_item.field == item.field
|
||||
)
|
||||
if (find_field) {
|
||||
return {
|
||||
...item,
|
||||
value: find_field.value,
|
||||
label:
|
||||
typeof item.label === 'object' && item.label != null ? item.label.label : item.label
|
||||
}
|
||||
} else {
|
||||
return item
|
||||
}
|
||||
})
|
||||
set(
|
||||
props.nodeModel.properties.node_data,
|
||||
'user_input_field_list',
|
||||
merge_user_input_field_list
|
||||
)
|
||||
const fileEnable = nodeData.file_upload_enable
|
||||
const fileUploadSetting = nodeData.file_upload_setting
|
||||
if (fileEnable) {
|
||||
handleFileUpload('document', fileUploadSetting.document)
|
||||
handleFileUpload('image', fileUploadSetting.image)
|
||||
handleFileUpload('audio', fileUploadSetting.audio)
|
||||
} else {
|
||||
;['document_list', 'image_list', 'audio_list'].forEach((list) => {
|
||||
// eslint-disable-next-line vue/no-mutating-props
|
||||
delete props.nodeModel.properties.node_data[list]
|
||||
})
|
||||
}
|
||||
set(props.nodeModel.properties, 'status', ok.data.id ? 200 : 500)
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(err)
|
||||
set(props.nodeModel.properties, 'status', 500)
|
||||
})
|
||||
}
|
||||
|
||||
const props = defineProps<{ nodeModel: any }>()
|
||||
|
||||
const validate = () => {
|
||||
return applicationNodeFormRef.value?.validate().catch((err) => {
|
||||
return Promise.reject({ node: props.nodeModel, errMessage: err })
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
update_field()
|
||||
set(props.nodeModel, 'validate', validate)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,132 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
:title="
|
||||
isEdit
|
||||
? $t('views.template.templateForm.title.editParam')
|
||||
: $t('views.template.templateForm.title.addParam')
|
||||
"
|
||||
v-model="dialogVisible"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
:destroy-on-close="true"
|
||||
:before-close="close"
|
||||
append-to-body
|
||||
>
|
||||
<el-form
|
||||
label-position="top"
|
||||
ref="fieldFormRef"
|
||||
:rules="rules"
|
||||
:model="form"
|
||||
require-asterisk-position="right"
|
||||
>
|
||||
<el-form-item :label="$t('dynamicsForm.paramForm.field.label')" prop="variable">
|
||||
<el-input
|
||||
v-model="form.variable"
|
||||
:placeholder="$t('dynamicsForm.paramForm.field.placeholder')"
|
||||
maxlength="64"
|
||||
show-word-limit
|
||||
@blur="form.variable = form.variable.trim()"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('dynamicsForm.paramForm.required.label')" @click.prevent>
|
||||
<el-switch size="small" v-model="form.is_required"></el-switch>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
:label="$t('dynamicsForm.default.label')"
|
||||
prop="default_value"
|
||||
:rules="{
|
||||
required: form.is_required,
|
||||
message: $t('dynamicsForm.default.placeholder'),
|
||||
trigger: 'blur'
|
||||
}"
|
||||
>
|
||||
<el-input
|
||||
v-model="form.default_value"
|
||||
:placeholder="$t('dynamicsForm.default.placeholder')"
|
||||
@blur="form.name = form.name.trim()"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click.prevent="dialogVisible = false"> {{ $t('common.cancel') }} </el-button>
|
||||
<el-button type="primary" @click="submit(fieldFormRef)" :loading="loading">
|
||||
{{ isEdit ? $t('common.save') : $t('common.add') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, watch } from 'vue'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { t } from '@/locales'
|
||||
const emit = defineEmits(['refresh'])
|
||||
|
||||
const fieldFormRef = ref()
|
||||
const loading = ref<boolean>(false)
|
||||
const isEdit = ref(false)
|
||||
|
||||
const form = ref<any>({
|
||||
name: '',
|
||||
variable: '',
|
||||
type: 'input',
|
||||
is_required: true,
|
||||
assignment_method: 'api_input',
|
||||
optionList: [''],
|
||||
default_value: ''
|
||||
})
|
||||
|
||||
const rules = reactive({
|
||||
name: [{ required: true, message: t('dynamicsForm.paramForm.name.requiredMessage'), trigger: 'blur' }],
|
||||
variable: [
|
||||
{ required: true, message: t('dynamicsForm.paramForm.field.requiredMessage'), trigger: 'blur' },
|
||||
{ pattern: /^[a-zA-Z0-9_]+$/, message: t('dynamicsForm.paramForm.field.requiredMessage2'), trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
const dialogVisible = ref<boolean>(false)
|
||||
|
||||
watch(dialogVisible, (bool) => {
|
||||
if (!bool) {
|
||||
form.value = {
|
||||
name: '',
|
||||
variable: '',
|
||||
type: 'input',
|
||||
is_required: true,
|
||||
assignment_method: 'api_input',
|
||||
optionList: [''],
|
||||
default_value: ''
|
||||
}
|
||||
isEdit.value = false
|
||||
}
|
||||
})
|
||||
|
||||
const open = (row: any) => {
|
||||
if (row) {
|
||||
form.value = cloneDeep(row)
|
||||
isEdit.value = true
|
||||
}
|
||||
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
dialogVisible.value = false
|
||||
isEdit.value = false
|
||||
}
|
||||
|
||||
const submit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
emit('refresh', form.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({ open, close })
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
<template>
|
||||
<div class="flex-between mb-16">
|
||||
<h5 class="lighter">{{ $t('views.template.templateForm.title.apiParamPassing') }}</h5>
|
||||
<el-button link type="primary" @click="openAddDialog()">
|
||||
<el-icon class="mr-4">
|
||||
<Plus />
|
||||
</el-icon>
|
||||
{{ $t('common.add') }}
|
||||
</el-button>
|
||||
</div>
|
||||
<el-table
|
||||
v-if="props.nodeModel.properties.api_input_field_list?.length > 0"
|
||||
:data="props.nodeModel.properties.api_input_field_list"
|
||||
class="mb-16"
|
||||
ref="tableRef"
|
||||
row-key="field"
|
||||
>
|
||||
<el-table-column prop="variable" :label="$t('dynamicsForm.paramForm.field.label')">
|
||||
<template #default="{ row }">
|
||||
<span class="ellipsis-1" :title="row.variable">
|
||||
{{ row.variable }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="default_value" :label="$t('dynamicsForm.default.label')">
|
||||
<template #default="{ row }">
|
||||
<span class="ellipsis-1" :title="row.default_value">
|
||||
{{ row.default_value }}
|
||||
</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('common.required')">
|
||||
<template #default="{ row }">
|
||||
<div @click.stop>
|
||||
<el-switch disabled size="small" v-model="row.is_required" />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('common.operation')" align="left" width="90">
|
||||
<template #default="{ row, $index }">
|
||||
<span class="mr-4">
|
||||
<el-tooltip effect="dark" :content="$t('common.modify')" placement="top">
|
||||
<el-button type="primary" text @click.stop="openAddDialog(row, $index)">
|
||||
<el-icon><EditPen /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<el-tooltip effect="dark" :content="$t('common.delete')" placement="top">
|
||||
<el-button type="primary" text @click="deleteField($index)">
|
||||
<el-icon>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<ApiFieldFormDialog ref="ApiFieldFormDialogRef" @refresh="refreshFieldList" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { set } from 'lodash'
|
||||
import Sortable from 'sortablejs'
|
||||
import ApiFieldFormDialog from './ApiFieldFormDialog.vue'
|
||||
import { MsgError } from '@/utils/message'
|
||||
import { t } from '@/locales'
|
||||
|
||||
const props = defineProps<{ nodeModel: any }>()
|
||||
const tableRef = ref()
|
||||
const currentIndex = ref(null)
|
||||
const ApiFieldFormDialogRef = ref()
|
||||
const inputFieldList = ref<any[]>([])
|
||||
|
||||
function openAddDialog(data?: any, index?: any) {
|
||||
if (typeof index !== 'undefined') {
|
||||
currentIndex.value = index
|
||||
}
|
||||
ApiFieldFormDialogRef.value.open(data)
|
||||
}
|
||||
|
||||
function deleteField(index: any) {
|
||||
inputFieldList.value.splice(index, 1)
|
||||
props.nodeModel.graphModel.eventCenter.emit('refreshFieldList')
|
||||
onDragHandle()
|
||||
}
|
||||
|
||||
function refreshFieldList(data: any) {
|
||||
for (let i = 0; i < inputFieldList.value.length; i++) {
|
||||
if (inputFieldList.value[i].variable === data.variable && currentIndex.value !== i) {
|
||||
MsgError(t('views.applicationWorkflow.tip.paramErrorMessage') + data.variable)
|
||||
return
|
||||
}
|
||||
}
|
||||
// 查看另一个list又没有重复的
|
||||
let arr = props.nodeModel.properties.user_input_field_list
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
if (arr[i].field === data.variable) {
|
||||
MsgError(t('views.applicationWorkflow.tip.paramErrorMessage') + data.variable)
|
||||
return
|
||||
}
|
||||
}
|
||||
if (currentIndex.value !== null) {
|
||||
inputFieldList.value.splice(currentIndex.value, 1, data)
|
||||
} else {
|
||||
inputFieldList.value.push(data)
|
||||
}
|
||||
currentIndex.value = null
|
||||
ApiFieldFormDialogRef.value.close()
|
||||
props.nodeModel.graphModel.eventCenter.emit('refreshFieldList')
|
||||
onDragHandle()
|
||||
}
|
||||
|
||||
// 表格排序拖拽
|
||||
function onDragHandle() {
|
||||
if (!tableRef.value) return
|
||||
|
||||
// 获取表格的 tbody DOM 元素
|
||||
const wrapper = tableRef.value.$el as HTMLElement
|
||||
const tbody = wrapper.querySelector('.el-table__body-wrapper tbody')
|
||||
if (!tbody) return
|
||||
// 初始化 Sortable
|
||||
Sortable.create(tbody as HTMLElement, {
|
||||
animation: 150,
|
||||
ghostClass: 'ghost-row',
|
||||
onEnd: (evt) => {
|
||||
if (evt.oldIndex === undefined || evt.newIndex === undefined) return
|
||||
// 更新数据顺序
|
||||
const items = [...inputFieldList.value]
|
||||
const [movedItem] = items.splice(evt.oldIndex, 1)
|
||||
items.splice(evt.newIndex, 0, movedItem)
|
||||
inputFieldList.value = items
|
||||
props.nodeModel.graphModel.eventCenter.emit('refreshFieldList')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!props.nodeModel.properties.api_input_field_list) {
|
||||
if (props.nodeModel.properties.input_field_list) {
|
||||
props.nodeModel.properties.input_field_list
|
||||
.filter((item: any) => {
|
||||
return item.assignment_method === 'api_input'
|
||||
})
|
||||
.forEach((item: any) => {
|
||||
inputFieldList.value.push(item)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
inputFieldList.value.push(...props.nodeModel.properties.api_input_field_list)
|
||||
}
|
||||
set(props.nodeModel.properties, 'api_input_field_list', inputFieldList)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
|
@ -0,0 +1,288 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
:title="$t('views.applicationWorkflow.nodes.baseNode.FileUploadSetting.title')"
|
||||
v-model="dialogVisible"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
:destroy-on-close="true"
|
||||
:before-close="close"
|
||||
append-to-body
|
||||
width="800"
|
||||
>
|
||||
<el-form
|
||||
label-position="top"
|
||||
ref="fieldFormRef"
|
||||
:model="form_data"
|
||||
require-asterisk-position="right"
|
||||
>
|
||||
<el-form-item
|
||||
:label="$t('views.applicationWorkflow.nodes.baseNode.FileUploadSetting.maxFiles')"
|
||||
>
|
||||
<el-slider
|
||||
v-model="form_data.maxFiles"
|
||||
show-input
|
||||
:show-input-controls="false"
|
||||
:min="1"
|
||||
:max="10"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
:label="$t('views.applicationWorkflow.nodes.baseNode.FileUploadSetting.fileLimit')"
|
||||
>
|
||||
<el-slider
|
||||
v-model="form_data.fileLimit"
|
||||
show-input
|
||||
:show-input-controls="false"
|
||||
:min="1"
|
||||
:max="100"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
:label="
|
||||
$t('views.applicationWorkflow.nodes.baseNode.FileUploadSetting.fileUploadType.label')
|
||||
"
|
||||
>
|
||||
<el-card
|
||||
shadow="hover"
|
||||
class="card-checkbox cursor w-full mb-8"
|
||||
:class="form_data.document ? 'active' : ''"
|
||||
style="--el-card-padding: 8px 16px"
|
||||
@click.stop="form_data.document = !form_data.document"
|
||||
>
|
||||
<div class="flex-between">
|
||||
<div class="flex align-center">
|
||||
<img class="mr-12" src="@/assets/icon_file-doc.svg" alt="" />
|
||||
<div>
|
||||
<p class="line-height-22 mt-4">
|
||||
{{ $t('common.fileUpload.document') }}
|
||||
<el-text class="color-secondary"
|
||||
>{{
|
||||
$t(
|
||||
'views.applicationWorkflow.nodes.baseNode.FileUploadSetting.fileUploadType.documentText'
|
||||
)
|
||||
}}
|
||||
</el-text>
|
||||
</p>
|
||||
<p>{{ documentExtensions.join('、') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<el-checkbox
|
||||
v-model="form_data.document"
|
||||
@change="form_data.document = !form_data.document"
|
||||
/>
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card
|
||||
shadow="hover"
|
||||
class="card-checkbox cursor w-full mb-8"
|
||||
:class="form_data.image ? 'active' : ''"
|
||||
style="--el-card-padding: 8px 16px"
|
||||
@click.stop="form_data.image = !form_data.image"
|
||||
>
|
||||
<div class="flex-between">
|
||||
<div class="flex align-center">
|
||||
<img class="mr-12" src="@/assets/icon_file-image.svg" alt="" />
|
||||
<div>
|
||||
<p class="line-height-22 mt-4">
|
||||
{{ $t('common.fileUpload.image') }}
|
||||
<el-text class="color-secondary"
|
||||
>{{
|
||||
$t(
|
||||
'views.applicationWorkflow.nodes.baseNode.FileUploadSetting.fileUploadType.imageText'
|
||||
)
|
||||
}}
|
||||
</el-text>
|
||||
</p>
|
||||
<p>{{ imageExtensions.join('、') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<el-checkbox v-model="form_data.image" @change="form_data.image = !form_data.image" />
|
||||
</div>
|
||||
</el-card>
|
||||
|
||||
<el-card
|
||||
shadow="hover"
|
||||
class="card-checkbox cursor w-full mb-8"
|
||||
:class="form_data.audio ? 'active' : ''"
|
||||
style="--el-card-padding: 8px 16px"
|
||||
@click.stop="form_data.audio = !form_data.audio"
|
||||
>
|
||||
<div class="flex-between">
|
||||
<div class="flex align-center">
|
||||
<img class="mr-12" src="@/assets/icon_file-audio.svg" alt="" />
|
||||
<div>
|
||||
<p class="line-height-22 mt-4">
|
||||
{{ $t('common.fileUpload.audio') }}
|
||||
<el-text class="color-secondary"
|
||||
>{{
|
||||
$t(
|
||||
'views.applicationWorkflow.nodes.baseNode.FileUploadSetting.fileUploadType.audioText'
|
||||
)
|
||||
}}
|
||||
</el-text>
|
||||
</p>
|
||||
<p>{{ audioExtensions.join('、') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<el-checkbox v-model="form_data.audio" @change="form_data.audio = !form_data.audio" />
|
||||
</div>
|
||||
</el-card>
|
||||
<el-card
|
||||
shadow="hover"
|
||||
class="card-checkbox cursor w-full mb-8"
|
||||
:class="form_data.other ? 'active' : ''"
|
||||
style="--el-card-padding: 8px 16px"
|
||||
@click.stop="form_data.other = !form_data.other"
|
||||
>
|
||||
<div class="flex-between">
|
||||
<div class="flex align-center">
|
||||
<img class="mr-12" :width="32" src="@/assets/fileType/unknown-icon.svg" alt="" />
|
||||
<div>
|
||||
<p class="line-height-22 mt-4">
|
||||
{{ $t('common.fileUpload.other') }}
|
||||
<el-text class="color-secondary"
|
||||
>{{
|
||||
$t(
|
||||
'views.applicationWorkflow.nodes.baseNode.FileUploadSetting.fileUploadType.otherText'
|
||||
)
|
||||
}}
|
||||
</el-text>
|
||||
</p>
|
||||
<el-space wrap :size="2" class="mt-4">
|
||||
<el-tag
|
||||
v-for="tag in form_data.otherExtensions"
|
||||
:key="tag"
|
||||
closable
|
||||
:disable-transitions="false"
|
||||
@close="handleClose(tag)"
|
||||
type="info"
|
||||
class="mr-4"
|
||||
effect="plain"
|
||||
style="
|
||||
--el-tag-border-radius: 4px;
|
||||
--el-tag-border-color: var(--el-border-color);
|
||||
"
|
||||
>
|
||||
{{ tag }}
|
||||
</el-tag>
|
||||
<el-input
|
||||
v-if="inputVisible"
|
||||
ref="InputRef"
|
||||
v-model="inputValue"
|
||||
size="small"
|
||||
@keyup.enter="handleInputConfirm"
|
||||
@blur="handleInputConfirm"
|
||||
/>
|
||||
<el-button v-else class="button-new-tag" size="small" @click.stop="showInput">
|
||||
+ {{ $t('common.fileUpload.addExtensions') }}
|
||||
</el-button>
|
||||
</el-space>
|
||||
</div>
|
||||
</div>
|
||||
<el-checkbox v-model="form_data.other" @change="form_data.other = !form_data.other" />
|
||||
</div>
|
||||
</el-card>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click.prevent="close"> {{ $t('common.cancel') }} </el-button>
|
||||
<el-button type="primary" @click="submit()" :loading="loading">
|
||||
{{ $t('common.confirm') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, ref } from 'vue'
|
||||
import type { InputInstance } from 'element-plus'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { MsgWarning } from '@/utils/message'
|
||||
import { t } from '@/locales'
|
||||
|
||||
const emit = defineEmits(['refresh'])
|
||||
const props = defineProps<{ nodeModel: any }>()
|
||||
|
||||
const dialogVisible = ref(false)
|
||||
const inputVisible = ref(false)
|
||||
const inputValue = ref('')
|
||||
const loading = ref(false)
|
||||
const fieldFormRef = ref()
|
||||
const InputRef = ref<InputInstance>()
|
||||
|
||||
const documentExtensions = ['TXT', 'MD', 'DOCX', 'HTML', 'CSV', 'XLSX', 'XLS', 'PDF']
|
||||
const imageExtensions = ['JPG', 'JPEG', 'PNG', 'GIF']
|
||||
const audioExtensions = ['MP3', 'WAV', 'OGG', 'ACC', 'M4A']
|
||||
|
||||
const form_data = ref({
|
||||
maxFiles: 3,
|
||||
fileLimit: 50,
|
||||
document: true,
|
||||
image: false,
|
||||
audio: false,
|
||||
video: false,
|
||||
other: false,
|
||||
otherExtensions: ['PPT', 'DOC']
|
||||
})
|
||||
|
||||
function open(data: any) {
|
||||
dialogVisible.value = true
|
||||
nextTick(() => {
|
||||
form_data.value = { ...form_data.value, ...data }
|
||||
})
|
||||
}
|
||||
|
||||
function close() {
|
||||
dialogVisible.value = false
|
||||
}
|
||||
|
||||
const handleClose = (tag: string) => {
|
||||
form_data.value.otherExtensions = form_data.value.otherExtensions.filter((item) => item !== tag)
|
||||
}
|
||||
|
||||
const showInput = () => {
|
||||
inputVisible.value = true
|
||||
nextTick(() => {
|
||||
InputRef.value!.input!.focus()
|
||||
})
|
||||
}
|
||||
const handleInputConfirm = () => {
|
||||
if (inputValue.value) {
|
||||
inputValue.value = inputValue.value.toUpperCase()
|
||||
if (
|
||||
form_data.value.otherExtensions.includes(inputValue.value) ||
|
||||
documentExtensions.includes(inputValue.value) ||
|
||||
imageExtensions.includes(inputValue.value) ||
|
||||
audioExtensions.includes(inputValue.value)
|
||||
) {
|
||||
inputVisible.value = false
|
||||
inputValue.value = ''
|
||||
MsgWarning(t('common.fileUpload.existingExtensionsTip'))
|
||||
return
|
||||
}
|
||||
form_data.value.otherExtensions.push(inputValue.value)
|
||||
}
|
||||
inputVisible.value = false
|
||||
inputValue.value = ''
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
const formEl = fieldFormRef.value
|
||||
if (!formEl) return
|
||||
await formEl.validate().then(() => {
|
||||
const formattedData = cloneDeep(form_data.value)
|
||||
emit('refresh', formattedData)
|
||||
// emit('refresh', form_data.value)
|
||||
props.nodeModel.graphModel.eventCenter.emit('refreshFileUploadConfig')
|
||||
dialogVisible.value = false
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
open
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
|
@ -0,0 +1,168 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
:title="
|
||||
isEdit
|
||||
? $t('views.template.templateForm.title.editParam')
|
||||
: $t('views.template.templateForm.title.addParam')
|
||||
"
|
||||
v-model="dialogVisible"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
:destroy-on-close="true"
|
||||
:before-close="close"
|
||||
append-to-body
|
||||
>
|
||||
<DynamicsFormConstructor
|
||||
v-model="currentRow"
|
||||
label-position="top"
|
||||
require-asterisk-position="right"
|
||||
:input_type_list="inputTypeList"
|
||||
ref="DynamicsFormConstructorRef"
|
||||
></DynamicsFormConstructor>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click.prevent="close"> {{ $t('common.cancel') }} </el-button>
|
||||
<el-button type="primary" @click="submit()" :loading="loading">
|
||||
{{ isEdit ? $t('common.save') : $t('common.add') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import DynamicsFormConstructor from '@/components/dynamics-form/constructor/index.vue'
|
||||
import type { FormField } from '@/components/dynamics-form/type'
|
||||
import _ from 'lodash'
|
||||
import { t } from '@/locales'
|
||||
const emit = defineEmits(['refresh'])
|
||||
|
||||
const DynamicsFormConstructorRef = ref()
|
||||
const loading = ref<boolean>(false)
|
||||
const isEdit = ref(false)
|
||||
const currentItem = ref<FormField | any>()
|
||||
const check_field = (field_list: Array<string>, obj: any) => {
|
||||
return field_list.every((field) => _.get(obj, field, undefined) !== undefined)
|
||||
}
|
||||
const currentRow = computed(() => {
|
||||
if (currentItem.value) {
|
||||
const row = currentItem.value
|
||||
switch (row.type) {
|
||||
case 'input':
|
||||
if (check_field(['field', 'input_type', 'label', 'required', 'attrs'], currentItem.value)) {
|
||||
return currentItem.value
|
||||
}
|
||||
return {
|
||||
attrs: row.attrs || { maxlength: 200, minlength: 0 },
|
||||
field: row.field || row.variable,
|
||||
input_type: 'TextInput',
|
||||
label: row.label || row.name,
|
||||
default_value: row.default_value,
|
||||
required: row.required != undefined ? row.required : row.is_required
|
||||
}
|
||||
case 'select':
|
||||
if (
|
||||
check_field(
|
||||
['field', 'input_type', 'label', 'required', 'option_list'],
|
||||
currentItem.value
|
||||
)
|
||||
) {
|
||||
return currentItem.value
|
||||
}
|
||||
return {
|
||||
attrs: row.attrs || {},
|
||||
field: row.field || row.variable,
|
||||
input_type: 'SingleSelect',
|
||||
label: row.label || row.name,
|
||||
default_value: row.default_value,
|
||||
required: row.required != undefined ? row.required : row.is_required,
|
||||
option_list: row.option_list
|
||||
? row.option_list
|
||||
: row.optionList.map((o: any) => {
|
||||
return { key: o, value: o }
|
||||
})
|
||||
}
|
||||
|
||||
case 'date':
|
||||
if (
|
||||
check_field(
|
||||
[
|
||||
'field',
|
||||
'input_type',
|
||||
'label',
|
||||
'required',
|
||||
'attrs.format',
|
||||
'attrs.value-format',
|
||||
'attrs.type'
|
||||
],
|
||||
currentItem.value
|
||||
)
|
||||
) {
|
||||
return currentItem.value
|
||||
}
|
||||
return {
|
||||
field: row.field || row.variable,
|
||||
input_type: 'DatePicker',
|
||||
label: row.label || row.name,
|
||||
default_value: row.default_value,
|
||||
required: row.required != undefined ? row.required : row.is_required,
|
||||
attrs: {
|
||||
format: 'YYYY-MM-DD HH:mm:ss',
|
||||
'value-format': 'YYYY-MM-DD HH:mm:ss',
|
||||
type: 'datetime'
|
||||
}
|
||||
}
|
||||
default:
|
||||
return currentItem.value
|
||||
}
|
||||
} else {
|
||||
return { input_type: 'TextInput', required: false, attrs: { maxlength: 200, minlength: 0 }, show_default_value: true }
|
||||
}
|
||||
})
|
||||
const currentIndex = ref(null)
|
||||
const inputTypeList = ref([
|
||||
{ label: t('dynamicsForm.input_type_list.TextInput'), value: 'TextInputConstructor' },
|
||||
{ label: t('dynamicsForm.input_type_list.PasswordInput'), value: 'PasswordInputConstructor' },
|
||||
{ label: t('dynamicsForm.input_type_list.SingleSelect'), value: 'SingleSelectConstructor' },
|
||||
{ label: t('dynamicsForm.input_type_list.MultiSelect'), value: 'MultiSelectConstructor' },
|
||||
{ label: t('dynamicsForm.input_type_list.RadioCard'), value: 'RadioCardConstructor' },
|
||||
{ label: t('dynamicsForm.input_type_list.DatePicker'), value: 'DatePickerConstructor' },
|
||||
{ label: t('dynamicsForm.input_type_list.SwitchInput'), value: 'SwitchInputConstructor' },
|
||||
])
|
||||
|
||||
const dialogVisible = ref<boolean>(false)
|
||||
|
||||
const open = (row: any, index: any) => {
|
||||
dialogVisible.value = true
|
||||
|
||||
if (row) {
|
||||
isEdit.value = true
|
||||
currentItem.value = cloneDeep(row)
|
||||
currentIndex.value = index
|
||||
} else {
|
||||
currentItem.value = null
|
||||
}
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
dialogVisible.value = false
|
||||
isEdit.value = false
|
||||
currentIndex.value = null
|
||||
currentItem.value = null as any
|
||||
}
|
||||
|
||||
const submit = async () => {
|
||||
const formEl = DynamicsFormConstructorRef.value
|
||||
if (!formEl) return
|
||||
await formEl.validate().then(() => {
|
||||
emit('refresh', formEl?.getData(), currentIndex.value)
|
||||
isEdit.value = false
|
||||
currentItem.value = null as any
|
||||
currentIndex.value = null
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({ open, close })
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,258 @@
|
|||
<template>
|
||||
<div class="flex-between mb-16">
|
||||
<h5 class="break-all ellipsis lighter" style="max-width:80%" :title="inputFieldConfig.title">
|
||||
{{ inputFieldConfig.title }}
|
||||
</h5>
|
||||
<div>
|
||||
<el-button type="primary" link @click="openChangeTitleDialog">
|
||||
<el-icon>
|
||||
<Setting />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<span class="ml-4">
|
||||
<el-button link type="primary" @click="openAddDialog()">
|
||||
<el-icon class="mr-4">
|
||||
<Plus />
|
||||
</el-icon>
|
||||
{{ $t('common.add') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<el-table
|
||||
v-if="props.nodeModel.properties.user_input_field_list?.length > 0"
|
||||
:data="props.nodeModel.properties.user_input_field_list"
|
||||
class="mb-16"
|
||||
ref="tableRef"
|
||||
row-key="field"
|
||||
>
|
||||
<el-table-column prop="field" :label="$t('dynamicsForm.paramForm.field.label')" width="95">
|
||||
<template #default="{ row }">
|
||||
<span :title="row.field" class="ellipsis-1">{{ row.field }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="label" :label="$t('dynamicsForm.paramForm.name.label')">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.label && row.label.input_type === 'TooltipLabel'">
|
||||
<span :title="row.label.label" class="ellipsis-1">
|
||||
{{ row.label.label }}
|
||||
</span>
|
||||
</span>
|
||||
<span v-else>
|
||||
<span :title="row.label" class="ellipsis-1">
|
||||
{{ row.label }}
|
||||
</span></span
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('dynamicsForm.paramForm.input_type.label')" width="95">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="info" class="info-tag" v-if="row.input_type === 'TextInput'">{{
|
||||
$t('dynamicsForm.input_type_list.TextInput')
|
||||
}}</el-tag>
|
||||
<el-tag type="info" class="info-tag" v-if="row.input_type === 'PasswordInput'">{{
|
||||
$t('dynamicsForm.input_type_list.PasswordInput')
|
||||
}}</el-tag>
|
||||
<el-tag type="info" class="info-tag" v-if="row.input_type === 'Slider'">{{
|
||||
$t('dynamicsForm.input_type_list.Slider')
|
||||
}}</el-tag>
|
||||
<el-tag type="info" class="info-tag" v-if="row.input_type === 'SwitchInput'">{{
|
||||
$t('dynamicsForm.input_type_list.SwitchInput')
|
||||
}}</el-tag>
|
||||
<el-tag type="info" class="info-tag" v-if="row.input_type === 'SingleSelect'">{{
|
||||
$t('dynamicsForm.input_type_list.SingleSelect')
|
||||
}}</el-tag>
|
||||
<el-tag type="info" class="info-tag" v-if="row.input_type === 'MultiSelect'">{{
|
||||
$t('dynamicsForm.input_type_list.MultiSelect')
|
||||
}}</el-tag>
|
||||
<el-tag type="info" class="info-tag" v-if="row.input_type === 'RadioCard'">{{
|
||||
$t('dynamicsForm.input_type_list.RadioCard')
|
||||
}}</el-tag>
|
||||
<el-tag type="info" class="info-tag" v-if="row.input_type === 'DatePicker'">{{
|
||||
$t('dynamicsForm.input_type_list.DatePicker')
|
||||
}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="default_value" :label="$t('dynamicsForm.default.label')">
|
||||
<template #default="{ row }">
|
||||
<span :title="row.default_value" class="ellipsis-1">{{ getDefaultValue(row) }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('common.required')">
|
||||
<template #default="{ row }">
|
||||
<div @click.stop>
|
||||
<el-switch disabled size="small" v-model="row.required" />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('common.operation')" align="left" width="90">
|
||||
<template #default="{ row, $index }">
|
||||
<span class="mr-4">
|
||||
<el-tooltip effect="dark" :content="$t('common.modify')" placement="top">
|
||||
<el-button type="primary" text @click.stop="openAddDialog(row, $index)">
|
||||
<el-icon><EditPen /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<el-tooltip effect="dark" :content="$t('common.delete')" placement="top">
|
||||
<el-button type="primary" text @click="deleteField($index)">
|
||||
<el-icon>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<UserFieldFormDialog ref="UserFieldFormDialogRef" @refresh="refreshFieldList" />
|
||||
<UserInputTitleDialog ref="UserInputTitleDialogRef" @refresh="refreshFieldTitle" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { set } from 'lodash'
|
||||
import Sortable from 'sortablejs'
|
||||
import UserFieldFormDialog from './UserFieldFormDialog.vue'
|
||||
import { MsgError } from '@/utils/message'
|
||||
import { t } from '@/locales'
|
||||
import UserInputTitleDialog from '@/workflow/nodes/base-node/component/UserInputTitleDialog.vue'
|
||||
const props = defineProps<{ nodeModel: any }>()
|
||||
|
||||
const tableRef = ref()
|
||||
const UserFieldFormDialogRef = ref()
|
||||
const UserInputTitleDialogRef = ref()
|
||||
const inputFieldList = ref<any[]>([])
|
||||
const inputFieldConfig = ref({ title: t('chat.userInput') })
|
||||
|
||||
function openAddDialog(data?: any, index?: any) {
|
||||
UserFieldFormDialogRef.value.open(data, index)
|
||||
}
|
||||
|
||||
function openChangeTitleDialog() {
|
||||
UserInputTitleDialogRef.value.open(inputFieldConfig.value)
|
||||
}
|
||||
|
||||
function deleteField(index: any) {
|
||||
inputFieldList.value.splice(index, 1)
|
||||
props.nodeModel.graphModel.eventCenter.emit('refreshFieldList')
|
||||
onDragHandle()
|
||||
}
|
||||
|
||||
function refreshFieldList(data: any, index: any) {
|
||||
for (let i = 0; i < inputFieldList.value.length; i++) {
|
||||
if (inputFieldList.value[i].field === data.field && index !== i) {
|
||||
MsgError(t('views.applicationWorkflow.tip.paramErrorMessage') + data.field)
|
||||
return
|
||||
}
|
||||
}
|
||||
// 查看另一个list又没有重复的
|
||||
let arr = props.nodeModel.properties.api_input_field_list
|
||||
for (let i = 0; i < arr.length; i++) {
|
||||
if (arr[i].variable === data.field) {
|
||||
MsgError(t('views.applicationWorkflow.tip.paramErrorMessage') + data.field)
|
||||
return
|
||||
}
|
||||
}
|
||||
if (index !== null) {
|
||||
inputFieldList.value.splice(index, 1, data)
|
||||
} else {
|
||||
inputFieldList.value.push(data)
|
||||
}
|
||||
UserFieldFormDialogRef.value.close()
|
||||
props.nodeModel.graphModel.eventCenter.emit('refreshFieldList')
|
||||
onDragHandle()
|
||||
}
|
||||
|
||||
function refreshFieldTitle(data: any) {
|
||||
inputFieldConfig.value = data
|
||||
UserInputTitleDialogRef.value.close()
|
||||
|
||||
// console.log('inputFieldConfig', inputFieldConfig.value)
|
||||
}
|
||||
|
||||
const getDefaultValue = (row: any) => {
|
||||
if (row.input_type === 'PasswordInput') {
|
||||
return '******'
|
||||
}
|
||||
if (row.default_value) {
|
||||
const default_value = row.option_list
|
||||
?.filter((v: any) => row.default_value.indexOf(v.value) > -1)
|
||||
.map((v: any) => v.label)
|
||||
.join(',')
|
||||
if (default_value) {
|
||||
return default_value
|
||||
}
|
||||
return row.default_value
|
||||
}
|
||||
if (row.default_value !== undefined) {
|
||||
return row.default_value
|
||||
}
|
||||
}
|
||||
|
||||
function onDragHandle() {
|
||||
if (!tableRef.value) return
|
||||
|
||||
// 获取表格的 tbody DOM 元素
|
||||
const wrapper = tableRef.value.$el as HTMLElement
|
||||
const tbody = wrapper.querySelector('.el-table__body-wrapper tbody')
|
||||
if (!tbody) return
|
||||
// 初始化 Sortable
|
||||
Sortable.create(tbody as HTMLElement, {
|
||||
animation: 150,
|
||||
ghostClass: 'ghost-row',
|
||||
onEnd: (evt) => {
|
||||
if (evt.oldIndex === undefined || evt.newIndex === undefined) return
|
||||
// 更新数据顺序
|
||||
const items = [...inputFieldList.value]
|
||||
const [movedItem] = items.splice(evt.oldIndex, 1)
|
||||
items.splice(evt.newIndex, 0, movedItem)
|
||||
inputFieldList.value = items
|
||||
props.nodeModel.graphModel.eventCenter.emit('refreshFieldList')
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (!props.nodeModel.properties.user_input_field_list) {
|
||||
if (props.nodeModel.properties.input_field_list) {
|
||||
props.nodeModel.properties.input_field_list
|
||||
.filter((item: any) => {
|
||||
return item.assignment_method === 'user_input'
|
||||
})
|
||||
.forEach((item: any) => {
|
||||
inputFieldList.value.push(item)
|
||||
})
|
||||
}
|
||||
} else {
|
||||
inputFieldList.value.push(...props.nodeModel.properties.user_input_field_list)
|
||||
}
|
||||
// 兼容旧数据
|
||||
inputFieldList.value.forEach((item, index) => {
|
||||
item.label = item.label || item.name
|
||||
item.field = item.field || item.variable
|
||||
item.required = item.required || item.is_required
|
||||
switch (item.type) {
|
||||
case 'input':
|
||||
item.input_type = 'TextInput'
|
||||
break
|
||||
case 'select':
|
||||
item.input_type = 'SingleSelect'
|
||||
break
|
||||
case 'date':
|
||||
item.input_type = 'DatePicker'
|
||||
break
|
||||
}
|
||||
})
|
||||
set(props.nodeModel.properties, 'user_input_field_list', inputFieldList)
|
||||
if (props.nodeModel.properties.user_input_config) {
|
||||
inputFieldConfig.value = props.nodeModel.properties.user_input_config
|
||||
}
|
||||
set(props.nodeModel.properties, 'user_input_config', inputFieldConfig)
|
||||
onDragHandle()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss"></style>
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
:title="$t('common.setting')"
|
||||
v-model="dialogVisible"
|
||||
:close-on-click-modal="false"
|
||||
:close-on-press-escape="false"
|
||||
:destroy-on-close="true"
|
||||
:before-close="close"
|
||||
append-to-body
|
||||
>
|
||||
<el-form
|
||||
label-position="top"
|
||||
ref="fieldFormRef"
|
||||
:rules="rules"
|
||||
:model="form"
|
||||
require-asterisk-position="right"
|
||||
>
|
||||
<el-form-item :label="$t('common.title')" prop="title">
|
||||
<el-input
|
||||
v-model="form.title"
|
||||
maxlength="64"
|
||||
show-word-limit
|
||||
@blur="form.title = form.title.trim()"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<span class="dialog-footer">
|
||||
<el-button @click.prevent="dialogVisible = false"> {{ $t('common.cancel') }} </el-button>
|
||||
<el-button type="primary" @click="submit(fieldFormRef)" :loading="loading">
|
||||
{{ $t('common.save') }}
|
||||
</el-button>
|
||||
</span>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { reactive, ref, watch } from 'vue'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { cloneDeep } from 'lodash'
|
||||
import { t } from '@/locales'
|
||||
const emit = defineEmits(['refresh'])
|
||||
|
||||
const fieldFormRef = ref()
|
||||
const loading = ref<boolean>(false)
|
||||
|
||||
const form = ref<any>({
|
||||
title: t('chat.userInput')
|
||||
})
|
||||
|
||||
const rules = reactive({
|
||||
title: [
|
||||
{ required: true, message: t('dynamicsForm.paramForm.name.requiredMessage'), trigger: 'blur' }
|
||||
]
|
||||
})
|
||||
|
||||
const dialogVisible = ref<boolean>(false)
|
||||
|
||||
const open = (row: any) => {
|
||||
if (row) {
|
||||
form.value = cloneDeep(row)
|
||||
}
|
||||
|
||||
dialogVisible.value = true
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
dialogVisible.value = false
|
||||
}
|
||||
|
||||
const submit = async (formEl: FormInstance | undefined) => {
|
||||
if (!formEl) return
|
||||
await formEl.validate((valid) => {
|
||||
if (valid) {
|
||||
emit('refresh', form.value)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
defineExpose({ open, close })
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import BaseNodeVue from './index.vue'
|
||||
import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
|
||||
|
||||
class BaseNode extends AppNode {
|
||||
constructor(props: any) {
|
||||
super(props, BaseNodeVue)
|
||||
}
|
||||
}
|
||||
|
||||
class BaseModel extends AppNodeModel {
|
||||
constructor(data: any, graphModel: any) {
|
||||
super(data, graphModel)
|
||||
}
|
||||
get_width() {
|
||||
return 600
|
||||
}
|
||||
}
|
||||
export default {
|
||||
type: 'base-node',
|
||||
model: BaseModel,
|
||||
view: BaseNode
|
||||
}
|
||||
|
|
@ -0,0 +1,349 @@
|
|||
<template>
|
||||
<NodeContainer :nodeModel="nodeModel">
|
||||
<el-form
|
||||
@submit.prevent
|
||||
:model="form_data"
|
||||
label-position="top"
|
||||
require-asterisk-position="right"
|
||||
class="mb-24"
|
||||
label-width="auto"
|
||||
ref="baseNodeFormRef"
|
||||
>
|
||||
<el-form-item
|
||||
:label="$t('views.applicationWorkflow.nodes.baseNode.appName.label')"
|
||||
prop="name"
|
||||
:rules="{
|
||||
message: t('views.application.applicationForm.form.appName.requiredMessage'),
|
||||
trigger: 'blur',
|
||||
required: true
|
||||
}"
|
||||
>
|
||||
<el-input
|
||||
v-model="form_data.name"
|
||||
maxlength="64"
|
||||
:placeholder="t('views.application.applicationForm.form.appName.placeholder')"
|
||||
show-word-limit
|
||||
@blur="form_data.name = form_data.name?.trim()"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('views.applicationWorkflow.nodes.baseNode.appDescription.label')">
|
||||
<el-input
|
||||
v-model="form_data.desc"
|
||||
:placeholder="$t('views.application.applicationForm.form.appDescription.placeholder')"
|
||||
:rows="3"
|
||||
type="textarea"
|
||||
maxlength="256"
|
||||
show-word-limit
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item :label="$t('views.application.applicationForm.form.prologue')">
|
||||
<MdEditorMagnify
|
||||
@wheel="wheel"
|
||||
:title="$t('views.application.applicationForm.form.prologue')"
|
||||
v-model="form_data.prologue"
|
||||
style="height: 150px"
|
||||
@submitDialog="submitDialog"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="flex-between">
|
||||
<div class="flex align-center">
|
||||
<span class="mr-4">{{
|
||||
$t('views.applicationWorkflow.nodes.baseNode.fileUpload.label')
|
||||
}}</span>
|
||||
<el-tooltip
|
||||
effect="dark"
|
||||
:content="$t('views.applicationWorkflow.nodes.baseNode.fileUpload.tooltip')"
|
||||
placement="right"
|
||||
>
|
||||
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
<div>
|
||||
<el-button
|
||||
v-if="form_data.file_upload_enable"
|
||||
type="primary"
|
||||
link
|
||||
@click="openFileUploadSettingDialog"
|
||||
class="mr-4"
|
||||
>
|
||||
<el-icon class="mr-4">
|
||||
<Setting />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
<el-switch
|
||||
size="small"
|
||||
v-model="form_data.file_upload_enable"
|
||||
@change="switchFileUpload"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-form-item>
|
||||
<UserInputFieldTable ref="UserInputFieldTableFef" :node-model="nodeModel" />
|
||||
<ApiInputFieldTable ref="ApiInputFieldTableFef" :node-model="nodeModel" />
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="flex-between">
|
||||
<span class="mr-4">{{
|
||||
$t('views.application.applicationForm.form.voiceInput.label')
|
||||
}}</span>
|
||||
<div class="flex">
|
||||
<el-checkbox v-if="form_data.stt_model_enable" v-model="form_data.stt_autosend">{{
|
||||
$t('views.application.applicationForm.form.voiceInput.autoSend')
|
||||
}}</el-checkbox>
|
||||
<el-switch
|
||||
class="ml-8"
|
||||
size="small"
|
||||
v-model="form_data.stt_model_enable"
|
||||
@change="sttModelEnableChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<ModelSelect
|
||||
@wheel="wheel"
|
||||
v-show="form_data.stt_model_enable"
|
||||
v-model="form_data.stt_model_id"
|
||||
:placeholder="$t('views.application.applicationForm.form.voiceInput.placeholder')"
|
||||
:options="sttModelOptions"
|
||||
showFooter
|
||||
:model-type="'STT'"
|
||||
></ModelSelect>
|
||||
</el-form-item>
|
||||
<el-form-item>
|
||||
<template #label>
|
||||
<div class="flex-between">
|
||||
<span class="mr-4">{{
|
||||
$t('views.application.applicationForm.form.voicePlay.label')
|
||||
}}</span>
|
||||
<div class="flex">
|
||||
<el-checkbox v-if="form_data.tts_model_enable" v-model="form_data.tts_autoplay">{{
|
||||
$t('views.application.applicationForm.form.voicePlay.autoPlay')
|
||||
}}</el-checkbox>
|
||||
<el-switch
|
||||
class="ml-8"
|
||||
size="small"
|
||||
v-model="form_data.tts_model_enable"
|
||||
@change="ttsModelEnableChange"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<div class="w-full">
|
||||
<el-radio-group v-model="form_data.tts_type" v-show="form_data.tts_model_enable">
|
||||
<el-radio
|
||||
:label="$t('views.application.applicationForm.form.voicePlay.browser')"
|
||||
value="BROWSER"
|
||||
/>
|
||||
<el-radio
|
||||
:label="$t('views.application.applicationForm.form.voicePlay.tts')"
|
||||
value="TTS"
|
||||
/>
|
||||
</el-radio-group>
|
||||
</div>
|
||||
<div class="flex-between w-full">
|
||||
<ModelSelect
|
||||
@wheel="wheel"
|
||||
v-if="form_data.tts_type === 'TTS' && form_data.tts_model_enable"
|
||||
v-model="form_data.tts_model_id"
|
||||
:placeholder="$t('views.application.applicationForm.form.voicePlay.placeholder')"
|
||||
:options="ttsModelOptions"
|
||||
@change="ttsModelChange()"
|
||||
showFooter
|
||||
:model-type="'TTS'"
|
||||
></ModelSelect>
|
||||
|
||||
<el-button
|
||||
v-if="form_data.tts_type === 'TTS' && form_data.tts_model_enable"
|
||||
@click="openTTSParamSettingDialog"
|
||||
:disabled="!form_data.tts_model_id"
|
||||
class="ml-8"
|
||||
>
|
||||
<el-icon>
|
||||
<el-icon><Operation /></el-icon>
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<TTSModeParamSettingDialog ref="TTSModeParamSettingDialogRef" @refresh="refreshTTSForm" />
|
||||
<FileUploadSettingDialog
|
||||
ref="FileUploadSettingDialogRef"
|
||||
:node-model="nodeModel"
|
||||
@refresh="refreshFileUploadForm"
|
||||
/>
|
||||
</NodeContainer>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { app } from '@/main'
|
||||
import { groupBy, set } from 'lodash'
|
||||
import NodeContainer from '@/workflow/common/NodeContainer.vue'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import applicationApi from '@/api/application'
|
||||
import { MsgError, MsgSuccess, MsgWarning } from '@/utils/message'
|
||||
import { t } from '@/locales'
|
||||
import TTSModeParamSettingDialog from '@/views/application/component/TTSModeParamSettingDialog.vue'
|
||||
import ApiInputFieldTable from './component/ApiInputFieldTable.vue'
|
||||
import UserInputFieldTable from './component/UserInputFieldTable.vue'
|
||||
import FileUploadSettingDialog from '@/workflow/nodes/base-node/component/FileUploadSettingDialog.vue'
|
||||
|
||||
const {
|
||||
params: { id }
|
||||
} = app.config.globalProperties.$route as any
|
||||
|
||||
const props = defineProps<{ nodeModel: any }>()
|
||||
|
||||
const sttModelOptions = ref<any>(null)
|
||||
const ttsModelOptions = ref<any>(null)
|
||||
const TTSModeParamSettingDialogRef = ref<InstanceType<typeof TTSModeParamSettingDialog>>()
|
||||
const UserInputFieldTableFef = ref()
|
||||
const ApiInputFieldTableFef = ref()
|
||||
const FileUploadSettingDialogRef = ref<InstanceType<typeof FileUploadSettingDialog>>()
|
||||
|
||||
const form = {
|
||||
name: '',
|
||||
desc: '',
|
||||
prologue: t('views.application.applicationForm.form.defaultPrologue')
|
||||
}
|
||||
|
||||
const wheel = (e: any) => {
|
||||
if (e.ctrlKey === true) {
|
||||
e.preventDefault()
|
||||
return true
|
||||
} else {
|
||||
e.stopPropagation()
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
function submitDialog(val: string) {
|
||||
set(props.nodeModel.properties.node_data, 'prologue', val)
|
||||
}
|
||||
|
||||
const form_data = computed({
|
||||
get: () => {
|
||||
if (props.nodeModel.properties.node_data) {
|
||||
return props.nodeModel.properties.node_data
|
||||
} else {
|
||||
set(props.nodeModel.properties, 'node_data', form)
|
||||
}
|
||||
return props.nodeModel.properties.node_data
|
||||
},
|
||||
set: (value) => {
|
||||
set(props.nodeModel.properties, 'node_data', value)
|
||||
}
|
||||
})
|
||||
|
||||
const baseNodeFormRef = ref<FormInstance>()
|
||||
|
||||
const validate = () => {
|
||||
if (
|
||||
form_data.value.tts_model_enable &&
|
||||
!form_data.value.tts_model_id &&
|
||||
form_data.value.tts_type === 'TTS'
|
||||
) {
|
||||
return Promise.reject({
|
||||
node: props.nodeModel,
|
||||
errMessage: t('views.application.applicationForm.form.voicePlay.requiredMessage')
|
||||
})
|
||||
}
|
||||
if (form_data.value.stt_model_enable && !form_data.value.stt_model_id) {
|
||||
return Promise.reject({
|
||||
node: props.nodeModel,
|
||||
errMessage: t('views.application.applicationForm.form.voiceInput.requiredMessage')
|
||||
})
|
||||
}
|
||||
return baseNodeFormRef.value?.validate().catch((err) => {
|
||||
return Promise.reject({ node: props.nodeModel, errMessage: err })
|
||||
})
|
||||
}
|
||||
|
||||
function getSTTModel() {
|
||||
applicationApi.getApplicationSTTModel(id).then((res: any) => {
|
||||
sttModelOptions.value = groupBy(res?.data, 'provider')
|
||||
})
|
||||
}
|
||||
|
||||
function getTTSModel() {
|
||||
applicationApi.getApplicationTTSModel(id).then((res: any) => {
|
||||
ttsModelOptions.value = groupBy(res?.data, 'provider')
|
||||
})
|
||||
}
|
||||
|
||||
function ttsModelChange() {
|
||||
if (form_data.value.tts_model_id) {
|
||||
TTSModeParamSettingDialogRef.value?.reset_default(form_data.value.tts_model_id, id)
|
||||
} else {
|
||||
refreshTTSForm({})
|
||||
}
|
||||
}
|
||||
|
||||
function ttsModelEnableChange() {
|
||||
if (!form_data.value.tts_model_enable) {
|
||||
form_data.value.tts_model_id = ''
|
||||
form_data.value.tts_type = 'BROWSER'
|
||||
}
|
||||
}
|
||||
|
||||
function sttModelEnableChange() {
|
||||
if (!form_data.value.stt_model_enable) {
|
||||
form_data.value.stt_model_id = ''
|
||||
}
|
||||
}
|
||||
|
||||
const openTTSParamSettingDialog = () => {
|
||||
const model_id = form_data.value.tts_model_id
|
||||
if (!model_id) {
|
||||
MsgSuccess(t('views.application.applicationForm.form.voicePlay.requiredMessage'))
|
||||
return
|
||||
}
|
||||
TTSModeParamSettingDialogRef.value?.open(model_id, id, form_data.value.tts_model_params_setting)
|
||||
}
|
||||
|
||||
const refreshTTSForm = (data: any) => {
|
||||
form_data.value.tts_model_params_setting = data
|
||||
}
|
||||
|
||||
const switchFileUpload = () => {
|
||||
const default_upload_setting = {
|
||||
maxFiles: 3,
|
||||
fileLimit: 50,
|
||||
document: true,
|
||||
image: false,
|
||||
audio: false,
|
||||
video: false,
|
||||
other: false,
|
||||
otherExtensions: ['ppt', 'doc']
|
||||
}
|
||||
|
||||
if (form_data.value.file_upload_enable) {
|
||||
form_data.value.file_upload_setting =
|
||||
form_data.value.file_upload_setting || default_upload_setting
|
||||
}
|
||||
props.nodeModel.graphModel.eventCenter.emit('refreshFileUploadConfig')
|
||||
}
|
||||
const openFileUploadSettingDialog = () => {
|
||||
FileUploadSettingDialogRef.value?.open(form_data.value.file_upload_setting)
|
||||
}
|
||||
|
||||
const refreshFileUploadForm = (data: any) => {
|
||||
form_data.value.file_upload_setting = data
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
set(props.nodeModel, 'validate', validate)
|
||||
if (!props.nodeModel.properties.node_data.tts_type) {
|
||||
set(props.nodeModel.properties.node_data, 'tts_type', 'BROWSER')
|
||||
}
|
||||
getTTSModel()
|
||||
getSTTModel()
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
:deep(.el-form-item__label) {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
import ConditioNodeVue from './index.vue'
|
||||
import { cloneDeep, set } from 'lodash'
|
||||
import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
|
||||
class ConditioNode extends AppNode {
|
||||
constructor(props: any) {
|
||||
super(props, ConditioNodeVue)
|
||||
}
|
||||
}
|
||||
const get_up_index_height = (condition_list: Array<any>, index: number) => {
|
||||
return condition_list
|
||||
.filter((item, i) => i < index)
|
||||
.map((item) => item.height + 8)
|
||||
.reduce((x, y) => x + y, 0)
|
||||
}
|
||||
class ConditionModel extends AppNodeModel {
|
||||
refreshBranch() {
|
||||
// 更新节点连接边的path
|
||||
this.incoming.edges.forEach((edge: any) => {
|
||||
// 调用自定义的更新方案
|
||||
edge.updatePathByAnchor()
|
||||
})
|
||||
this.outgoing.edges.forEach((edge: any) => {
|
||||
edge.updatePathByAnchor()
|
||||
})
|
||||
}
|
||||
getDefaultAnchor() {
|
||||
const {
|
||||
id,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
properties: { branch_condition_list }
|
||||
} = this
|
||||
if (this.height === undefined) {
|
||||
this.height = 200
|
||||
}
|
||||
const showNode = this.properties.showNode === undefined ? true : this.properties.showNode
|
||||
const anchors: any = []
|
||||
anchors.push({
|
||||
x: x - width / 2 + 10,
|
||||
y: showNode ? y : y - 15,
|
||||
id: `${id}_left`,
|
||||
edgeAddable: false,
|
||||
type: 'left'
|
||||
})
|
||||
|
||||
if (branch_condition_list) {
|
||||
for (let index = 0; index < branch_condition_list.length; index++) {
|
||||
const element = branch_condition_list[index]
|
||||
const h = get_up_index_height(branch_condition_list, index)
|
||||
anchors.push({
|
||||
x: x + width / 2 - 10,
|
||||
y: showNode ? y - height / 2 + 75 + h + element.height / 2 : y - 15,
|
||||
id: `${id}_${element.id}_right`,
|
||||
type: 'right'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return anchors
|
||||
}
|
||||
}
|
||||
export default {
|
||||
type: 'condition-node',
|
||||
model: ConditionModel,
|
||||
view: ConditioNode
|
||||
}
|
||||
|
|
@ -0,0 +1,370 @@
|
|||
<template>
|
||||
<NodeContainer :nodeModel="nodeModel">
|
||||
<el-form
|
||||
:model="form_data"
|
||||
label-position="top"
|
||||
require-asterisk-position="right"
|
||||
label-width="auto"
|
||||
ref="ConditionNodeFormRef"
|
||||
@submit.prevent
|
||||
>
|
||||
<VueDraggable
|
||||
ref="el"
|
||||
v-bind:modelValue="form_data.branch"
|
||||
:disabled="form_data.branch === 2"
|
||||
:filter="'.no-drag'"
|
||||
handle=".handle"
|
||||
:animation="150"
|
||||
ghostClass="ghost"
|
||||
@end="onEnd"
|
||||
>
|
||||
<template v-for="(item, index) in form_data.branch" :key="item.id">
|
||||
<el-card
|
||||
v-resize="(wh: any) => resizeCondition(wh, item, index)"
|
||||
shadow="never"
|
||||
class="drag-card card-never mb-8"
|
||||
:class="{
|
||||
'no-drag': index === form_data.branch.length - 1 || form_data.branch.length === 2
|
||||
}"
|
||||
style="--el-card-padding: 12px"
|
||||
>
|
||||
<div class="handle flex-between lighter">
|
||||
<span class="flex align-center">
|
||||
<img src="@/assets/sort.svg" alt="" height="15" class="handle-img mr-4" />
|
||||
{{ item.type }}
|
||||
</span>
|
||||
<div class="info" v-if="item.conditions.length > 1">
|
||||
<span>{{
|
||||
$t('views.applicationWorkflow.nodes.conditionNode.conditions.info')
|
||||
}}</span>
|
||||
<el-select
|
||||
:teleported="false"
|
||||
v-model="item.condition"
|
||||
size="small"
|
||||
style="width: 60px; margin: 0 8px"
|
||||
>
|
||||
<el-option :label="$t('views.applicationWorkflow.condition.AND')" value="and" />
|
||||
<el-option :label="$t('views.applicationWorkflow.condition.OR')" value="or" />
|
||||
</el-select>
|
||||
<span>{{
|
||||
$t('views.applicationWorkflow.nodes.conditionNode.conditions.label')
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="index !== form_data.branch.length - 1" class="mt-8">
|
||||
<template v-for="(condition, cIndex) in item.conditions" :key="cIndex">
|
||||
<el-row :gutter="8">
|
||||
<el-col :span="11">
|
||||
<el-form-item
|
||||
:prop="'branch.' + index + '.conditions.' + cIndex + '.field'"
|
||||
:rules="{
|
||||
type: 'array',
|
||||
required: true,
|
||||
message: $t('views.applicationWorkflow.variable.placeholder'),
|
||||
trigger: 'change'
|
||||
}"
|
||||
>
|
||||
<NodeCascader
|
||||
ref="nodeCascaderRef"
|
||||
:nodeModel="nodeModel"
|
||||
class="w-full"
|
||||
:placeholder="$t('views.applicationWorkflow.variable.placeholder')"
|
||||
v-model="condition.field"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item
|
||||
:prop="'branch.' + index + '.conditions.' + cIndex + '.compare'"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: $t(
|
||||
'views.applicationWorkflow.nodes.conditionNode.conditions.requiredMessage'
|
||||
),
|
||||
trigger: 'change'
|
||||
}"
|
||||
>
|
||||
<el-select
|
||||
@wheel="wheel"
|
||||
:teleported="false"
|
||||
v-model="condition.compare"
|
||||
:placeholder="
|
||||
$t(
|
||||
'views.applicationWorkflow.nodes.conditionNode.conditions.requiredMessage'
|
||||
)
|
||||
"
|
||||
clearable
|
||||
@change="changeCondition($event, index, cIndex)"
|
||||
>
|
||||
<template v-for="(item, index) in compareList" :key="index">
|
||||
<el-option :label="item.label" :value="item.value" />
|
||||
</template>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<el-form-item
|
||||
v-if="
|
||||
!['is_null', 'is_not_null', 'is_true', 'is_not_true'].includes(
|
||||
condition.compare
|
||||
)
|
||||
"
|
||||
:prop="'branch.' + index + '.conditions.' + cIndex + '.value'"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: $t('views.applicationWorkflow.nodes.conditionNode.valueMessage'),
|
||||
trigger: 'blur'
|
||||
}"
|
||||
>
|
||||
<el-input
|
||||
v-model="condition.value"
|
||||
:placeholder="
|
||||
$t('views.applicationWorkflow.nodes.conditionNode.valueMessage')
|
||||
"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-col>
|
||||
<el-col :span="1">
|
||||
<el-button
|
||||
:disabled="index === 0 && cIndex === 0"
|
||||
link
|
||||
type="info"
|
||||
class="mt-4"
|
||||
@click="deleteCondition(index, cIndex)"
|
||||
>
|
||||
<el-icon><Delete /></el-icon>
|
||||
</el-button>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<el-button
|
||||
link
|
||||
type="primary"
|
||||
@click="addCondition(index)"
|
||||
v-if="index !== form_data.branch.length - 1"
|
||||
>
|
||||
<el-icon class="mr-4"><Plus /></el-icon>
|
||||
{{ $t('views.applicationWorkflow.nodes.conditionNode.addCondition') }}
|
||||
</el-button>
|
||||
</el-card>
|
||||
</template>
|
||||
</VueDraggable>
|
||||
<el-button link type="primary" @click="addBranch">
|
||||
<el-icon class="mr-4"><Plus /></el-icon>
|
||||
{{ $t('views.applicationWorkflow.nodes.conditionNode.addBranch') }}
|
||||
</el-button>
|
||||
</el-form>
|
||||
</NodeContainer>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { cloneDeep, set } from 'lodash'
|
||||
import NodeContainer from '@/workflow/common/NodeContainer.vue'
|
||||
import NodeCascader from '@/workflow/common/NodeCascader.vue'
|
||||
import type { FormInstance } from 'element-plus'
|
||||
import { ref, computed, onMounted, nextTick } from 'vue'
|
||||
import { randomId } from '@/utils/utils'
|
||||
import { compareList } from '@/workflow/common/data'
|
||||
import { VueDraggable } from 'vue-draggable-plus'
|
||||
|
||||
const props = defineProps<{ nodeModel: any }>()
|
||||
|
||||
const form = {
|
||||
branch: [
|
||||
{
|
||||
conditions: [
|
||||
{
|
||||
field: [],
|
||||
compare: '',
|
||||
value: ''
|
||||
}
|
||||
],
|
||||
id: randomId(),
|
||||
type: 'IF',
|
||||
condition: 'and'
|
||||
},
|
||||
{
|
||||
conditions: [],
|
||||
id: randomId(),
|
||||
type: 'ELSE',
|
||||
condition: 'and'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const wheel = (e: any) => {
|
||||
if (e.ctrlKey === true) {
|
||||
e.preventDefault()
|
||||
return true
|
||||
} else {
|
||||
e.stopPropagation()
|
||||
return true
|
||||
}
|
||||
}
|
||||
const resizeCondition = (wh: any, row: any, index: number) => {
|
||||
const branch_condition_list = cloneDeep(
|
||||
props.nodeModel.properties.branch_condition_list
|
||||
? props.nodeModel.properties.branch_condition_list
|
||||
: []
|
||||
)
|
||||
const new_branch_condition_list = branch_condition_list.map((item: any) => {
|
||||
if (item.id === row.id) {
|
||||
return { ...item, height: wh.height, index: index }
|
||||
}
|
||||
return item
|
||||
})
|
||||
set(props.nodeModel.properties, 'branch_condition_list', new_branch_condition_list)
|
||||
refreshBranchAnchor(props.nodeModel.properties.node_data.branch, true)
|
||||
}
|
||||
const form_data = computed({
|
||||
get: () => {
|
||||
if (props.nodeModel.properties.node_data) {
|
||||
return props.nodeModel.properties.node_data
|
||||
} else {
|
||||
set(props.nodeModel.properties, 'node_data', form)
|
||||
refreshBranchAnchor(form.branch, true)
|
||||
}
|
||||
return props.nodeModel.properties.node_data
|
||||
},
|
||||
set: (value) => {
|
||||
set(props.nodeModel.properties, 'node_data', value)
|
||||
}
|
||||
})
|
||||
|
||||
const ConditionNodeFormRef = ref<FormInstance>()
|
||||
const nodeCascaderRef = ref()
|
||||
const validate = () => {
|
||||
const v_list = [
|
||||
ConditionNodeFormRef.value?.validate(),
|
||||
...nodeCascaderRef.value.map((item: any) => item.validate())
|
||||
]
|
||||
return Promise.all(v_list).catch((err) => {
|
||||
return Promise.reject({ node: props.nodeModel, errMessage: err })
|
||||
})
|
||||
}
|
||||
|
||||
function onEnd(event?: any) {
|
||||
const { oldIndex, newIndex } = event
|
||||
if (oldIndex === undefined || newIndex === undefined) return
|
||||
const list = cloneDeep(props.nodeModel.properties.node_data.branch)
|
||||
if (oldIndex === list.length - 1 || newIndex === list.length - 1) {
|
||||
return
|
||||
}
|
||||
const newInstance = { ...list[oldIndex], type: list[newIndex].type, id: list[newIndex].id }
|
||||
const oldInstance = { ...list[newIndex], type: list[oldIndex].type, id: list[oldIndex].id }
|
||||
list[newIndex] = newInstance
|
||||
list[oldIndex] = oldInstance
|
||||
set(props.nodeModel.properties.node_data, 'branch', list)
|
||||
}
|
||||
|
||||
function addBranch() {
|
||||
const list = cloneDeep(props.nodeModel.properties.node_data.branch)
|
||||
const obj = {
|
||||
conditions: [
|
||||
{
|
||||
field: [],
|
||||
compare: '',
|
||||
value: ''
|
||||
}
|
||||
],
|
||||
type: 'ELSE IF ' + (list.length - 1),
|
||||
id: randomId(),
|
||||
condition: 'and'
|
||||
}
|
||||
list.splice(list.length - 1, 0, obj)
|
||||
refreshBranchAnchor(list, true)
|
||||
set(props.nodeModel.properties.node_data, 'branch', list)
|
||||
}
|
||||
function refreshBranchAnchor(list: Array<any>, is_add: boolean) {
|
||||
const branch_condition_list = cloneDeep(
|
||||
props.nodeModel.properties.branch_condition_list
|
||||
? props.nodeModel.properties.branch_condition_list
|
||||
: []
|
||||
)
|
||||
const new_branch_condition_list = list
|
||||
.map((item, index) => {
|
||||
const find = branch_condition_list.find((b: any) => b.id === item.id)
|
||||
if (find) {
|
||||
return { index: index, height: find.height, id: item.id }
|
||||
} else {
|
||||
if (is_add) {
|
||||
return { index: index, height: 12, id: item.id }
|
||||
}
|
||||
}
|
||||
})
|
||||
.filter((item) => item)
|
||||
|
||||
set(props.nodeModel.properties, 'branch_condition_list', new_branch_condition_list)
|
||||
props.nodeModel.refreshBranch()
|
||||
}
|
||||
|
||||
function addCondition(index: number) {
|
||||
const list = cloneDeep(props.nodeModel.properties.node_data.branch)
|
||||
list[index]['conditions'].push({
|
||||
field: [],
|
||||
compare: '',
|
||||
value: ''
|
||||
})
|
||||
set(props.nodeModel.properties.node_data, 'branch', list)
|
||||
}
|
||||
|
||||
function deleteCondition(index: number, cIndex: number) {
|
||||
const list = cloneDeep(props.nodeModel.properties.node_data.branch)
|
||||
list[index]['conditions'].splice(cIndex, 1)
|
||||
if (list[index]['conditions'].length === 0) {
|
||||
const delete_edge = list.splice(index, 1)
|
||||
const delete_target_anchor_id_list = delete_edge.map(
|
||||
(item: any) => props.nodeModel.id + '_' + item.id + '_right'
|
||||
)
|
||||
|
||||
props.nodeModel.graphModel.eventCenter.emit(
|
||||
'delete_edge',
|
||||
props.nodeModel.outgoing.edges
|
||||
.filter((item: any) => delete_target_anchor_id_list.includes(item.sourceAnchorId))
|
||||
.map((item: any) => item.id)
|
||||
)
|
||||
refreshBranchAnchor(list, false)
|
||||
|
||||
list.forEach((item: any, index: number) => {
|
||||
if (item.type === 'ELSE IF ' + (index + 1)) {
|
||||
item.type = 'ELSE IF ' + index
|
||||
}
|
||||
})
|
||||
}
|
||||
set(props.nodeModel.properties.node_data, 'branch', list)
|
||||
}
|
||||
|
||||
function changeCondition(val: string, index: number, cIndex: number) {
|
||||
if (['is_null', 'is_not_null', 'is_true', 'is_not_true'].includes(val)) {
|
||||
const list = cloneDeep(props.nodeModel.properties.node_data.branch)
|
||||
list[index]['conditions'][cIndex].value = 1
|
||||
set(props.nodeModel.properties.node_data, 'branch', list)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
set(props.nodeModel, 'validate', validate)
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.drag-card.no-drag {
|
||||
.handle {
|
||||
.handle-img {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.drag-card:not(.no-drag) {
|
||||
.handle {
|
||||
.handle-img {
|
||||
display: none;
|
||||
}
|
||||
&:hover {
|
||||
.handle-img {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import DocumentExtractNodeVue from './index.vue'
|
||||
import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
|
||||
class RerankerNode extends AppNode {
|
||||
constructor(props: any) {
|
||||
super(props, DocumentExtractNodeVue)
|
||||
}
|
||||
}
|
||||
export default {
|
||||
type: 'document-extract-node',
|
||||
model: AppNodeModel,
|
||||
view: RerankerNode
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
<template>
|
||||
<NodeContainer :nodeModel="nodeModel">
|
||||
<h5 class="title-decoration-1 mb-8">{{ $t('views.applicationWorkflow.nodeSetting') }}</h5>
|
||||
<el-card shadow="never" class="card-never">
|
||||
<el-form
|
||||
@submit.prevent
|
||||
:model="form_data"
|
||||
label-position="top"
|
||||
require-asterisk-position="right"
|
||||
label-width="auto"
|
||||
ref="DatasetNodeFormRef"
|
||||
>
|
||||
<el-form-item :label="$t('views.problem.relateParagraph.selectDocument')" :rules="{
|
||||
type: 'array',
|
||||
required: true,
|
||||
message: $t('views.log.documentPlaceholder'),
|
||||
trigger: 'change'
|
||||
}"
|
||||
>
|
||||
<NodeCascader
|
||||
ref="nodeCascaderRef"
|
||||
:nodeModel="nodeModel"
|
||||
class="w-full"
|
||||
:placeholder="$t('views.log.documentPlaceholder')"
|
||||
v-model="form_data.document_list"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
</NodeContainer>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import NodeContainer from '@/workflow/common/NodeContainer.vue'
|
||||
import { computed } from 'vue'
|
||||
import { set } from 'lodash'
|
||||
import NodeCascader from '@/workflow/common/NodeCascader.vue'
|
||||
|
||||
const props = defineProps<{ nodeModel: any }>()
|
||||
|
||||
const form = {
|
||||
document_list: ["start-node", "document"]
|
||||
}
|
||||
|
||||
|
||||
const form_data = computed({
|
||||
get: () => {
|
||||
if (props.nodeModel.properties.node_data) {
|
||||
return props.nodeModel.properties.node_data
|
||||
} else {
|
||||
set(props.nodeModel.properties, 'node_data', form)
|
||||
}
|
||||
return props.nodeModel.properties.node_data
|
||||
},
|
||||
set: (value) => {
|
||||
set(props.nodeModel.properties, 'node_data', value)
|
||||
}
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
</style>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import FormNodeVue from './index.vue'
|
||||
import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
|
||||
class FormNode extends AppNode {
|
||||
constructor(props: any) {
|
||||
super(props, FormNodeVue)
|
||||
}
|
||||
}
|
||||
export default {
|
||||
type: 'form-node',
|
||||
model: AppNodeModel,
|
||||
view: FormNode
|
||||
}
|
||||
|
|
@ -0,0 +1,281 @@
|
|||
<template>
|
||||
<NodeContainer :nodeModel="nodeModel">
|
||||
<h5 class="title-decoration-1 mb-8">{{ $t('views.applicationWorkflow.nodeSetting') }}</h5>
|
||||
<el-card shadow="never" class="card-never" style="--el-card-padding: 12px">
|
||||
<el-form
|
||||
@submit.prevent
|
||||
:model="form_data"
|
||||
label-position="top"
|
||||
require-asterisk-position="right"
|
||||
label-width="auto"
|
||||
ref="formNodeFormRef"
|
||||
hide-required-asterisk
|
||||
>
|
||||
<el-form-item
|
||||
:label="$t('views.applicationWorkflow.nodes.formNode.formContent.label')"
|
||||
prop="form_content_format"
|
||||
:rules="{
|
||||
required: true,
|
||||
message: $t('views.applicationWorkflow.nodes.formNode.formContent.requiredMessage'),
|
||||
trigger: 'blur'
|
||||
}"
|
||||
>
|
||||
<template #label>
|
||||
<div class="flex align-center">
|
||||
<div class="mr-4">
|
||||
<span
|
||||
>{{ $t('views.applicationWorkflow.nodes.formNode.formContent.label')
|
||||
}}<span class="danger">*</span></span
|
||||
>
|
||||
</div>
|
||||
<el-tooltip effect="dark" placement="right" popper-class="max-w-200">
|
||||
<template #content>
|
||||
{{
|
||||
$t('views.applicationWorkflow.nodes.formNode.formContent.tooltip', {
|
||||
form: '{ form }'
|
||||
})
|
||||
}}
|
||||
</template>
|
||||
<AppIcon iconName="app-warning" class="app-warning-icon"></AppIcon>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<MdEditorMagnify
|
||||
:title="$t('views.applicationWorkflow.nodes.formNode.formContent.label')"
|
||||
v-model="form_data.form_content_format"
|
||||
style="height: 150px"
|
||||
@submitDialog="submitDialog"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item
|
||||
:label="$t('views.applicationWorkflow.nodes.formNode.formSetting')"
|
||||
@click.prevent
|
||||
>
|
||||
<template #label>
|
||||
<div class="flex-between">
|
||||
<h5 class="lighter">
|
||||
{{ $t('views.applicationWorkflow.nodes.formNode.formSetting') }}
|
||||
</h5>
|
||||
<el-button link type="primary" @click="openAddFormCollect()">
|
||||
<el-icon class="mr-4">
|
||||
<Plus />
|
||||
</el-icon>
|
||||
{{ $t('common.add') }}
|
||||
</el-button>
|
||||
</div></template
|
||||
>
|
||||
|
||||
<el-table
|
||||
class="border"
|
||||
v-if="form_data.form_field_list.length > 0"
|
||||
:data="form_data.form_field_list"
|
||||
ref="tableRef"
|
||||
row-key="field"
|
||||
>
|
||||
<el-table-column
|
||||
prop="field"
|
||||
:label="$t('dynamicsForm.paramForm.field.label')"
|
||||
width="95"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<span :title="row.field" class="ellipsis-1">{{ row.field }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="label" :label="$t('dynamicsForm.paramForm.name.label')">
|
||||
<template #default="{ row }">
|
||||
<span v-if="row.label && row.label.input_type === 'TooltipLabel'">
|
||||
<span :title="row.label.label" class="ellipsis-1">
|
||||
{{ row.label.label }}
|
||||
</span>
|
||||
</span>
|
||||
<span v-else>
|
||||
<span :title="row.label" class="ellipsis-1">
|
||||
{{ row.label }}
|
||||
</span></span
|
||||
>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column :label="$t('dynamicsForm.paramForm.input_type.label')" width="110px">
|
||||
<template #default="{ row }">
|
||||
<el-tag type="info" class="info-tag">{{
|
||||
input_type_list.find((item) => item.value === row.input_type)?.label
|
||||
}}</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
|
||||
<el-table-column prop="default_value" :label="$t('dynamicsForm.default.label')">
|
||||
<template #default="{ row }">
|
||||
<span :title="row.default_value" class="ellipsis-1">{{
|
||||
getDefaultValue(row)
|
||||
}}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('common.required')" width="85">
|
||||
<template #default="{ row }">
|
||||
<div @click.stop>
|
||||
<el-switch disabled size="small" v-model="row.required" />
|
||||
</div>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column :label="$t('common.operation')" align="left" width="90">
|
||||
<template #default="{ row, $index }">
|
||||
<span class="mr-4">
|
||||
<el-tooltip effect="dark" :content="$t('common.modify')" placement="top">
|
||||
<el-button type="primary" text @click.stop="openEditFormCollect(row, $index)">
|
||||
<el-icon><EditPen /></el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</span>
|
||||
<el-tooltip effect="dark" :content="$t('common.delete')" placement="top">
|
||||
<el-button type="primary" text @click="deleteField(row)">
|
||||
<el-icon>
|
||||
<Delete />
|
||||
</el-icon>
|
||||
</el-button>
|
||||
</el-tooltip>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</el-card>
|
||||
<AddFormCollect ref="addFormCollectRef" :addFormField="addFormField"></AddFormCollect>
|
||||
<EditFormCollect ref="editFormCollectRef" :editFormField="editFormField"></EditFormCollect>
|
||||
</NodeContainer>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import NodeContainer from '@/workflow/common/NodeContainer.vue'
|
||||
import AddFormCollect from '@/workflow/common/AddFormCollect.vue'
|
||||
import EditFormCollect from '@/workflow/common/EditFormCollect.vue'
|
||||
import { type FormInstance } from 'element-plus'
|
||||
import { ref, onMounted, computed } from 'vue'
|
||||
import { input_type_list } from '@/components/dynamics-form/constructor/data'
|
||||
import { MsgError } from '@/utils/message'
|
||||
import { set, cloneDeep } from 'lodash'
|
||||
import Sortable from 'sortablejs'
|
||||
import { t } from '@/locales'
|
||||
const props = defineProps<{ nodeModel: any }>()
|
||||
const formNodeFormRef = ref<FormInstance>()
|
||||
const tableRef = ref()
|
||||
const editFormField = (form_field_data: any, field_index: number) => {
|
||||
const _value = form_data.value.form_field_list.map((item: any, index: number) => {
|
||||
if (field_index === index) {
|
||||
return cloneDeep(form_field_data)
|
||||
}
|
||||
return cloneDeep(item)
|
||||
})
|
||||
form_data.value.form_field_list = _value
|
||||
sync_form_field_list()
|
||||
}
|
||||
const addFormField = (form_field_data: any) => {
|
||||
if (form_data.value.form_field_list.some((field: any) => field.field === form_field_data.field)) {
|
||||
MsgError(t('views.applicationWorkflow.tip.paramErrorMessage') + form_field_data.field)
|
||||
return
|
||||
}
|
||||
form_data.value.form_field_list = cloneDeep([...form_data.value.form_field_list, form_field_data])
|
||||
sync_form_field_list()
|
||||
}
|
||||
const sync_form_field_list = () => {
|
||||
const fields = [
|
||||
{
|
||||
label: t('views.applicationWorkflow.nodes.formNode.formAllContent'),
|
||||
value: 'form_data'
|
||||
},
|
||||
...form_data.value.form_field_list.map((item: any) => ({
|
||||
value: item.field,
|
||||
label: typeof item.label == 'string' ? item.label : item.label.label
|
||||
}))
|
||||
]
|
||||
set(props.nodeModel.properties.config, 'fields', fields)
|
||||
props.nodeModel.clear_next_node_field(false)
|
||||
onDragHandle()
|
||||
}
|
||||
const addFormCollectRef = ref<InstanceType<typeof AddFormCollect>>()
|
||||
const editFormCollectRef = ref<InstanceType<typeof EditFormCollect>>()
|
||||
const openAddFormCollect = () => {
|
||||
addFormCollectRef.value?.open()
|
||||
}
|
||||
const openEditFormCollect = (form_field_data: any, index: number) => {
|
||||
editFormCollectRef.value?.open(cloneDeep(form_field_data), index)
|
||||
}
|
||||
const deleteField = (form_field_data: any) => {
|
||||
form_data.value.form_field_list = form_data.value.form_field_list.filter(
|
||||
(field: any) => field.field !== form_field_data.field
|
||||
)
|
||||
sync_form_field_list()
|
||||
}
|
||||
const form = ref<any>({
|
||||
is_result: true,
|
||||
form_content_format: `${t('views.applicationWorkflow.nodes.formNode.form_content_format1')}
|
||||
{{form}}
|
||||
${t('views.applicationWorkflow.nodes.formNode.form_content_format2')}`,
|
||||
form_field_list: []
|
||||
})
|
||||
const form_data = computed({
|
||||
get: () => {
|
||||
if (props.nodeModel.properties.node_data) {
|
||||
return props.nodeModel.properties.node_data
|
||||
} else {
|
||||
set(props.nodeModel.properties, 'node_data', form.value)
|
||||
}
|
||||
return props.nodeModel.properties.node_data
|
||||
},
|
||||
set: (value) => {
|
||||
set(props.nodeModel.properties, 'node_data', value)
|
||||
}
|
||||
})
|
||||
|
||||
const getDefaultValue = (row: any) => {
|
||||
if (row.default_value) {
|
||||
const default_value = row.option_list
|
||||
?.filter((v: any) => row.default_value.indexOf(v.value) > -1)
|
||||
.map((v: any) => v.label)
|
||||
.join(',')
|
||||
if (default_value) {
|
||||
return default_value
|
||||
}
|
||||
return row.default_value
|
||||
}
|
||||
if (row.default_value !== undefined) {
|
||||
return row.default_value
|
||||
}
|
||||
}
|
||||
|
||||
const validate = () => {
|
||||
return formNodeFormRef.value?.validate()
|
||||
}
|
||||
function submitDialog(val: string) {
|
||||
set(props.nodeModel.properties.node_data, 'form_content_format', val)
|
||||
}
|
||||
|
||||
// 表格排序拖拽
|
||||
function onDragHandle() {
|
||||
if (!tableRef.value) return
|
||||
|
||||
// 获取表格的 tbody DOM 元素
|
||||
const wrapper = tableRef.value.$el as HTMLElement
|
||||
const tbody = wrapper.querySelector('.el-table__body-wrapper tbody')
|
||||
if (!tbody) return
|
||||
// 初始化 Sortable
|
||||
Sortable.create(tbody as HTMLElement, {
|
||||
animation: 150,
|
||||
ghostClass: 'ghost-row',
|
||||
onEnd: (evt) => {
|
||||
if (evt.oldIndex === undefined || evt.newIndex === undefined) return
|
||||
// 更新数据顺序
|
||||
const items = [...form_data.value.form_field_list]
|
||||
const [movedItem] = items.splice(evt.oldIndex, 1)
|
||||
items.splice(evt.newIndex, 0, movedItem)
|
||||
form_data.value.form_field_list = items
|
||||
sync_form_field_list()
|
||||
}
|
||||
})
|
||||
}
|
||||
onMounted(() => {
|
||||
set(props.nodeModel, 'validate', validate)
|
||||
sync_form_field_list()
|
||||
props.nodeModel.graphModel.eventCenter.emit('refresh_incoming_node_field')
|
||||
})
|
||||
</script>
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
import FunctionLibNodeVue from './index.vue'
|
||||
import { AppNode, AppNodeModel } from '@/workflow/common/app-node'
|
||||
class FunctionLibNode extends AppNode {
|
||||
constructor(props: any) {
|
||||
super(props, FunctionLibNodeVue)
|
||||
}
|
||||
}
|
||||
export default {
|
||||
type: 'function-lib-node',
|
||||
model: AppNodeModel,
|
||||
view: FunctionLibNode
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue