feat: 归档L03与L09审批发布能力
- 新增统一审批中心与审批管理页面,支持流程配置、审批详情与角色/用户审批对象 - 接入聊天助手、知识库、工作流的发布与删除审批,并补齐发布态校验与快照展示
This commit is contained in:
@@ -53,6 +53,22 @@ export const removeBotFromId = (id: string) => {
|
||||
return api.post<RequestResult>('/api/v1/bot/remove', { id });
|
||||
};
|
||||
|
||||
/** 提交 Bot 发布审批 */
|
||||
export const submitBotPublishApproval = (id: string) => {
|
||||
return api.post<RequestResult<number | string>>(
|
||||
'/api/v1/bot/submitPublishApproval',
|
||||
{ id },
|
||||
);
|
||||
};
|
||||
|
||||
/** 提交 Bot 删除审批 */
|
||||
export const submitBotDeleteApproval = (id: string) => {
|
||||
return api.post<RequestResult<number | string>>(
|
||||
'/api/v1/bot/submitDeleteApproval',
|
||||
{ id },
|
||||
);
|
||||
};
|
||||
|
||||
export interface GetMessageListParams {
|
||||
conversationId: string;
|
||||
botId: string;
|
||||
|
||||
@@ -23,7 +23,7 @@ export type ActionTone = 'danger' | 'default';
|
||||
|
||||
export interface ActionButton {
|
||||
icon?: any;
|
||||
text: string;
|
||||
text: ((row: any) => string) | string;
|
||||
className?: string;
|
||||
permission?: string;
|
||||
placement?: ActionPlacement;
|
||||
@@ -135,6 +135,10 @@ function handleActionClick(event: Event, action: ActionButton, item: any) {
|
||||
event.stopPropagation();
|
||||
action.onClick(item);
|
||||
}
|
||||
|
||||
function resolveActionText(action: ActionButton, item: any) {
|
||||
return typeof action.text === 'function' ? action.text(item) : action.text;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -215,8 +219,8 @@ function handleActionClick(event: Event, action: ActionButton, item: any) {
|
||||
@click.stop
|
||||
>
|
||||
<ElButton
|
||||
v-for="action in inlineActions"
|
||||
:key="action.text"
|
||||
v-for="(action, actionIndex) in inlineActions"
|
||||
:key="`${item.id ?? index}-inline-${actionIndex}`"
|
||||
:icon="typeof action.icon === 'string' ? undefined : action.icon"
|
||||
size="small"
|
||||
class="card-action-btn"
|
||||
@@ -227,7 +231,7 @@ function handleActionClick(event: Event, action: ActionButton, item: any) {
|
||||
<template v-if="typeof action.icon === 'string'" #icon>
|
||||
<IconifyIcon :icon="action.icon" />
|
||||
</template>
|
||||
{{ action.text }}
|
||||
{{ resolveActionText(action, item) }}
|
||||
</ElButton>
|
||||
|
||||
<ElDropdown
|
||||
@@ -244,8 +248,8 @@ function handleActionClick(event: Event, action: ActionButton, item: any) {
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem
|
||||
v-for="action in menuActions"
|
||||
:key="action.text"
|
||||
v-for="(action, actionIndex) in menuActions"
|
||||
:key="`${item.id ?? index}-menu-${actionIndex}`"
|
||||
:class="{
|
||||
'card-menu-item--danger': action.tone === 'danger',
|
||||
}"
|
||||
@@ -259,7 +263,7 @@ function handleActionClick(event: Event, action: ActionButton, item: any) {
|
||||
/>
|
||||
<component v-else :is="action.icon" />
|
||||
</ElIcon>
|
||||
<span>{{ action.text }}</span>
|
||||
<span>{{ resolveActionText(action, item) }}</span>
|
||||
</div>
|
||||
</ElDropdownItem>
|
||||
</ElDropdownMenu>
|
||||
|
||||
@@ -62,6 +62,15 @@
|
||||
"subProcess": "SubProcess",
|
||||
"workflowSelect": "WorkflowSelect",
|
||||
"bochaSearch": "BochaSearch",
|
||||
"publishStatusDraft": "Draft",
|
||||
"publishStatusPublishPending": "Publish Pending",
|
||||
"publishStatusPublished": "Published",
|
||||
"publishStatusDeletePending": "Delete Pending",
|
||||
"publishStatusLabel": "Release",
|
||||
"submitPublishApprovalConfirm": "The current draft will enter the publish approval flow. It becomes externally available only after approval.",
|
||||
"submitDeleteApprovalConfirm": "The workflow will enter the delete approval flow. It will be physically deleted only after approval.",
|
||||
"publishPendingHint": "There is already an approval in progress for this workflow.",
|
||||
"deletePendingHint": "There is already an approval in progress for this workflow.",
|
||||
"check": "Check",
|
||||
"checkPassed": "Workflow check passed",
|
||||
"checkFailed": "Workflow check failed. Please fix the issues first",
|
||||
|
||||
161
easyflow-ui-admin/app/src/locales/langs/en-US/approval.json
Normal file
161
easyflow-ui-admin/app/src/locales/langs/en-US/approval.json
Normal file
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"title": "Approval Detail",
|
||||
"tab": {
|
||||
"flow": "Flow Config",
|
||||
"pending": "Pending",
|
||||
"processed": "Processed",
|
||||
"initiated": "Initiated"
|
||||
},
|
||||
"resource": {
|
||||
"bot": "Chat Assistant",
|
||||
"workflow": "Workflow",
|
||||
"knowledge": "Knowledge Base"
|
||||
},
|
||||
"action": {
|
||||
"publish": "Publish",
|
||||
"delete": "Delete",
|
||||
"addFlow": "New Flow",
|
||||
"editFlow": "Edit Flow",
|
||||
"enableFlow": "Enable Flow",
|
||||
"disableFlow": "Disable Flow",
|
||||
"approve": "Approve",
|
||||
"reject": "Reject",
|
||||
"revoke": "Revoke",
|
||||
"addScope": "Add Scope",
|
||||
"addStep": "Add Step"
|
||||
},
|
||||
"scope": {
|
||||
"category": "Category",
|
||||
"dept": "Department"
|
||||
},
|
||||
"assignee": {
|
||||
"role": "Role",
|
||||
"user": "User"
|
||||
},
|
||||
"status": {
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"pending": "Pending",
|
||||
"processing": "Processing",
|
||||
"approved": "Approved",
|
||||
"rejected": "Rejected",
|
||||
"revoked": "Revoked"
|
||||
},
|
||||
"section": {
|
||||
"basic": "Basic Info",
|
||||
"scope": "Scope Config",
|
||||
"steps": "Approval Steps",
|
||||
"tasks": "Approval Tasks",
|
||||
"logs": "Approval Logs",
|
||||
"snapshot": "Approval Snapshot"
|
||||
},
|
||||
"helper": {
|
||||
"scope": "Limit the flow by resource category or applicant department. Leave empty to apply globally.",
|
||||
"scopeEmpty": "No scope configured. This flow applies globally."
|
||||
},
|
||||
"fields": {
|
||||
"flowName": "Flow Name",
|
||||
"resourceType": "Resource Type",
|
||||
"actionType": "Action Type",
|
||||
"priority": "Priority",
|
||||
"version": "Version",
|
||||
"status": "Status",
|
||||
"remark": "Remark",
|
||||
"scopeSummary": "Scope Summary",
|
||||
"stepCount": "Step Count",
|
||||
"includeChildren": "Include Children",
|
||||
"stepName": "Step Name",
|
||||
"stepNoLabel": "Step No.",
|
||||
"currentStep": "Current Step",
|
||||
"summary": "Summary",
|
||||
"resourceId": "Resource ID",
|
||||
"taskId": "Approval Task ID",
|
||||
"applicant": "Applicant",
|
||||
"applicantId": "Applicant ID",
|
||||
"submittedAt": "Submitted At",
|
||||
"finishedAt": "Finished At",
|
||||
"assigneeTarget": "Assignee",
|
||||
"actedBy": "Acted By",
|
||||
"actedAt": "Acted At",
|
||||
"comment": "Comment",
|
||||
"eventType": "Event Type",
|
||||
"operatorId": "Operator ID",
|
||||
"operatorName": "Operator Name",
|
||||
"createdAt": "Created At",
|
||||
"eventInfo": "Event Info",
|
||||
"stepNo": "Step {value}"
|
||||
},
|
||||
"event": {
|
||||
"approved": "Approved",
|
||||
"rejected": "Rejected",
|
||||
"revoked": "Revoked",
|
||||
"stepCreated": "Step Created",
|
||||
"submitted": "Submitted"
|
||||
},
|
||||
"placeholder": {
|
||||
"flowName": "Search flow name",
|
||||
"keyword": "Search summary",
|
||||
"resourceType": "Filter resource type",
|
||||
"actionType": "Filter action type",
|
||||
"flowStatus": "Filter flow status",
|
||||
"instanceStatus": "Status",
|
||||
"scopeValue": "Select scope value",
|
||||
"assigneeType": "Select assignee type",
|
||||
"assigneeTarget": "Select assignee",
|
||||
"stepName": "Enter step name",
|
||||
"actionComment": "Enter a comment"
|
||||
},
|
||||
"message": {
|
||||
"needStep": "At least one step is required",
|
||||
"needStepAssignee": "Each approval step requires an assignee",
|
||||
"saveSuccess": "Flow saved",
|
||||
"statusUpdated": "Flow status updated",
|
||||
"deleteSuccess": "Flow deleted",
|
||||
"actionSuccess": "Approval action completed",
|
||||
"confirmDeleteFlow": "This action cannot be undone. Continue?",
|
||||
"confirmFlowStatus": "Confirm to {title}?",
|
||||
"eventApproved": "Approval completed",
|
||||
"eventApprovedStep": "Step {value} approved",
|
||||
"eventRejected": "Approval rejected",
|
||||
"eventRejectedStep": "Step {value} rejected",
|
||||
"eventRevoked": "Approval revoked",
|
||||
"eventRevokedStep": "Step {value} revoked",
|
||||
"workflowSnapshotUntitled": "Untitled workflow snapshot",
|
||||
"workflowSnapshotMissing": "Workflow snapshot not found",
|
||||
"workflowSnapshotParseFailed": "Failed to parse workflow snapshot"
|
||||
},
|
||||
"snapshot": {
|
||||
"knowledgeBasic": "Basic Info",
|
||||
"knowledgeConfig": "Retrieval Config",
|
||||
"botOverview": "Assistant Overview",
|
||||
"botModelConfig": "Model Config",
|
||||
"botBindings": "Capability Bindings",
|
||||
"systemPrompt": "System Prompt",
|
||||
"department": "Department",
|
||||
"category": "Category",
|
||||
"modelName": "Model Name",
|
||||
"vectorStoreType": "Vector Store Type",
|
||||
"vectorEmbedModel": "Embedding Model",
|
||||
"rerankModel": "Rerank Model",
|
||||
"maxMessageCount": "Max Context Messages",
|
||||
"canUpdateEmbeddingModel": "Allow Embedding Update",
|
||||
"anonymousEnabled": "Anonymous Access",
|
||||
"anonymousDisabled": "Login Only",
|
||||
"knowledgeBindings": "Knowledge Bases",
|
||||
"workflowBindings": "Workflows",
|
||||
"pluginBindings": "Plugins",
|
||||
"mcpBindings": "MCP",
|
||||
"expandPrompt": "Expand",
|
||||
"collapsePrompt": "Collapse",
|
||||
"enabled": "Enabled",
|
||||
"disabled": "Disabled",
|
||||
"notConfigured": "Not configured",
|
||||
"noBindings": "No bindings",
|
||||
"untitledKnowledge": "Untitled knowledge base",
|
||||
"untitledBot": "Untitled assistant",
|
||||
"unnamedKnowledge": "Unnamed knowledge base",
|
||||
"unnamedWorkflow": "Unnamed workflow",
|
||||
"unnamedPlugin": "Unnamed plugin",
|
||||
"unnamedMcp": "Unnamed MCP"
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,20 @@
|
||||
"deepThinking": "DeepThinking",
|
||||
"enableDeepThinking": "EnableDeepThinking",
|
||||
"publish": "Publish",
|
||||
"publishStatusLabel": "Current Release",
|
||||
"publishStatusDraft": "Draft",
|
||||
"publishStatusDraftDesc": "Only the draft is saved. External chat and Public API are still unavailable.",
|
||||
"publishStatusPublishPending": "Publish Pending",
|
||||
"publishStatusPublishPendingDesc": "The assistant will switch to the new release after approval.",
|
||||
"publishStatusPublished": "Published",
|
||||
"publishStatusPublishedDesc": "The current release is externally available. Ongoing edits still stay in draft.",
|
||||
"publishStatusDeletePending": "Delete Pending",
|
||||
"publishStatusDeletePendingDesc": "The current release remains available, but it is no longer offered as a new binding candidate.",
|
||||
"submitPublishApprovalConfirm": "Submit the current draft to publish approval. It becomes externally available only after approval.",
|
||||
"submitDeleteApprovalConfirm": "Submit the bot to delete approval. It will be physically deleted only after approval.",
|
||||
"publishPendingHint": "There is already an approval in progress for this bot.",
|
||||
"deletePendingHint": "There is already an approval in progress for this bot.",
|
||||
"publishRequiredHint": "There is no released version yet. Submit publish approval first.",
|
||||
"postToWeChatOfficialAccount": "PostToWeChatOfficialAccount",
|
||||
"publishExternalLink": "Publish External Chat Link",
|
||||
"configured": "Configured",
|
||||
|
||||
@@ -41,7 +41,10 @@
|
||||
"markAsResolved": "MarkAsResolved",
|
||||
"optimizing": "Optimizing",
|
||||
"regenerate": "Regenerate",
|
||||
"republish": "Republish",
|
||||
"hide": "Hide",
|
||||
"more": "Mode",
|
||||
"submitDeleteApproval": "Submit Delete Approval",
|
||||
"submitPublishApproval": "Submit Publish Approval",
|
||||
"viewSegmentation": "ViewSegmentation"
|
||||
}
|
||||
|
||||
@@ -35,6 +35,15 @@
|
||||
"documentType": "DocumentType",
|
||||
"fileName": "fileName",
|
||||
"knowledgeCount": "Number of knowledge items",
|
||||
"publishStatusDraft": "Draft",
|
||||
"publishStatusPublishPending": "Publish Pending",
|
||||
"publishStatusPublished": "Published",
|
||||
"publishStatusDeletePending": "Delete Pending",
|
||||
"publishStatusLabel": "Release",
|
||||
"submitPublishApprovalConfirm": "The knowledge base will enter the publish approval flow. It can be referenced by bots only after approval.",
|
||||
"submitDeleteApprovalConfirm": "The knowledge base will enter the delete approval flow. It will be physically deleted only after approval.",
|
||||
"publishPendingHint": "There is already an approval in progress for this knowledge base.",
|
||||
"deletePendingHint": "There is already an approval in progress for this knowledge base.",
|
||||
"createdModifyTime": "Creation/update time",
|
||||
"documentList": "documentList",
|
||||
"knowledgeRetrieval": "knowledgeRetrieval",
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
"sysJob": "Job",
|
||||
"sysLog": "Log",
|
||||
"sysFeedback": "UserFeedback",
|
||||
"approval": "Approval",
|
||||
"approvalFlow": "Flow Setup",
|
||||
"approvalPending": "Pending",
|
||||
"approvalProcessed": "Processed",
|
||||
"approvalInitiated": "Initiated",
|
||||
"sysAppearance": "Appearance",
|
||||
"oauth": "OAuth"
|
||||
},
|
||||
|
||||
@@ -62,6 +62,15 @@
|
||||
"subProcess": "子流程",
|
||||
"workflowSelect": "工作流选择",
|
||||
"bochaSearch": "博查搜索",
|
||||
"publishStatusDraft": "草稿",
|
||||
"publishStatusPublishPending": "发布审批中",
|
||||
"publishStatusPublished": "已发布",
|
||||
"publishStatusDeletePending": "删除审批中",
|
||||
"publishStatusLabel": "发布状态",
|
||||
"submitPublishApprovalConfirm": "提交后会进入发布审批,审批通过后新版本才会正式对外可用。",
|
||||
"submitDeleteApprovalConfirm": "提交后会进入删除审批,审批通过后将执行真实删除。",
|
||||
"publishPendingHint": "当前工作流已有进行中的审批,请等待处理完成。",
|
||||
"deletePendingHint": "当前工作流已有进行中的审批,请等待处理完成。",
|
||||
"check": "检查",
|
||||
"checkPassed": "工作流检查通过",
|
||||
"checkFailed": "工作流检查未通过,请先修复问题",
|
||||
|
||||
161
easyflow-ui-admin/app/src/locales/langs/zh-CN/approval.json
Normal file
161
easyflow-ui-admin/app/src/locales/langs/zh-CN/approval.json
Normal file
@@ -0,0 +1,161 @@
|
||||
{
|
||||
"title": "审批详情",
|
||||
"tab": {
|
||||
"flow": "流程配置",
|
||||
"pending": "待审批",
|
||||
"processed": "已审批",
|
||||
"initiated": "我发起"
|
||||
},
|
||||
"resource": {
|
||||
"bot": "聊天助手",
|
||||
"workflow": "工作流",
|
||||
"knowledge": "知识库"
|
||||
},
|
||||
"action": {
|
||||
"publish": "发布",
|
||||
"delete": "删除",
|
||||
"addFlow": "新建流程",
|
||||
"editFlow": "编辑流程",
|
||||
"enableFlow": "启用流程",
|
||||
"disableFlow": "停用流程",
|
||||
"approve": "通过",
|
||||
"reject": "驳回",
|
||||
"revoke": "撤回",
|
||||
"addScope": "新增范围",
|
||||
"addStep": "新增步骤"
|
||||
},
|
||||
"scope": {
|
||||
"category": "分类",
|
||||
"dept": "部门"
|
||||
},
|
||||
"assignee": {
|
||||
"role": "角色",
|
||||
"user": "用户"
|
||||
},
|
||||
"status": {
|
||||
"enabled": "启用",
|
||||
"disabled": "停用",
|
||||
"pending": "待审批",
|
||||
"processing": "审批中",
|
||||
"approved": "已通过",
|
||||
"rejected": "已驳回",
|
||||
"revoked": "已撤回"
|
||||
},
|
||||
"section": {
|
||||
"basic": "基础信息",
|
||||
"scope": "范围配置",
|
||||
"steps": "审批步骤",
|
||||
"tasks": "审批任务",
|
||||
"logs": "审批日志",
|
||||
"snapshot": "审批快照"
|
||||
},
|
||||
"helper": {
|
||||
"scope": "按资源分类或申请人部门限定流程命中范围;留空时默认对全部资源生效。",
|
||||
"scopeEmpty": "未配置范围,当前流程默认全局生效。"
|
||||
},
|
||||
"fields": {
|
||||
"flowName": "流程名称",
|
||||
"resourceType": "资源类型",
|
||||
"actionType": "动作类型",
|
||||
"priority": "优先级",
|
||||
"version": "版本",
|
||||
"status": "状态",
|
||||
"remark": "备注",
|
||||
"scopeSummary": "命中范围",
|
||||
"stepCount": "步骤数",
|
||||
"includeChildren": "包含子级",
|
||||
"stepName": "步骤名称",
|
||||
"stepNoLabel": "步骤序号",
|
||||
"currentStep": "当前步骤",
|
||||
"summary": "审批摘要",
|
||||
"resourceId": "资源ID",
|
||||
"taskId": "审批任务ID",
|
||||
"applicant": "申请人",
|
||||
"applicantId": "申请人ID",
|
||||
"submittedAt": "提交时间",
|
||||
"finishedAt": "完成时间",
|
||||
"assigneeTarget": "审批对象",
|
||||
"actedBy": "处理人",
|
||||
"actedAt": "处理时间",
|
||||
"comment": "处理意见",
|
||||
"eventType": "事件类型",
|
||||
"operatorId": "操作人ID",
|
||||
"operatorName": "操作人名称",
|
||||
"createdAt": "创建时间",
|
||||
"eventInfo": "事件信息",
|
||||
"stepNo": "第 {value} 步"
|
||||
},
|
||||
"event": {
|
||||
"approved": "审批通过",
|
||||
"rejected": "审批驳回",
|
||||
"revoked": "审批撤回",
|
||||
"stepCreated": "步骤创建",
|
||||
"submitted": "提交审批"
|
||||
},
|
||||
"placeholder": {
|
||||
"flowName": "搜索流程名称",
|
||||
"keyword": "搜索审批摘要",
|
||||
"resourceType": "筛选资源类型",
|
||||
"actionType": "筛选动作类型",
|
||||
"flowStatus": "筛选流程状态",
|
||||
"instanceStatus": "审批状态",
|
||||
"scopeValue": "请选择范围值",
|
||||
"assigneeType": "请选择审批方式",
|
||||
"assigneeTarget": "请选择审批对象",
|
||||
"stepName": "请输入步骤名称",
|
||||
"actionComment": "请输入处理说明"
|
||||
},
|
||||
"message": {
|
||||
"needStep": "至少需要一个审批步骤",
|
||||
"needStepAssignee": "每个审批步骤都需要配置审批对象",
|
||||
"saveSuccess": "审批流程已保存",
|
||||
"statusUpdated": "流程状态已更新",
|
||||
"deleteSuccess": "流程已删除",
|
||||
"actionSuccess": "审批操作已完成",
|
||||
"confirmDeleteFlow": "删除后将无法恢复,确认继续吗?",
|
||||
"confirmFlowStatus": "确认执行{title}吗?",
|
||||
"eventApproved": "审批已通过",
|
||||
"eventApprovedStep": "第 {value} 步已通过",
|
||||
"eventRejected": "审批已驳回",
|
||||
"eventRejectedStep": "第 {value} 步已驳回",
|
||||
"eventRevoked": "审批已撤回",
|
||||
"eventRevokedStep": "第 {value} 步已撤回",
|
||||
"workflowSnapshotUntitled": "未命名工作流快照",
|
||||
"workflowSnapshotMissing": "未找到工作流快照",
|
||||
"workflowSnapshotParseFailed": "工作流快照解析失败"
|
||||
},
|
||||
"snapshot": {
|
||||
"knowledgeBasic": "基础信息",
|
||||
"knowledgeConfig": "检索配置",
|
||||
"botOverview": "助手概览",
|
||||
"botModelConfig": "模型配置",
|
||||
"botBindings": "能力绑定",
|
||||
"systemPrompt": "系统提示词",
|
||||
"department": "所属部门",
|
||||
"category": "所属分类",
|
||||
"modelName": "模型名称",
|
||||
"vectorStoreType": "向量数据库类型",
|
||||
"vectorEmbedModel": "向量模型",
|
||||
"rerankModel": "重排模型",
|
||||
"maxMessageCount": "最大上下文消息数",
|
||||
"canUpdateEmbeddingModel": "允许更新向量模型",
|
||||
"anonymousEnabled": "允许匿名访问",
|
||||
"anonymousDisabled": "仅登录访问",
|
||||
"knowledgeBindings": "知识库",
|
||||
"workflowBindings": "工作流",
|
||||
"pluginBindings": "插件",
|
||||
"mcpBindings": "MCP",
|
||||
"expandPrompt": "展开全文",
|
||||
"collapsePrompt": "收起全文",
|
||||
"enabled": "已开启",
|
||||
"disabled": "已关闭",
|
||||
"notConfigured": "未配置",
|
||||
"noBindings": "未绑定任何能力",
|
||||
"untitledKnowledge": "未命名知识库",
|
||||
"untitledBot": "未命名聊天助手",
|
||||
"unnamedKnowledge": "未命名知识库",
|
||||
"unnamedWorkflow": "未命名工作流",
|
||||
"unnamedPlugin": "未命名插件",
|
||||
"unnamedMcp": "未命名MCP"
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,20 @@
|
||||
"deepThinking": "深度思考",
|
||||
"enableDeepThinking": "是否启用深度思考",
|
||||
"publish": "发布",
|
||||
"publishStatusLabel": "当前正式版本",
|
||||
"publishStatusDraft": "草稿",
|
||||
"publishStatusDraftDesc": "当前仅保存草稿,外链聊天和 Public API 仍不可用。",
|
||||
"publishStatusPublishPending": "发布审批中",
|
||||
"publishStatusPublishPendingDesc": "审批通过后,聊天助手会切换为新的正式版本。",
|
||||
"publishStatusPublished": "已发布",
|
||||
"publishStatusPublishedDesc": "当前正式版本已可对外使用,编辑中的草稿不会立即影响线上。",
|
||||
"publishStatusDeletePending": "删除审批中",
|
||||
"publishStatusDeletePendingDesc": "当前正式版本仍可访问,但不会继续作为新的绑定候选。",
|
||||
"submitPublishApprovalConfirm": "提交后会进入发布审批,审批通过后聊天助手才会正式对外可用。",
|
||||
"submitDeleteApprovalConfirm": "提交后会进入删除审批,审批通过后将执行真实删除。",
|
||||
"publishPendingHint": "当前聊天助手已有进行中的审批,请等待处理完成。",
|
||||
"deletePendingHint": "当前聊天助手已有进行中的审批,请等待处理完成。",
|
||||
"publishRequiredHint": "当前还没有正式发布版本,请先提交发布审批。",
|
||||
"postToWeChatOfficialAccount": "发布到微信公众号",
|
||||
"publishExternalLink": "发布外链聊天页",
|
||||
"configured": "已配置",
|
||||
|
||||
@@ -41,7 +41,10 @@
|
||||
"markAsResolved": "标记已处理",
|
||||
"optimizing": "正在优化中...",
|
||||
"regenerate": "重新生成",
|
||||
"republish": "重新发布",
|
||||
"hide": "隐藏",
|
||||
"more": "更多",
|
||||
"submitDeleteApproval": "提交删除审批",
|
||||
"submitPublishApproval": "提交发布审批",
|
||||
"viewSegmentation": "查看分段"
|
||||
}
|
||||
|
||||
@@ -35,6 +35,15 @@
|
||||
"documentType": "文件类型",
|
||||
"fileName": "文件名",
|
||||
"knowledgeCount": "知识条数",
|
||||
"publishStatusDraft": "草稿",
|
||||
"publishStatusPublishPending": "发布审批中",
|
||||
"publishStatusPublished": "已发布",
|
||||
"publishStatusDeletePending": "删除审批中",
|
||||
"publishStatusLabel": "发布状态",
|
||||
"submitPublishApprovalConfirm": "提交后会进入发布审批,审批通过后该知识库才可作为正式版本被聊天助手引用。",
|
||||
"submitDeleteApprovalConfirm": "提交后会进入删除审批,审批通过后将执行真实删除。",
|
||||
"publishPendingHint": "当前知识库已有进行中的审批,请等待处理完成。",
|
||||
"deletePendingHint": "当前知识库已有进行中的审批,请等待处理完成。",
|
||||
"createdModifyTime": "创建/更新时间",
|
||||
"documentList": "文档列表",
|
||||
"knowledgeRetrieval": "知识检索",
|
||||
|
||||
@@ -13,6 +13,11 @@
|
||||
"sysJob": "定时任务",
|
||||
"sysLog": "日志管理",
|
||||
"sysFeedback": "用户反馈",
|
||||
"approval": "审批管理",
|
||||
"approvalFlow": "流程配置",
|
||||
"approvalPending": "待审批",
|
||||
"approvalProcessed": "已审批",
|
||||
"approvalInitiated": "我发起",
|
||||
"sysAppearance": "外观设置",
|
||||
"oauth": "认证设置"
|
||||
},
|
||||
|
||||
20
easyflow-ui-admin/app/src/router/routes/modules/approval.ts
Normal file
20
easyflow-ui-admin/app/src/router/routes/modules/approval.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { RouteRecordRaw } from 'vue-router';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
name: 'ApprovalDetail',
|
||||
path: '/sys/approval/detail/:id',
|
||||
component: () => import('#/views/system/approval/ApprovalDetail.vue'),
|
||||
meta: {
|
||||
title: $t('menus.system.approval'),
|
||||
hideInMenu: true,
|
||||
hideInBreadcrumb: true,
|
||||
hideInTab: true,
|
||||
activePath: '/sys/approval',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
@@ -1,9 +1,14 @@
|
||||
declare module '@tinyflow-ai/vue' {
|
||||
import type { DefineComponent } from 'vue';
|
||||
import type { TinyflowData } from '@tinyflow-ai/ui';
|
||||
|
||||
export const Tinyflow: DefineComponent<
|
||||
Record<string, any>,
|
||||
Record<string, any>,
|
||||
any
|
||||
>;
|
||||
|
||||
export function sanitizeTinyflowDataForReadonlyPreview(
|
||||
input: TinyflowData | string,
|
||||
): TinyflowData | null | undefined;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ import { useRouter } from 'vue-router';
|
||||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||
import { $t } from '@easyflow/locales';
|
||||
|
||||
import { Delete, Edit, Plus, Setting } from '@element-plus/icons-vue';
|
||||
import { Delete, Edit, Plus, Promotion, Setting } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
@@ -22,16 +22,22 @@ import {
|
||||
ElInputNumber,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElTag,
|
||||
} from 'element-plus';
|
||||
import { tryit } from 'radash';
|
||||
|
||||
import { removeBotFromId } from '#/api';
|
||||
import { submitBotDeleteApproval, submitBotPublishApproval } from '#/api';
|
||||
import { api } from '#/api/request';
|
||||
import defaultAvatar from '#/assets/ai/bot/defaultBotAvatar.png';
|
||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||
import CardList from '#/components/page/CardList.vue';
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
import PageSide from '#/components/page/PageSide.vue';
|
||||
import {
|
||||
isAiResourceApprovalPending,
|
||||
isAiResourcePublished,
|
||||
normalizeAiPublishStatus,
|
||||
} from '#/views/ai/shared/publish-status';
|
||||
import { useDictStore } from '#/store';
|
||||
|
||||
import Modal from './modal.vue';
|
||||
@@ -97,37 +103,101 @@ const actions: ActionButton[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: Delete,
|
||||
text: $t('button.delete'),
|
||||
tone: 'danger',
|
||||
permission: '/api/v1/bot/remove',
|
||||
icon: Promotion,
|
||||
text: (row: BotInfo) =>
|
||||
isAiResourcePublished(row.publishStatus)
|
||||
? $t('button.republish')
|
||||
: $t('button.submitPublishApproval'),
|
||||
permission: '/api/v1/bot/save',
|
||||
placement: 'inline',
|
||||
onClick(row: BotInfo) {
|
||||
removeBot(row);
|
||||
handleSubmitPublishApproval(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: Delete,
|
||||
text: $t('button.submitDeleteApproval'),
|
||||
tone: 'danger',
|
||||
permission: '/api/v1/bot/remove',
|
||||
placement: 'menu',
|
||||
onClick(row: BotInfo) {
|
||||
handleSubmitDeleteApproval(row);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const removeBot = async (bot: BotInfo) => {
|
||||
const [action] = await tryit(ElMessageBox.confirm)(
|
||||
$t('message.deleteAlert'),
|
||||
$t('message.noticeTitle'),
|
||||
{
|
||||
confirmButtonText: $t('message.ok'),
|
||||
cancelButtonText: $t('message.cancel'),
|
||||
type: 'warning',
|
||||
},
|
||||
);
|
||||
|
||||
if (!action) {
|
||||
const [err, res] = await tryit(removeBotFromId)(bot.id);
|
||||
|
||||
if (!err && res.errorCode === 0) {
|
||||
ElMessage.success($t('message.deleteOkMessage'));
|
||||
pageDataRef.value.setQuery({});
|
||||
}
|
||||
const handleSubmitPublishApproval = async (bot: BotInfo) => {
|
||||
if (isAiResourceApprovalPending(bot.publishStatus)) {
|
||||
ElMessage.warning($t('bot.publishPendingHint'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
$t('bot.submitPublishApprovalConfirm'),
|
||||
$t('message.noticeTitle'),
|
||||
{
|
||||
confirmButtonText: $t('button.confirm'),
|
||||
cancelButtonText: $t('button.cancel'),
|
||||
type: 'info',
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const res = await submitBotPublishApproval(String(bot.id));
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message || $t('message.saveOkMessage'));
|
||||
pageDataRef.value?.reload?.();
|
||||
}
|
||||
};
|
||||
const handleSubmitDeleteApproval = async (bot: BotInfo) => {
|
||||
if (isAiResourceApprovalPending(bot.publishStatus)) {
|
||||
ElMessage.warning($t('bot.deletePendingHint'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
$t('bot.submitDeleteApprovalConfirm'),
|
||||
$t('message.noticeTitle'),
|
||||
{
|
||||
confirmButtonText: $t('button.confirm'),
|
||||
cancelButtonText: $t('button.cancel'),
|
||||
type: 'warning',
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const res = await submitBotDeleteApproval(String(bot.id));
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message || $t('message.saveOkMessage'));
|
||||
pageDataRef.value?.reload?.();
|
||||
}
|
||||
};
|
||||
function resolvePublishStatusMeta(status?: string) {
|
||||
switch (normalizeAiPublishStatus(status)) {
|
||||
case 'PUBLISHED':
|
||||
return {
|
||||
label: $t('bot.publishStatusPublished'),
|
||||
type: 'success' as const,
|
||||
};
|
||||
case 'PUBLISH_PENDING':
|
||||
return {
|
||||
label: $t('bot.publishStatusPublishPending'),
|
||||
type: 'warning' as const,
|
||||
};
|
||||
case 'DELETE_PENDING':
|
||||
return {
|
||||
label: $t('bot.publishStatusDeletePending'),
|
||||
type: 'danger' as const,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
label: $t('bot.publishStatusDraft'),
|
||||
type: 'info' as const,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const handleSearch = (params: string) => {
|
||||
pageDataRef.value.setQuery({ title: params, isQueryOr: true });
|
||||
@@ -302,7 +372,18 @@ const getSideList = async () => {
|
||||
:data="pageList"
|
||||
:primary-action="primaryAction"
|
||||
:actions="actions"
|
||||
/>
|
||||
>
|
||||
<template #corner="{ item }">
|
||||
<ElTag
|
||||
size="small"
|
||||
effect="plain"
|
||||
round
|
||||
:type="resolvePublishStatusMeta(item.publishStatus).type"
|
||||
>
|
||||
{{ resolvePublishStatusMeta(item.publishStatus).label }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</CardList>
|
||||
</template>
|
||||
</PageData>
|
||||
</div>
|
||||
|
||||
@@ -34,12 +34,15 @@ import {
|
||||
ElSkeleton,
|
||||
ElSlider,
|
||||
ElSwitch,
|
||||
ElTag,
|
||||
ElTooltip,
|
||||
} from 'element-plus';
|
||||
import { tryit } from 'radash';
|
||||
|
||||
import {
|
||||
getPerQuestions,
|
||||
submitBotDeleteApproval,
|
||||
submitBotPublishApproval,
|
||||
updateBotApi,
|
||||
updateBotOptions,
|
||||
updateLlmId,
|
||||
@@ -52,6 +55,12 @@ import CollapseViewItem from '#/components/collapseViewItem/CollapseViewItem.vue
|
||||
import CommonSelectDataModal from '#/components/commonSelectModal/CommonSelectDataModal.vue';
|
||||
import DictSelect from '#/components/dict/DictSelect.vue';
|
||||
import UploadAvatar from '#/components/upload/UploadAvatar.vue';
|
||||
import {
|
||||
isAiResourceApprovalPending,
|
||||
isAiResourceExternallyVisible,
|
||||
isAiResourcePublished,
|
||||
normalizeAiPublishStatus,
|
||||
} from '#/views/ai/shared/publish-status';
|
||||
|
||||
interface SelectedMcpTool {
|
||||
name: string;
|
||||
@@ -154,6 +163,46 @@ const publicChatUrl = computed(() => {
|
||||
const publicChatEmbedUrl = computed(() => {
|
||||
return buildPublicChatUrl(true);
|
||||
});
|
||||
const publishStatusMeta = computed<{
|
||||
description: string;
|
||||
label: string;
|
||||
type: 'danger' | 'info' | 'success' | 'warning';
|
||||
}>(() => {
|
||||
switch (normalizeAiPublishStatus(botInfo.value?.publishStatus)) {
|
||||
case 'PUBLISHED':
|
||||
return {
|
||||
label: $t('bot.publishStatusPublished'),
|
||||
type: 'success',
|
||||
description: $t('bot.publishStatusPublishedDesc'),
|
||||
};
|
||||
case 'PUBLISH_PENDING':
|
||||
return {
|
||||
label: $t('bot.publishStatusPublishPending'),
|
||||
type: 'warning',
|
||||
description: $t('bot.publishStatusPublishPendingDesc'),
|
||||
};
|
||||
case 'DELETE_PENDING':
|
||||
return {
|
||||
label: $t('bot.publishStatusDeletePending'),
|
||||
type: 'danger',
|
||||
description: $t('bot.publishStatusDeletePendingDesc'),
|
||||
};
|
||||
default:
|
||||
return {
|
||||
label: $t('bot.publishStatusDraft'),
|
||||
type: 'info',
|
||||
description: $t('bot.publishStatusDraftDesc'),
|
||||
};
|
||||
}
|
||||
});
|
||||
const canUsePublicAccess = computed(() =>
|
||||
isAiResourceExternallyVisible(botInfo.value?.publishStatus),
|
||||
);
|
||||
const publishPrimaryActionLabel = computed(() =>
|
||||
isAiResourcePublished(botInfo.value?.publishStatus)
|
||||
? $t('button.republish')
|
||||
: $t('button.submitPublishApproval'),
|
||||
);
|
||||
const iframeCode = computed(() => {
|
||||
if (!publicChatEmbedUrl.value) {
|
||||
return '';
|
||||
@@ -494,6 +543,10 @@ const handleCopyValue = async (value: string, successMessage?: string) => {
|
||||
};
|
||||
|
||||
const openPublicPage = () => {
|
||||
if (!canUsePublicAccess.value) {
|
||||
ElMessage.warning($t('bot.publishRequiredHint'));
|
||||
return;
|
||||
}
|
||||
if (!publicChatUrl.value) {
|
||||
ElMessage.warning($t('bot.chatPublishBaseUrlMissing'));
|
||||
return;
|
||||
@@ -767,6 +820,64 @@ const handleDeletePresetQuestion = (item: any) => {
|
||||
const handlePublishWx = () => {
|
||||
publishWxRef.value.openDialog(botId.value, botInfo.value?.options || {});
|
||||
};
|
||||
const handleSubmitPublishApproval = async () => {
|
||||
if (!botInfo.value) {
|
||||
return;
|
||||
}
|
||||
if (isAiResourceApprovalPending(botInfo.value.publishStatus)) {
|
||||
ElMessage.warning($t('bot.publishPendingHint'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
$t('bot.submitPublishApprovalConfirm'),
|
||||
$t('message.noticeTitle'),
|
||||
{
|
||||
confirmButtonText: $t('button.confirm'),
|
||||
cancelButtonText: $t('button.cancel'),
|
||||
type: 'info',
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const res = await submitBotPublishApproval(String(botInfo.value.id));
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message || $t('message.saveOkMessage'));
|
||||
getBotDetail();
|
||||
} else {
|
||||
ElMessage.error(res.message || $t('message.saveFailMessage'));
|
||||
}
|
||||
};
|
||||
const handleSubmitDeleteApproval = async () => {
|
||||
if (!botInfo.value) {
|
||||
return;
|
||||
}
|
||||
if (isAiResourceApprovalPending(botInfo.value.publishStatus)) {
|
||||
ElMessage.warning($t('bot.deletePendingHint'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
$t('bot.submitDeleteApprovalConfirm'),
|
||||
$t('message.noticeTitle'),
|
||||
{
|
||||
confirmButtonText: $t('button.confirm'),
|
||||
cancelButtonText: $t('button.cancel'),
|
||||
type: 'warning',
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const res = await submitBotDeleteApproval(String(botInfo.value.id));
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message || $t('message.saveOkMessage'));
|
||||
getBotDetail();
|
||||
} else {
|
||||
ElMessage.error(res.message || $t('message.saveFailMessage'));
|
||||
}
|
||||
};
|
||||
const handleUpdatePublishWx = () => {
|
||||
api
|
||||
.post('/api/v1/bot/updateOptions', {
|
||||
@@ -1352,6 +1463,44 @@ const handleBasicInfoChange = async (
|
||||
<h1 class="text-base font-medium">
|
||||
{{ $t('bot.publish') }}
|
||||
</h1>
|
||||
<div class="publish-summary-card">
|
||||
<div class="publish-summary-main">
|
||||
<div class="publish-summary-label">
|
||||
{{ $t('bot.publishStatusLabel') }}
|
||||
</div>
|
||||
<div class="publish-summary-row">
|
||||
<ElTag :type="publishStatusMeta.type" effect="plain" round>
|
||||
{{ publishStatusMeta.label }}
|
||||
</ElTag>
|
||||
<span
|
||||
v-if="botInfo?.currentApprovalInstanceId"
|
||||
class="publish-summary-instance"
|
||||
>
|
||||
#{{ botInfo.currentApprovalInstanceId }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="publish-summary-desc">
|
||||
{{ publishStatusMeta.description }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="publish-summary-actions">
|
||||
<ElButton
|
||||
type="primary"
|
||||
:disabled="!hasSavePermission"
|
||||
@click="handleSubmitPublishApproval"
|
||||
>
|
||||
{{ publishPrimaryActionLabel }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
plain
|
||||
type="danger"
|
||||
:disabled="!hasSavePermission"
|
||||
@click="handleSubmitDeleteApproval"
|
||||
>
|
||||
{{ $t('button.submitDeleteApproval') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex w-full flex-col justify-between rounded-lg">
|
||||
<ElCollapse expand-icon-position="left">
|
||||
<ElCollapseItem :title="$t('bot.postToWeChatOfficialAccount')">
|
||||
@@ -1428,12 +1577,23 @@ const handleBasicInfoChange = async (
|
||||
<label class="publish-external-label">
|
||||
{{ $t('bot.chatExternalLink') }}
|
||||
</label>
|
||||
<div
|
||||
v-if="!canUsePublicAccess"
|
||||
class="publish-external-alert"
|
||||
>
|
||||
<ElAlert
|
||||
:title="$t('bot.publishRequiredHint')"
|
||||
type="info"
|
||||
:closable="false"
|
||||
/>
|
||||
</div>
|
||||
<ElInput :model-value="publicChatUrl" readonly />
|
||||
<div class="publish-external-actions">
|
||||
<ElButton
|
||||
size="small"
|
||||
type="primary"
|
||||
plain
|
||||
:disabled="!canUsePublicAccess"
|
||||
@click="handleCopyValue(publicChatUrl)"
|
||||
>
|
||||
<ElIcon class="mr-1">
|
||||
@@ -1441,7 +1601,12 @@ const handleBasicInfoChange = async (
|
||||
</ElIcon>
|
||||
{{ $t('bot.copyLink') }}
|
||||
</ElButton>
|
||||
<ElButton size="small" type="primary" @click="openPublicPage">
|
||||
<ElButton
|
||||
size="small"
|
||||
type="primary"
|
||||
:disabled="!canUsePublicAccess"
|
||||
@click="openPublicPage"
|
||||
>
|
||||
<ElIcon class="mr-1">
|
||||
<Link />
|
||||
</ElIcon>
|
||||
@@ -1477,6 +1642,7 @@ const handleBasicInfoChange = async (
|
||||
<ElButton
|
||||
size="small"
|
||||
plain
|
||||
:disabled="!canUsePublicAccess"
|
||||
@click="handleCopyValue(iframeCode)"
|
||||
>
|
||||
<ElIcon class="mr-1">
|
||||
@@ -1511,6 +1677,7 @@ const handleBasicInfoChange = async (
|
||||
width="730"
|
||||
ref="knowledgeDataRef"
|
||||
page-url="/api/v1/documentCollection/page"
|
||||
:extra-query-params="{ publishedOnly: true }"
|
||||
@get-data="confirmUpdateAiBotKnowledge"
|
||||
/>
|
||||
|
||||
@@ -1520,6 +1687,7 @@ const handleBasicInfoChange = async (
|
||||
width="730"
|
||||
ref="workflowDataRef"
|
||||
page-url="/api/v1/workflow/page"
|
||||
:extra-query-params="{ publishedOnly: true }"
|
||||
@get-data="confirmUpdateAiBotWorkflow"
|
||||
/>
|
||||
|
||||
@@ -1643,6 +1811,58 @@ const handleBasicInfoChange = async (
|
||||
background-color: var(--bot-collapse-itme-back);
|
||||
}
|
||||
|
||||
.publish-summary-card {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 16px;
|
||||
background: hsl(var(--surface-subtle) / 78%);
|
||||
border: 1px solid hsl(var(--line-subtle));
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.publish-summary-main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
min-width: 220px;
|
||||
}
|
||||
|
||||
.publish-summary-label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.publish-summary-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.publish-summary-instance {
|
||||
font-size: 12px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.publish-summary-desc {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: hsl(var(--text-secondary));
|
||||
}
|
||||
|
||||
.publish-summary-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.publish-wx {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -44,6 +44,12 @@ import CardPage from '#/components/page/CardList.vue';
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
import PageSide from '#/components/page/PageSide.vue';
|
||||
import DocumentCollectionModal from '#/views/ai/documentCollection/DocumentCollectionModal.vue';
|
||||
import AiResourceCornerMeta from '#/views/ai/shared/AiResourceCornerMeta.vue';
|
||||
import {
|
||||
isAiResourceApprovalPending,
|
||||
isAiResourcePublished,
|
||||
normalizeAiPublishStatus,
|
||||
} from '#/views/ai/shared/publish-status';
|
||||
|
||||
const router = useRouter();
|
||||
const userStore = useUserStore();
|
||||
@@ -178,16 +184,31 @@ const actions: ActionButton[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
text: $t('button.delete'),
|
||||
icon: Delete,
|
||||
tone: 'danger',
|
||||
permission: '/api/v1/documentCollection/remove',
|
||||
icon: Promotion,
|
||||
text: (row) =>
|
||||
isAiResourcePublished(row.publishStatus)
|
||||
? $t('button.republish')
|
||||
: $t('button.submitPublishApproval'),
|
||||
permission: '/api/v1/documentCollection/save',
|
||||
placement: 'inline',
|
||||
onClick(row) {
|
||||
if (!ensureManageKnowledgeItem(row)) {
|
||||
return;
|
||||
}
|
||||
handleDelete(row);
|
||||
submitPublishApproval(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
text: $t('button.submitDeleteApproval'),
|
||||
icon: Delete,
|
||||
tone: 'danger',
|
||||
permission: '/api/v1/documentCollection/remove',
|
||||
placement: 'menu',
|
||||
onClick(row) {
|
||||
if (!ensureManageKnowledgeItem(row)) {
|
||||
return;
|
||||
}
|
||||
submitDeleteApproval(row);
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -195,24 +216,92 @@ const actions: ActionButton[] = [
|
||||
onMounted(() => {
|
||||
getCategoryList();
|
||||
});
|
||||
const handleDelete = (item: any) => {
|
||||
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
||||
confirmButtonText: $t('message.ok'),
|
||||
cancelButtonText: $t('message.cancel'),
|
||||
type: 'warning',
|
||||
})
|
||||
.then(() => {
|
||||
api
|
||||
.post('/api/v1/documentCollection/remove', { id: item.id })
|
||||
.then((res) => {
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success($t('message.deleteOkMessage'));
|
||||
pageDataRef.value.setQuery({});
|
||||
}
|
||||
});
|
||||
})
|
||||
.catch(() => {});
|
||||
const submitPublishApproval = async (item: any) => {
|
||||
if (isAiResourceApprovalPending(item.publishStatus)) {
|
||||
ElMessage.warning($t('documentCollection.publishPendingHint'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
$t('documentCollection.submitPublishApprovalConfirm'),
|
||||
$t('message.noticeTitle'),
|
||||
{
|
||||
confirmButtonText: $t('button.confirm'),
|
||||
cancelButtonText: $t('button.cancel'),
|
||||
type: 'info',
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const res = await api.post(
|
||||
'/api/v1/documentCollection/submitPublishApproval',
|
||||
{
|
||||
id: item.id,
|
||||
},
|
||||
);
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message || $t('message.saveOkMessage'));
|
||||
reloadKnowledgeList();
|
||||
}
|
||||
};
|
||||
const submitDeleteApproval = async (item: any) => {
|
||||
if (isAiResourceApprovalPending(item.publishStatus)) {
|
||||
ElMessage.warning($t('documentCollection.deletePendingHint'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
$t('documentCollection.submitDeleteApprovalConfirm'),
|
||||
$t('message.noticeTitle'),
|
||||
{
|
||||
confirmButtonText: $t('button.confirm'),
|
||||
cancelButtonText: $t('button.cancel'),
|
||||
type: 'warning',
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const res = await api.post(
|
||||
'/api/v1/documentCollection/submitDeleteApproval',
|
||||
{
|
||||
id: item.id,
|
||||
},
|
||||
);
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message || $t('message.saveOkMessage'));
|
||||
reloadKnowledgeList();
|
||||
}
|
||||
};
|
||||
function resolvePublishStatusMeta(status?: string) {
|
||||
switch (normalizeAiPublishStatus(status)) {
|
||||
case 'DELETE_PENDING': {
|
||||
return {
|
||||
label: $t('documentCollection.publishStatusDeletePending'),
|
||||
tone: 'danger',
|
||||
};
|
||||
}
|
||||
case 'PUBLISH_PENDING': {
|
||||
return {
|
||||
label: $t('documentCollection.publishStatusPublishPending'),
|
||||
tone: 'pending',
|
||||
};
|
||||
}
|
||||
case 'PUBLISHED': {
|
||||
return {
|
||||
label: $t('documentCollection.publishStatusPublished'),
|
||||
tone: 'published',
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return {
|
||||
label: $t('documentCollection.publishStatusDraft'),
|
||||
tone: 'draft',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const pageDataRef = ref();
|
||||
const aiKnowledgeModalRef = ref();
|
||||
@@ -460,21 +549,97 @@ function changeCategory(category: any) {
|
||||
:tag-map="collectionTypeLabelMap"
|
||||
>
|
||||
<template #corner="{ item }">
|
||||
<ElPopover
|
||||
v-if="canManageKnowledgeItem(item)"
|
||||
:ref="(el) => setVisibilityScopePopoverRef(item.id, el)"
|
||||
trigger="click"
|
||||
placement="bottom-end"
|
||||
:width="208"
|
||||
popper-class="knowledge-visibility-popover"
|
||||
>
|
||||
<template #reference>
|
||||
<button
|
||||
type="button"
|
||||
class="knowledge-scope-chip"
|
||||
<AiResourceCornerMeta>
|
||||
<template #publish>
|
||||
<div
|
||||
class="knowledge-publish-chip"
|
||||
:class="`knowledge-publish-chip--${resolvePublishStatusMeta(item.publishStatus).tone}`"
|
||||
>
|
||||
<span class="knowledge-publish-chip__dot"></span>
|
||||
<span>{{
|
||||
resolvePublishStatusMeta(item.publishStatus).label
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #scope>
|
||||
<ElPopover
|
||||
v-if="canManageKnowledgeItem(item)"
|
||||
:ref="(el) => setVisibilityScopePopoverRef(item.id, el)"
|
||||
trigger="click"
|
||||
placement="bottom-end"
|
||||
:width="208"
|
||||
popper-class="knowledge-visibility-popover"
|
||||
>
|
||||
<template #reference>
|
||||
<button
|
||||
type="button"
|
||||
class="knowledge-scope-chip"
|
||||
:class="`knowledge-scope-chip--${resolveVisibilityScopeMeta(item.visibilityScope).tone}`"
|
||||
:disabled="updatingScopeId === item.id"
|
||||
@click.stop
|
||||
>
|
||||
<ElIcon class="knowledge-scope-chip__icon">
|
||||
<component
|
||||
:is="
|
||||
resolveVisibilityScopeMeta(item.visibilityScope)
|
||||
.icon
|
||||
"
|
||||
/>
|
||||
</ElIcon>
|
||||
<span class="knowledge-scope-chip__label">
|
||||
{{
|
||||
resolveVisibilityScopeMeta(item.visibilityScope)
|
||||
.label
|
||||
}}
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
<div class="knowledge-scope-panel" @click.stop>
|
||||
<button
|
||||
v-for="option in visibilityScopeOptions"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
class="knowledge-scope-option"
|
||||
:class="[
|
||||
`knowledge-scope-option--${option.tone}`,
|
||||
{
|
||||
'knowledge-scope-option--active':
|
||||
item.visibilityScope === option.value,
|
||||
},
|
||||
]"
|
||||
:disabled="updatingScopeId === item.id"
|
||||
@click.stop="
|
||||
updateVisibilityScope(item, option.value)
|
||||
"
|
||||
>
|
||||
<span class="knowledge-scope-option__leading">
|
||||
<span class="knowledge-scope-option__icon-wrap">
|
||||
<ElIcon class="knowledge-scope-option__icon">
|
||||
<component :is="option.icon" />
|
||||
</ElIcon>
|
||||
</span>
|
||||
<span class="knowledge-scope-option__text">
|
||||
<span class="knowledge-scope-option__label">
|
||||
{{ option.label }}
|
||||
</span>
|
||||
<span class="knowledge-scope-option__desc">
|
||||
{{ option.description }}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<ElIcon
|
||||
v-if="item.visibilityScope === option.value"
|
||||
class="knowledge-scope-option__check"
|
||||
>
|
||||
<Check />
|
||||
</ElIcon>
|
||||
</button>
|
||||
</div>
|
||||
</ElPopover>
|
||||
<div
|
||||
v-else
|
||||
class="knowledge-scope-chip knowledge-scope-chip--readonly"
|
||||
:class="`knowledge-scope-chip--${resolveVisibilityScopeMeta(item.visibilityScope).tone}`"
|
||||
:disabled="updatingScopeId === item.id"
|
||||
@click.stop
|
||||
>
|
||||
<ElIcon class="knowledge-scope-chip__icon">
|
||||
<component
|
||||
@@ -489,64 +654,9 @@ function changeCategory(category: any) {
|
||||
resolveVisibilityScopeMeta(item.visibilityScope).label
|
||||
}}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="knowledge-scope-panel" @click.stop>
|
||||
<button
|
||||
v-for="option in visibilityScopeOptions"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
class="knowledge-scope-option"
|
||||
:class="[
|
||||
`knowledge-scope-option--${option.tone}`,
|
||||
{
|
||||
'knowledge-scope-option--active':
|
||||
item.visibilityScope === option.value,
|
||||
},
|
||||
]"
|
||||
:disabled="updatingScopeId === item.id"
|
||||
@click.stop="updateVisibilityScope(item, option.value)"
|
||||
>
|
||||
<span class="knowledge-scope-option__leading">
|
||||
<span class="knowledge-scope-option__icon-wrap">
|
||||
<ElIcon class="knowledge-scope-option__icon">
|
||||
<component :is="option.icon" />
|
||||
</ElIcon>
|
||||
</span>
|
||||
<span class="knowledge-scope-option__text">
|
||||
<span class="knowledge-scope-option__label">
|
||||
{{ option.label }}
|
||||
</span>
|
||||
<span class="knowledge-scope-option__desc">
|
||||
{{ option.description }}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<ElIcon
|
||||
v-if="item.visibilityScope === option.value"
|
||||
class="knowledge-scope-option__check"
|
||||
>
|
||||
<Check />
|
||||
</ElIcon>
|
||||
</button>
|
||||
</div>
|
||||
</ElPopover>
|
||||
<div
|
||||
v-else
|
||||
class="knowledge-scope-chip knowledge-scope-chip--readonly"
|
||||
:class="`knowledge-scope-chip--${resolveVisibilityScopeMeta(item.visibilityScope).tone}`"
|
||||
>
|
||||
<ElIcon class="knowledge-scope-chip__icon">
|
||||
<component
|
||||
:is="
|
||||
resolveVisibilityScopeMeta(item.visibilityScope).icon
|
||||
"
|
||||
/>
|
||||
</ElIcon>
|
||||
<span class="knowledge-scope-chip__label">
|
||||
{{ resolveVisibilityScopeMeta(item.visibilityScope).label }}
|
||||
</span>
|
||||
</div>
|
||||
</AiResourceCornerMeta>
|
||||
</template>
|
||||
</CardPage>
|
||||
</template>
|
||||
@@ -600,6 +710,54 @@ function changeCategory(category: any) {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.knowledge-publish-chip {
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 22px;
|
||||
min-width: 70px;
|
||||
padding: 0 9px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 999px;
|
||||
box-shadow: inset 0 1px 0 hsl(var(--card) / 46%);
|
||||
}
|
||||
|
||||
.knowledge-publish-chip__dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: currentColor;
|
||||
border-radius: 999px;
|
||||
opacity: 0.88;
|
||||
}
|
||||
|
||||
.knowledge-publish-chip--draft {
|
||||
color: hsl(var(--muted-foreground));
|
||||
background: hsl(var(--muted) / 42%);
|
||||
border-color: hsl(var(--line-subtle));
|
||||
}
|
||||
|
||||
.knowledge-publish-chip--pending {
|
||||
color: hsl(var(--warning));
|
||||
background: hsl(var(--warning) / 12%);
|
||||
border-color: hsl(var(--warning) / 14%);
|
||||
}
|
||||
|
||||
.knowledge-publish-chip--published {
|
||||
color: hsl(var(--success));
|
||||
background: hsl(var(--success) / 12%);
|
||||
border-color: hsl(var(--success) / 14%);
|
||||
}
|
||||
|
||||
.knowledge-publish-chip--danger {
|
||||
color: hsl(var(--destructive));
|
||||
background: hsl(var(--destructive) / 10%);
|
||||
border-color: hsl(var(--destructive) / 14%);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin-bottom: 30px;
|
||||
color: #303133;
|
||||
@@ -608,16 +766,18 @@ h1 {
|
||||
|
||||
.knowledge-scope-chip {
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
min-height: 30px;
|
||||
padding: 0 12px;
|
||||
font-size: 12px;
|
||||
justify-content: center;
|
||||
min-height: 22px;
|
||||
min-width: 70px;
|
||||
padding: 0 9px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
color: hsl(var(--text-strong));
|
||||
background: hsl(var(--surface-subtle) / 92%);
|
||||
border: 1px solid hsl(var(--line-subtle));
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 999px;
|
||||
transition:
|
||||
border-color 0.18s ease,
|
||||
@@ -632,8 +792,8 @@ button.knowledge-scope-chip {
|
||||
}
|
||||
|
||||
button.knowledge-scope-chip:hover {
|
||||
box-shadow: 0 10px 22px -18px hsl(var(--foreground) / 32%);
|
||||
transform: translateY(-1px);
|
||||
background: hsl(var(--card) / 76%);
|
||||
box-shadow: 0 8px 16px -16px hsl(var(--foreground) / 28%);
|
||||
}
|
||||
|
||||
button.knowledge-scope-chip:focus-visible {
|
||||
@@ -646,7 +806,6 @@ button.knowledge-scope-chip:focus-visible {
|
||||
button.knowledge-scope-chip:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.72;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.knowledge-scope-chip--readonly {
|
||||
@@ -654,7 +813,7 @@ button.knowledge-scope-chip:disabled {
|
||||
}
|
||||
|
||||
.knowledge-scope-chip__icon {
|
||||
font-size: 14px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.knowledge-scope-chip__label {
|
||||
@@ -663,20 +822,20 @@ button.knowledge-scope-chip:disabled {
|
||||
|
||||
.knowledge-scope-chip--private {
|
||||
color: hsl(var(--primary));
|
||||
background: hsl(var(--primary) / 9%);
|
||||
border-color: hsl(var(--primary) / 20%);
|
||||
background: hsl(var(--primary) / 10%);
|
||||
border-color: hsl(var(--primary) / 14%);
|
||||
}
|
||||
|
||||
.knowledge-scope-chip--dept {
|
||||
color: hsl(var(--warning));
|
||||
background: hsl(var(--warning) / 12%);
|
||||
border-color: hsl(var(--warning) / 20%);
|
||||
border-color: hsl(var(--warning) / 14%);
|
||||
}
|
||||
|
||||
.knowledge-scope-chip--public {
|
||||
color: hsl(var(--success));
|
||||
background: hsl(var(--success) / 12%);
|
||||
border-color: hsl(var(--success) / 20%);
|
||||
border-color: hsl(var(--success) / 14%);
|
||||
}
|
||||
|
||||
.knowledge-scope-panel {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div class="ai-resource-corner-meta">
|
||||
<div class="ai-resource-corner-meta__item">
|
||||
<slot name="publish"></slot>
|
||||
</div>
|
||||
<div class="ai-resource-corner-meta__item">
|
||||
<slot name="scope"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.ai-resource-corner-meta {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.ai-resource-corner-meta__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
min-width: 0;
|
||||
}
|
||||
</style>
|
||||
51
easyflow-ui-admin/app/src/views/ai/shared/publish-status.ts
Normal file
51
easyflow-ui-admin/app/src/views/ai/shared/publish-status.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
export type AiPublishStatus =
|
||||
| 'DELETE_PENDING'
|
||||
| 'DRAFT'
|
||||
| 'PUBLISHED'
|
||||
| 'PUBLISH_PENDING';
|
||||
|
||||
/**
|
||||
* 规范化发布状态,避免页面散落默认值判断。
|
||||
*/
|
||||
export function normalizeAiPublishStatus(
|
||||
value?: null | string,
|
||||
): AiPublishStatus {
|
||||
switch (value) {
|
||||
case 'PUBLISHED':
|
||||
case 'PUBLISH_PENDING':
|
||||
case 'DELETE_PENDING':
|
||||
return value;
|
||||
default:
|
||||
return 'DRAFT';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前资源是否已有正式线上版本。
|
||||
*/
|
||||
export function isAiResourcePublished(value?: null | string) {
|
||||
return normalizeAiPublishStatus(value) === 'PUBLISHED';
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前资源是否允许对外可见。
|
||||
*/
|
||||
export function isAiResourceExternallyVisible(value?: null | string) {
|
||||
const normalized = normalizeAiPublishStatus(value);
|
||||
return normalized === 'PUBLISHED' || normalized === 'DELETE_PENDING';
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前资源是否允许作为新的 Bot 引用候选。
|
||||
*/
|
||||
export function isAiResourceSelectableForBot(value?: null | string) {
|
||||
return normalizeAiPublishStatus(value) === 'PUBLISHED';
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前资源是否处于审批处理中。
|
||||
*/
|
||||
export function isAiResourceApprovalPending(value?: null | string) {
|
||||
const normalized = normalizeAiPublishStatus(value);
|
||||
return normalized === 'PUBLISH_PENDING' || normalized === 'DELETE_PENDING';
|
||||
}
|
||||
@@ -5,15 +5,30 @@ import { useRoute } from 'vue-router';
|
||||
import { usePreferences } from '@easyflow/preferences';
|
||||
import { getOptions, sortNodes } from '@easyflow/utils';
|
||||
|
||||
import { ArrowLeft, CircleCheck, Close } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ArrowLeft,
|
||||
CircleCheck,
|
||||
Close,
|
||||
Promotion,
|
||||
} from '@element-plus/icons-vue';
|
||||
import { Tinyflow } from '@tinyflow-ai/vue';
|
||||
import { ElButton, ElDrawer, ElMessage, ElSkeleton } from 'element-plus';
|
||||
import {
|
||||
ElButton,
|
||||
ElDrawer,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElSkeleton,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import CommonSelectDataModal from '#/components/commonSelectModal/CommonSelectDataModal.vue';
|
||||
import { $t } from '#/locales';
|
||||
import { router } from '#/router';
|
||||
import { getIconByValue } from '#/views/ai/model/modelUtils/defaultIcon';
|
||||
import {
|
||||
isAiResourceApprovalPending,
|
||||
normalizeAiPublishStatus,
|
||||
} from '#/views/ai/shared/publish-status';
|
||||
import ExecResult from '#/views/ai/workflow/components/ExecResult.vue';
|
||||
import SingleRun from '#/views/ai/workflow/components/SingleRun.vue';
|
||||
import WorkflowForm from '#/views/ai/workflow/components/WorkflowForm.vue';
|
||||
@@ -135,6 +150,7 @@ const customNode = ref();
|
||||
const showTinyFlow = ref(false);
|
||||
const saveLoading = ref(false);
|
||||
const checkLoading = ref(false);
|
||||
const publishLoading = ref(false);
|
||||
const checkIssuesVisible = ref(false);
|
||||
const checkResult = ref<any>(null);
|
||||
const checkContentSnapshot = ref<any>(null);
|
||||
@@ -234,6 +250,30 @@ const pluginSelectRef = ref();
|
||||
const updatePluginNode = ref<any>(null);
|
||||
const pageLoading = ref(false);
|
||||
const chainInfo = ref<any>(null);
|
||||
const publishActionText = computed(() => {
|
||||
switch (normalizeAiPublishStatus(workflowInfo.value?.publishStatus)) {
|
||||
case 'DELETE_PENDING': {
|
||||
return $t('aiWorkflow.publishStatusDeletePending');
|
||||
}
|
||||
case 'PUBLISH_PENDING': {
|
||||
return $t('aiWorkflow.publishStatusPublishPending');
|
||||
}
|
||||
case 'PUBLISHED': {
|
||||
return `${$t('aiWorkflow.publishStatusPublished')} · ${$t('button.republish')}`;
|
||||
}
|
||||
default: {
|
||||
return `${$t('aiWorkflow.publishStatusDraft')} · ${$t('button.submitPublishApproval')}`;
|
||||
}
|
||||
}
|
||||
});
|
||||
const publishActionDisabled = computed(
|
||||
() =>
|
||||
!workflowId.value ||
|
||||
saveLoading.value ||
|
||||
checkLoading.value ||
|
||||
publishLoading.value ||
|
||||
isAiResourceApprovalPending(workflowInfo.value?.publishStatus),
|
||||
);
|
||||
|
||||
function syncNavTitle(title: string) {
|
||||
if (!title) {
|
||||
@@ -458,6 +498,44 @@ function closeCheckIssues() {
|
||||
async function handleCheck() {
|
||||
await runCheck('PRE_EXECUTE');
|
||||
}
|
||||
async function handlePublish() {
|
||||
if (publishLoading.value) {
|
||||
return;
|
||||
}
|
||||
if (isAiResourceApprovalPending(workflowInfo.value?.publishStatus)) {
|
||||
ElMessage.warning($t('aiWorkflow.publishPendingHint'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
$t('aiWorkflow.submitPublishApprovalConfirm'),
|
||||
$t('message.noticeTitle'),
|
||||
{
|
||||
confirmButtonText: $t('button.confirm'),
|
||||
cancelButtonText: $t('button.cancel'),
|
||||
type: 'info',
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const saved = await handleSave();
|
||||
if (!saved) {
|
||||
return;
|
||||
}
|
||||
publishLoading.value = true;
|
||||
try {
|
||||
const res = await api.post('/api/v1/workflow/submitPublishApproval', {
|
||||
id: workflowId.value,
|
||||
});
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message || $t('message.saveOkMessage'));
|
||||
await getWorkflowInfo(workflowId.value);
|
||||
}
|
||||
} finally {
|
||||
publishLoading.value = false;
|
||||
}
|
||||
}
|
||||
function onSubmit() {
|
||||
initState.value = !initState.value;
|
||||
}
|
||||
@@ -584,7 +662,7 @@ function onAsyncExecute(info: any) {
|
||||
<div class="workflow-head-actions">
|
||||
<ElButton
|
||||
:loading="checkLoading"
|
||||
:disabled="saveLoading"
|
||||
:disabled="saveLoading || publishLoading"
|
||||
:icon="CircleCheck"
|
||||
@click="handleCheck"
|
||||
>
|
||||
@@ -592,11 +670,21 @@ function onAsyncExecute(info: any) {
|
||||
</ElButton>
|
||||
<ElButton
|
||||
type="primary"
|
||||
:disabled="saveLoading || checkLoading"
|
||||
:disabled="saveLoading || checkLoading || publishLoading"
|
||||
@click="handleSave(true)"
|
||||
>
|
||||
{{ $t('button.save') }}(ctrl+s)
|
||||
</ElButton>
|
||||
<ElButton
|
||||
:icon="Promotion"
|
||||
:loading="publishLoading"
|
||||
:disabled="publishActionDisabled"
|
||||
class="workflow-publish-button"
|
||||
:class="`workflow-publish-button--${normalizeAiPublishStatus(workflowInfo?.publishStatus)}`"
|
||||
@click="handlePublish"
|
||||
>
|
||||
{{ publishActionText }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
<Tinyflow
|
||||
@@ -695,8 +783,42 @@ function onAsyncExecute(info: any) {
|
||||
|
||||
.workflow-head-actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
:deep(.workflow-publish-button.el-button) {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
:deep(.workflow-publish-button.el-button:not(.is-disabled)) {
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
:deep(.workflow-publish-button--DRAFT.el-button) {
|
||||
color: hsl(var(--foreground) / 72%);
|
||||
background: hsl(var(--muted) / 68%);
|
||||
border-color: hsl(var(--foreground) / 14%);
|
||||
}
|
||||
|
||||
:deep(.workflow-publish-button--PUBLISH_PENDING.el-button) {
|
||||
color: hsl(var(--warning));
|
||||
background: hsl(var(--warning) / 18%);
|
||||
border-color: hsl(var(--warning) / 24%);
|
||||
}
|
||||
|
||||
:deep(.workflow-publish-button--PUBLISHED.el-button) {
|
||||
color: hsl(var(--success));
|
||||
background: hsl(var(--success) / 18%);
|
||||
border-color: hsl(var(--success) / 24%);
|
||||
}
|
||||
|
||||
:deep(.workflow-publish-button--DELETE_PENDING.el-button) {
|
||||
color: hsl(var(--destructive));
|
||||
background: hsl(var(--destructive) / 16%);
|
||||
border-color: hsl(var(--destructive) / 24%);
|
||||
}
|
||||
|
||||
.tiny-flow-container {
|
||||
|
||||
@@ -48,6 +48,12 @@ import PageSide from '#/components/page/PageSide.vue';
|
||||
import { $t } from '#/locales';
|
||||
import { router } from '#/router';
|
||||
import { useDictStore } from '#/store';
|
||||
import AiResourceCornerMeta from '#/views/ai/shared/AiResourceCornerMeta.vue';
|
||||
import {
|
||||
isAiResourceApprovalPending,
|
||||
isAiResourcePublished,
|
||||
normalizeAiPublishStatus,
|
||||
} from '#/views/ai/shared/publish-status';
|
||||
|
||||
import WorkflowModal from './WorkflowModal.vue';
|
||||
|
||||
@@ -167,12 +173,25 @@ const actions: ActionButton[] = [
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: Delete,
|
||||
text: $t('button.delete'),
|
||||
tone: 'danger',
|
||||
icon: Promotion,
|
||||
text: (row: any) =>
|
||||
isAiResourcePublished(row.publishStatus)
|
||||
? $t('button.republish')
|
||||
: $t('button.submitPublishApproval'),
|
||||
permission: '/api/v1/workflow/save',
|
||||
placement: 'inline',
|
||||
onClick: (row: any) => {
|
||||
remove(row);
|
||||
submitPublishApproval(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: Delete,
|
||||
text: $t('button.submitDeleteApproval'),
|
||||
tone: 'danger',
|
||||
permission: '/api/v1/workflow/remove',
|
||||
placement: 'menu',
|
||||
onClick: (row: any) => {
|
||||
submitDeleteApproval(row);
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -263,32 +282,85 @@ function showDialog(row: any, importMode = false) {
|
||||
function resolveNavTitle(row: any) {
|
||||
return row?.title || row?.name || '';
|
||||
}
|
||||
function remove(row: any) {
|
||||
ElMessageBox.confirm($t('message.deleteAlert'), $t('message.noticeTitle'), {
|
||||
confirmButtonText: $t('message.ok'),
|
||||
cancelButtonText: $t('message.cancel'),
|
||||
type: 'warning',
|
||||
beforeClose: (action, instance, done) => {
|
||||
if (action === 'confirm') {
|
||||
instance.confirmButtonLoading = true;
|
||||
api
|
||||
.post('/api/v1/workflow/remove', { id: row.id })
|
||||
.then((res) => {
|
||||
instance.confirmButtonLoading = false;
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message);
|
||||
reset();
|
||||
done();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
instance.confirmButtonLoading = false;
|
||||
});
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
},
|
||||
}).catch(() => {});
|
||||
async function submitPublishApproval(row: any) {
|
||||
if (isAiResourceApprovalPending(row.publishStatus)) {
|
||||
ElMessage.warning($t('aiWorkflow.publishPendingHint'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
$t('aiWorkflow.submitPublishApprovalConfirm'),
|
||||
$t('message.noticeTitle'),
|
||||
{
|
||||
confirmButtonText: $t('button.confirm'),
|
||||
cancelButtonText: $t('button.cancel'),
|
||||
type: 'info',
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const res = await api.post('/api/v1/workflow/submitPublishApproval', {
|
||||
id: row.id,
|
||||
});
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message || $t('message.saveOkMessage'));
|
||||
pageDataRef.value?.reload?.();
|
||||
}
|
||||
}
|
||||
async function submitDeleteApproval(row: any) {
|
||||
if (isAiResourceApprovalPending(row.publishStatus)) {
|
||||
ElMessage.warning($t('aiWorkflow.deletePendingHint'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
$t('aiWorkflow.submitDeleteApprovalConfirm'),
|
||||
$t('message.noticeTitle'),
|
||||
{
|
||||
confirmButtonText: $t('button.confirm'),
|
||||
cancelButtonText: $t('button.cancel'),
|
||||
type: 'warning',
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const res = await api.post('/api/v1/workflow/submitDeleteApproval', {
|
||||
id: row.id,
|
||||
});
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message || $t('message.saveOkMessage'));
|
||||
pageDataRef.value?.reload?.();
|
||||
}
|
||||
}
|
||||
function resolvePublishStatusMeta(status?: string) {
|
||||
switch (normalizeAiPublishStatus(status)) {
|
||||
case 'DELETE_PENDING': {
|
||||
return {
|
||||
label: $t('aiWorkflow.publishStatusDeletePending'),
|
||||
tone: 'danger',
|
||||
};
|
||||
}
|
||||
case 'PUBLISH_PENDING': {
|
||||
return {
|
||||
label: $t('aiWorkflow.publishStatusPublishPending'),
|
||||
tone: 'pending',
|
||||
};
|
||||
}
|
||||
case 'PUBLISHED': {
|
||||
return {
|
||||
label: $t('aiWorkflow.publishStatusPublished'),
|
||||
tone: 'published',
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return {
|
||||
label: $t('aiWorkflow.publishStatusDraft'),
|
||||
tone: 'draft',
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
function toDesignPage(row: any) {
|
||||
router.push({
|
||||
@@ -496,21 +568,97 @@ function handleHeaderButtonClick(data: any) {
|
||||
:actions="actions"
|
||||
>
|
||||
<template #corner="{ item }">
|
||||
<ElPopover
|
||||
v-if="canManageWorkflow"
|
||||
:ref="(el) => setVisibilityScopePopoverRef(item.id, el)"
|
||||
trigger="click"
|
||||
placement="bottom-end"
|
||||
:width="208"
|
||||
popper-class="workflow-visibility-popover"
|
||||
>
|
||||
<template #reference>
|
||||
<button
|
||||
type="button"
|
||||
class="workflow-scope-chip"
|
||||
<AiResourceCornerMeta>
|
||||
<template #publish>
|
||||
<div
|
||||
class="workflow-publish-chip"
|
||||
:class="`workflow-publish-chip--${resolvePublishStatusMeta(item.publishStatus).tone}`"
|
||||
>
|
||||
<span class="workflow-publish-chip__dot"></span>
|
||||
<span>{{
|
||||
resolvePublishStatusMeta(item.publishStatus).label
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #scope>
|
||||
<ElPopover
|
||||
v-if="canManageWorkflow"
|
||||
:ref="(el) => setVisibilityScopePopoverRef(item.id, el)"
|
||||
trigger="click"
|
||||
placement="bottom-end"
|
||||
:width="208"
|
||||
popper-class="workflow-visibility-popover"
|
||||
>
|
||||
<template #reference>
|
||||
<button
|
||||
type="button"
|
||||
class="workflow-scope-chip"
|
||||
:class="`workflow-scope-chip--${resolveVisibilityScopeMeta(item.visibilityScope).tone}`"
|
||||
:disabled="updatingScopeId === item.id"
|
||||
@click.stop
|
||||
>
|
||||
<ElIcon class="workflow-scope-chip__icon">
|
||||
<component
|
||||
:is="
|
||||
resolveVisibilityScopeMeta(item.visibilityScope)
|
||||
.icon
|
||||
"
|
||||
/>
|
||||
</ElIcon>
|
||||
<span class="workflow-scope-chip__label">
|
||||
{{
|
||||
resolveVisibilityScopeMeta(item.visibilityScope)
|
||||
.label
|
||||
}}
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
<div class="workflow-scope-panel" @click.stop>
|
||||
<button
|
||||
v-for="option in visibilityScopeOptions"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
class="workflow-scope-option"
|
||||
:class="[
|
||||
`workflow-scope-option--${option.tone}`,
|
||||
{
|
||||
'workflow-scope-option--active':
|
||||
item.visibilityScope === option.value,
|
||||
},
|
||||
]"
|
||||
:disabled="updatingScopeId === item.id"
|
||||
@click.stop="
|
||||
updateVisibilityScope(item, option.value)
|
||||
"
|
||||
>
|
||||
<span class="workflow-scope-option__leading">
|
||||
<span class="workflow-scope-option__icon-wrap">
|
||||
<ElIcon class="workflow-scope-option__icon">
|
||||
<component :is="option.icon" />
|
||||
</ElIcon>
|
||||
</span>
|
||||
<span class="workflow-scope-option__text">
|
||||
<span class="workflow-scope-option__label">
|
||||
{{ option.label }}
|
||||
</span>
|
||||
<span class="workflow-scope-option__desc">
|
||||
{{ option.description }}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<ElIcon
|
||||
v-if="item.visibilityScope === option.value"
|
||||
class="workflow-scope-option__check"
|
||||
>
|
||||
<Check />
|
||||
</ElIcon>
|
||||
</button>
|
||||
</div>
|
||||
</ElPopover>
|
||||
<div
|
||||
v-else
|
||||
class="workflow-scope-chip workflow-scope-chip--readonly"
|
||||
:class="`workflow-scope-chip--${resolveVisibilityScopeMeta(item.visibilityScope).tone}`"
|
||||
:disabled="updatingScopeId === item.id"
|
||||
@click.stop
|
||||
>
|
||||
<ElIcon class="workflow-scope-chip__icon">
|
||||
<component
|
||||
@@ -525,64 +673,9 @@ function handleHeaderButtonClick(data: any) {
|
||||
resolveVisibilityScopeMeta(item.visibilityScope).label
|
||||
}}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
<div class="workflow-scope-panel" @click.stop>
|
||||
<button
|
||||
v-for="option in visibilityScopeOptions"
|
||||
:key="option.value"
|
||||
type="button"
|
||||
class="workflow-scope-option"
|
||||
:class="[
|
||||
`workflow-scope-option--${option.tone}`,
|
||||
{
|
||||
'workflow-scope-option--active':
|
||||
item.visibilityScope === option.value,
|
||||
},
|
||||
]"
|
||||
:disabled="updatingScopeId === item.id"
|
||||
@click.stop="updateVisibilityScope(item, option.value)"
|
||||
>
|
||||
<span class="workflow-scope-option__leading">
|
||||
<span class="workflow-scope-option__icon-wrap">
|
||||
<ElIcon class="workflow-scope-option__icon">
|
||||
<component :is="option.icon" />
|
||||
</ElIcon>
|
||||
</span>
|
||||
<span class="workflow-scope-option__text">
|
||||
<span class="workflow-scope-option__label">
|
||||
{{ option.label }}
|
||||
</span>
|
||||
<span class="workflow-scope-option__desc">
|
||||
{{ option.description }}
|
||||
</span>
|
||||
</span>
|
||||
</span>
|
||||
<ElIcon
|
||||
v-if="item.visibilityScope === option.value"
|
||||
class="workflow-scope-option__check"
|
||||
>
|
||||
<Check />
|
||||
</ElIcon>
|
||||
</button>
|
||||
</div>
|
||||
</ElPopover>
|
||||
<div
|
||||
v-else
|
||||
class="workflow-scope-chip workflow-scope-chip--readonly"
|
||||
:class="`workflow-scope-chip--${resolveVisibilityScopeMeta(item.visibilityScope).tone}`"
|
||||
>
|
||||
<ElIcon class="workflow-scope-chip__icon">
|
||||
<component
|
||||
:is="
|
||||
resolveVisibilityScopeMeta(item.visibilityScope).icon
|
||||
"
|
||||
/>
|
||||
</ElIcon>
|
||||
<span class="workflow-scope-chip__label">
|
||||
{{ resolveVisibilityScopeMeta(item.visibilityScope).label }}
|
||||
</span>
|
||||
</div>
|
||||
</AiResourceCornerMeta>
|
||||
</template>
|
||||
</CardList>
|
||||
</template>
|
||||
@@ -631,18 +724,68 @@ function handleHeaderButtonClick(data: any) {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.workflow-publish-chip {
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 22px;
|
||||
min-width: 70px;
|
||||
padding: 0 9px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 999px;
|
||||
box-shadow: inset 0 1px 0 hsl(var(--card) / 46%);
|
||||
}
|
||||
|
||||
.workflow-publish-chip__dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: currentColor;
|
||||
border-radius: 999px;
|
||||
opacity: 0.88;
|
||||
}
|
||||
|
||||
.workflow-publish-chip--draft {
|
||||
color: hsl(var(--muted-foreground));
|
||||
background: hsl(var(--muted) / 42%);
|
||||
border-color: hsl(var(--line-subtle));
|
||||
}
|
||||
|
||||
.workflow-publish-chip--pending {
|
||||
color: hsl(var(--warning));
|
||||
background: hsl(var(--warning) / 12%);
|
||||
border-color: hsl(var(--warning) / 14%);
|
||||
}
|
||||
|
||||
.workflow-publish-chip--published {
|
||||
color: hsl(var(--success));
|
||||
background: hsl(var(--success) / 12%);
|
||||
border-color: hsl(var(--success) / 14%);
|
||||
}
|
||||
|
||||
.workflow-publish-chip--danger {
|
||||
color: hsl(var(--destructive));
|
||||
background: hsl(var(--destructive) / 10%);
|
||||
border-color: hsl(var(--destructive) / 14%);
|
||||
}
|
||||
|
||||
.workflow-scope-chip {
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
min-height: 30px;
|
||||
padding: 0 12px;
|
||||
font-size: 12px;
|
||||
justify-content: center;
|
||||
min-height: 22px;
|
||||
min-width: 70px;
|
||||
padding: 0 9px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
color: hsl(var(--text-strong));
|
||||
background: hsl(var(--surface-subtle) / 92%);
|
||||
border: 1px solid hsl(var(--line-subtle));
|
||||
background: transparent;
|
||||
border: 1px solid transparent;
|
||||
border-radius: 999px;
|
||||
transition:
|
||||
border-color 0.18s ease,
|
||||
@@ -657,8 +800,8 @@ button.workflow-scope-chip {
|
||||
}
|
||||
|
||||
button.workflow-scope-chip:hover {
|
||||
box-shadow: 0 10px 22px -18px hsl(var(--foreground) / 32%);
|
||||
transform: translateY(-1px);
|
||||
background: hsl(var(--card) / 76%);
|
||||
box-shadow: 0 8px 16px -16px hsl(var(--foreground) / 28%);
|
||||
}
|
||||
|
||||
button.workflow-scope-chip:focus-visible {
|
||||
@@ -671,7 +814,6 @@ button.workflow-scope-chip:focus-visible {
|
||||
button.workflow-scope-chip:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.72;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.workflow-scope-chip--readonly {
|
||||
@@ -679,7 +821,7 @@ button.workflow-scope-chip:disabled {
|
||||
}
|
||||
|
||||
.workflow-scope-chip__icon {
|
||||
font-size: 14px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.workflow-scope-chip__label {
|
||||
@@ -688,20 +830,20 @@ button.workflow-scope-chip:disabled {
|
||||
|
||||
.workflow-scope-chip--private {
|
||||
color: hsl(var(--primary));
|
||||
background: hsl(var(--primary) / 9%);
|
||||
border-color: hsl(var(--primary) / 20%);
|
||||
background: hsl(var(--primary) / 10%);
|
||||
border-color: hsl(var(--primary) / 14%);
|
||||
}
|
||||
|
||||
.workflow-scope-chip--dept {
|
||||
color: hsl(var(--warning));
|
||||
background: hsl(var(--warning) / 12%);
|
||||
border-color: hsl(var(--warning) / 20%);
|
||||
border-color: hsl(var(--warning) / 14%);
|
||||
}
|
||||
|
||||
.workflow-scope-chip--public {
|
||||
color: hsl(var(--success));
|
||||
background: hsl(var(--success) / 12%);
|
||||
border-color: hsl(var(--success) / 20%);
|
||||
border-color: hsl(var(--success) / 14%);
|
||||
}
|
||||
|
||||
.workflow-scope-panel {
|
||||
|
||||
@@ -0,0 +1,455 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
|
||||
import { IconifyIcon } from '@easyflow/icons';
|
||||
|
||||
import { ArrowLeft } from '@element-plus/icons-vue';
|
||||
import {
|
||||
ElButton,
|
||||
ElDescriptions,
|
||||
ElDescriptionsItem,
|
||||
ElEmpty,
|
||||
ElMessage,
|
||||
ElMessageBox,
|
||||
ElTable,
|
||||
ElTableColumn,
|
||||
ElTag,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import { $t } from '#/locales';
|
||||
import { router } from '#/router';
|
||||
import BotApprovalSnapshotPreview from '#/views/system/approval/components/BotApprovalSnapshotPreview.vue';
|
||||
import KnowledgeApprovalSnapshotPreview from '#/views/system/approval/components/KnowledgeApprovalSnapshotPreview.vue';
|
||||
import WorkflowApprovalSnapshotPreview from '#/views/system/approval/components/WorkflowApprovalSnapshotPreview.vue';
|
||||
|
||||
const route = useRoute();
|
||||
const loading = ref(false);
|
||||
const detail = ref<any>(null);
|
||||
|
||||
const resourceLabelMap: Record<string, string> = {
|
||||
BOT: $t('approval.resource.bot'),
|
||||
KNOWLEDGE: $t('approval.resource.knowledge'),
|
||||
WORKFLOW: $t('approval.resource.workflow'),
|
||||
};
|
||||
|
||||
const actionLabelMap: Record<string, string> = {
|
||||
DELETE: $t('approval.action.delete'),
|
||||
PUBLISH: $t('approval.action.publish'),
|
||||
};
|
||||
|
||||
const canOperate = computed(() => {
|
||||
return (
|
||||
!!detail.value?.canApprove ||
|
||||
!!detail.value?.canReject ||
|
||||
!!detail.value?.canRevoke
|
||||
);
|
||||
});
|
||||
|
||||
const workflowSnapshot = computed(() => {
|
||||
if (detail.value?.resourceType !== 'WORKFLOW') {
|
||||
return null;
|
||||
}
|
||||
const snapshot = detail.value?.snapshotJson?.resourceSnapshot;
|
||||
return {
|
||||
title: snapshot?.title || '',
|
||||
description: snapshot?.description || '',
|
||||
content: snapshot?.content || '',
|
||||
};
|
||||
});
|
||||
|
||||
const knowledgeSnapshot = computed(() => {
|
||||
if (detail.value?.resourceType !== 'KNOWLEDGE') {
|
||||
return null;
|
||||
}
|
||||
return detail.value?.snapshotJson?.resourceSnapshot || null;
|
||||
});
|
||||
|
||||
const botSnapshot = computed(() => {
|
||||
if (detail.value?.resourceType !== 'BOT') {
|
||||
return null;
|
||||
}
|
||||
return detail.value?.snapshotJson?.resourceSnapshot || null;
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
void loadDetail();
|
||||
});
|
||||
|
||||
async function loadDetail() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const res = await api.get('/api/v1/approvalInstance/detail', {
|
||||
params: {
|
||||
id: route.params.id,
|
||||
},
|
||||
});
|
||||
if (res.errorCode !== 0) {
|
||||
return;
|
||||
}
|
||||
detail.value = res.data;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitApprovalAction(action: 'approve' | 'reject' | 'revoke') {
|
||||
const titleMap = {
|
||||
approve: $t('approval.action.approve'),
|
||||
reject: $t('approval.action.reject'),
|
||||
revoke: $t('approval.action.revoke'),
|
||||
};
|
||||
const { value } = await ElMessageBox.prompt(
|
||||
$t('approval.placeholder.actionComment'),
|
||||
titleMap[action],
|
||||
{
|
||||
inputValue: '',
|
||||
inputType: 'textarea',
|
||||
},
|
||||
);
|
||||
const res = await api.post(`/api/v1/approvalInstance/${action}`, {
|
||||
comment: value || '',
|
||||
instanceId: detail.value?.id,
|
||||
});
|
||||
if (res.errorCode !== 0) {
|
||||
return;
|
||||
}
|
||||
ElMessage.success($t('approval.message.actionSuccess'));
|
||||
await loadDetail();
|
||||
}
|
||||
|
||||
function getStatusType(status: string) {
|
||||
const map: Record<string, any> = {
|
||||
APPROVED: 'success',
|
||||
PENDING: 'warning',
|
||||
PROCESSING: 'primary',
|
||||
REJECTED: 'danger',
|
||||
REVOKED: 'info',
|
||||
};
|
||||
return map[status] || 'info';
|
||||
}
|
||||
|
||||
function getStatusLabel(status: string) {
|
||||
return $t(`approval.status.${String(status || '').toLowerCase()}`);
|
||||
}
|
||||
|
||||
function formatPayload(payload: Record<string, any>) {
|
||||
return JSON.stringify(payload || {}, null, 2);
|
||||
}
|
||||
|
||||
function formatAccountDisplay(name?: string, id?: null | number | string) {
|
||||
if (name && id) {
|
||||
return `${name}(${id})`;
|
||||
}
|
||||
if (name) {
|
||||
return name;
|
||||
}
|
||||
return id || '-';
|
||||
}
|
||||
|
||||
function formatOperatorId(id?: null | number | string) {
|
||||
return id || '-';
|
||||
}
|
||||
|
||||
function formatOperatorName(name?: null | string) {
|
||||
return name || '-';
|
||||
}
|
||||
|
||||
function formatAssigneeDisplay(row: Record<string, any>) {
|
||||
if (!row?.assigneeType || !row?.assigneeTargetName) {
|
||||
return '-';
|
||||
}
|
||||
if (row.assigneeType === 'ROLE') {
|
||||
return `${$t('approval.assignee.role')}:${row.assigneeTargetName}`;
|
||||
}
|
||||
return `${$t('approval.assignee.user')}:${row.assigneeTargetName}`;
|
||||
}
|
||||
|
||||
function getEventTypeLabel(eventType?: string) {
|
||||
const eventTypeMap: Record<string, string> = {
|
||||
APPROVED: $t('approval.event.approved'),
|
||||
REJECTED: $t('approval.event.rejected'),
|
||||
REVOKED: $t('approval.event.revoked'),
|
||||
STEP_CREATED: $t('approval.event.stepCreated'),
|
||||
SUBMITTED: $t('approval.event.submitted'),
|
||||
};
|
||||
return eventTypeMap[String(eventType || '')] || eventType || '-';
|
||||
}
|
||||
|
||||
function formatEventInfo(row: Record<string, any>) {
|
||||
const payload = row?.payloadJson || {};
|
||||
const eventType = String(row?.eventType || '');
|
||||
|
||||
switch (eventType) {
|
||||
case 'APPROVED': {
|
||||
return payload.stepNo
|
||||
? $t('approval.message.eventApprovedStep', { value: payload.stepNo })
|
||||
: $t('approval.message.eventApproved');
|
||||
}
|
||||
case 'REJECTED': {
|
||||
return payload.stepNo
|
||||
? $t('approval.message.eventRejectedStep', { value: payload.stepNo })
|
||||
: $t('approval.message.eventRejected');
|
||||
}
|
||||
case 'REVOKED': {
|
||||
return payload.stepNo
|
||||
? $t('approval.message.eventRevokedStep', { value: payload.stepNo })
|
||||
: $t('approval.message.eventRevoked');
|
||||
}
|
||||
case 'STEP_CREATED': {
|
||||
return payload.stepName || '-';
|
||||
}
|
||||
case 'SUBMITTED': {
|
||||
return payload.summary || detail.value?.summary || '-';
|
||||
}
|
||||
default: {
|
||||
return payload.stepName || payload.summary || row?.eventType || '-';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex h-full flex-col gap-4 p-6" v-loading="loading">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<ElButton :icon="ArrowLeft" @click="router.back()">
|
||||
{{ $t('button.back') }}
|
||||
</ElButton>
|
||||
<div class="flex items-center gap-2 text-lg font-semibold">
|
||||
<IconifyIcon icon="svg:approval" class="size-5" />
|
||||
<span>{{ $t('approval.title') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="canOperate" class="flex items-center gap-3">
|
||||
<ElButton
|
||||
v-if="detail?.canApprove"
|
||||
type="success"
|
||||
@click="submitApprovalAction('approve')"
|
||||
>
|
||||
{{ $t('approval.action.approve') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="detail?.canReject"
|
||||
type="danger"
|
||||
@click="submitApprovalAction('reject')"
|
||||
>
|
||||
{{ $t('approval.action.reject') }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="detail?.canRevoke"
|
||||
@click="submitApprovalAction('revoke')"
|
||||
>
|
||||
{{ $t('approval.action.revoke') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="detail">
|
||||
<section class="approval-detail__panel">
|
||||
<div class="approval-detail__panel-header">
|
||||
<h3>{{ $t('approval.section.basic') }}</h3>
|
||||
<ElTag :type="getStatusType(detail.status)">
|
||||
{{ getStatusLabel(detail.status) }}
|
||||
</ElTag>
|
||||
</div>
|
||||
<ElDescriptions :column="2" border>
|
||||
<ElDescriptionsItem :label="$t('approval.fields.summary')">
|
||||
{{ detail.summary || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="$t('approval.fields.currentStep')">
|
||||
{{ detail.currentStepNo || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="$t('approval.fields.resourceType')">
|
||||
{{
|
||||
resourceLabelMap[detail.resourceType] ||
|
||||
detail.resourceType ||
|
||||
'-'
|
||||
}}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="$t('approval.fields.actionType')">
|
||||
{{ actionLabelMap[detail.actionType] || detail.actionType || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="$t('approval.fields.taskId')">
|
||||
{{ detail.id || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="$t('approval.fields.applicant')">
|
||||
{{ formatAccountDisplay(detail.applicantName, detail.applicantId) }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="$t('approval.fields.submittedAt')">
|
||||
{{ detail.submittedAt || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="$t('approval.fields.finishedAt')">
|
||||
{{ detail.finishedAt || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
</ElDescriptions>
|
||||
</section>
|
||||
|
||||
<section class="approval-detail__panel">
|
||||
<div class="approval-detail__panel-header">
|
||||
<h3>{{ $t('approval.section.tasks') }}</h3>
|
||||
</div>
|
||||
<ElTable :data="detail.tasks || []" show-overflow-tooltip>
|
||||
<ElTableColumn
|
||||
prop="stepNo"
|
||||
:label="$t('approval.fields.stepNoLabel')"
|
||||
width="100"
|
||||
/>
|
||||
<ElTableColumn
|
||||
prop="stepName"
|
||||
:label="$t('approval.fields.stepName')"
|
||||
min-width="180"
|
||||
/>
|
||||
<ElTableColumn :label="$t('approval.fields.status')" width="120">
|
||||
<template #default="{ row }">
|
||||
<ElTag :type="getStatusType(row.status)">
|
||||
{{ getStatusLabel(row.status) }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
:label="$t('approval.fields.assigneeTarget')"
|
||||
width="220"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatAssigneeDisplay(row) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn :label="$t('approval.fields.actedBy')" width="180">
|
||||
<template #default="{ row }">
|
||||
{{ formatAccountDisplay(row.actedByName, row.actedBy) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
prop="actedAt"
|
||||
:label="$t('approval.fields.actedAt')"
|
||||
width="180"
|
||||
/>
|
||||
<ElTableColumn
|
||||
prop="comment"
|
||||
:label="$t('approval.fields.comment')"
|
||||
min-width="200"
|
||||
/>
|
||||
</ElTable>
|
||||
</section>
|
||||
|
||||
<section class="approval-detail__panel">
|
||||
<div class="approval-detail__panel-header">
|
||||
<h3>{{ $t('approval.section.logs') }}</h3>
|
||||
</div>
|
||||
<ElTable :data="detail.logs || []" show-overflow-tooltip>
|
||||
<ElTableColumn :label="$t('approval.fields.eventType')" width="160">
|
||||
<template #default="{ row }">
|
||||
{{ getEventTypeLabel(row.eventType) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn :label="$t('approval.fields.operatorId')" width="140">
|
||||
<template #default="{ row }">
|
||||
{{ formatOperatorId(row.operatorId) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
:label="$t('approval.fields.operatorName')"
|
||||
width="180"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
{{ formatOperatorName(row.operatorName) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
prop="created"
|
||||
:label="$t('approval.fields.createdAt')"
|
||||
width="180"
|
||||
/>
|
||||
<ElTableColumn
|
||||
:label="$t('approval.fields.eventInfo')"
|
||||
min-width="280"
|
||||
>
|
||||
<template #default="{ row }">
|
||||
<div class="approval-detail__event-info">
|
||||
{{ formatEventInfo(row) }}
|
||||
</div>
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
</ElTable>
|
||||
</section>
|
||||
|
||||
<section class="approval-detail__panel">
|
||||
<div class="approval-detail__panel-header">
|
||||
<h3>{{ $t('approval.section.snapshot') }}</h3>
|
||||
</div>
|
||||
<WorkflowApprovalSnapshotPreview
|
||||
v-if="workflowSnapshot"
|
||||
:title="workflowSnapshot.title"
|
||||
:description="workflowSnapshot.description"
|
||||
:content="workflowSnapshot.content"
|
||||
/>
|
||||
<KnowledgeApprovalSnapshotPreview
|
||||
v-else-if="knowledgeSnapshot"
|
||||
:snapshot="knowledgeSnapshot"
|
||||
/>
|
||||
<BotApprovalSnapshotPreview
|
||||
v-else-if="botSnapshot"
|
||||
:snapshot="botSnapshot"
|
||||
/>
|
||||
<pre v-else class="approval-detail__snapshot">{{
|
||||
formatPayload(detail.snapshotJson)
|
||||
}}</pre>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<ElEmpty v-else />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.approval-detail__panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
hsl(var(--glass-border) / 72%) 0%,
|
||||
hsl(var(--surface-panel) / 95%) 100%
|
||||
);
|
||||
border: 1px solid hsl(var(--divider-faint) / 50%);
|
||||
border-radius: 24px;
|
||||
box-shadow:
|
||||
inset 0 1px 0 hsl(var(--glass-border) / 72%),
|
||||
0 18px 36px -32px hsl(var(--primary) / 20%);
|
||||
}
|
||||
|
||||
.approval-detail__panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.approval-detail__panel-header h3 {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.approval-detail__snapshot,
|
||||
.approval-detail__payload {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
padding: 14px 16px;
|
||||
background: hsl(var(--surface-contrast-soft) / 0.9);
|
||||
border: 1px solid hsl(var(--divider-faint) / 0.54);
|
||||
border-radius: 16px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.approval-detail__event-info {
|
||||
line-height: 1.6;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,792 @@
|
||||
<script setup lang="ts">
|
||||
import type { FormInstance } from 'element-plus';
|
||||
|
||||
import { nextTick, ref, watch } from 'vue';
|
||||
|
||||
import { EasyFlowFormModal } from '@easyflow/common-ui';
|
||||
|
||||
import {
|
||||
ElButton,
|
||||
ElDivider,
|
||||
ElForm,
|
||||
ElFormItem,
|
||||
ElInput,
|
||||
ElInputNumber,
|
||||
ElMessage,
|
||||
ElOption,
|
||||
ElSelect,
|
||||
ElSwitch,
|
||||
ElTreeSelect,
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const emit = defineEmits<{
|
||||
reload: [];
|
||||
}>();
|
||||
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
|
||||
type ResourceType = '' | 'BOT' | 'KNOWLEDGE' | 'WORKFLOW';
|
||||
type ActionType = '' | 'DELETE' | 'PUBLISH';
|
||||
type AssigneeType = 'ROLE' | 'USER';
|
||||
type ScopeType = 'CATEGORY' | 'DEPT';
|
||||
type FlowStatus = 'DISABLED' | 'ENABLED';
|
||||
|
||||
interface SelectOption {
|
||||
id: number | string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
interface ScopeItem {
|
||||
id?: number | string;
|
||||
includeChildren: boolean;
|
||||
scopeType: ScopeType;
|
||||
scopeValue?: number | string;
|
||||
}
|
||||
|
||||
interface StepItem {
|
||||
assigneeTargetCode?: string;
|
||||
assigneeTargetId?: number | string;
|
||||
assigneeTargetName?: string;
|
||||
assigneeType: AssigneeType;
|
||||
id?: number | string;
|
||||
stepName: string;
|
||||
}
|
||||
|
||||
interface FlowFormModel {
|
||||
actionType: ActionType;
|
||||
id?: number | string;
|
||||
name: string;
|
||||
priority: number;
|
||||
remark: string;
|
||||
resourceType: ResourceType;
|
||||
scopes: ScopeItem[];
|
||||
status: FlowStatus;
|
||||
steps: StepItem[];
|
||||
}
|
||||
|
||||
const RESOURCE_OPTIONS = [
|
||||
{ label: $t('approval.resource.bot'), value: 'BOT' },
|
||||
{ label: $t('approval.resource.workflow'), value: 'WORKFLOW' },
|
||||
{ label: $t('approval.resource.knowledge'), value: 'KNOWLEDGE' },
|
||||
];
|
||||
|
||||
const ACTION_OPTIONS = [
|
||||
{ label: $t('approval.action.publish'), value: 'PUBLISH' },
|
||||
{ label: $t('approval.action.delete'), value: 'DELETE' },
|
||||
];
|
||||
|
||||
const STATUS_OPTIONS = [
|
||||
{ label: $t('approval.status.enabled'), value: 'ENABLED' },
|
||||
{ label: $t('approval.status.disabled'), value: 'DISABLED' },
|
||||
];
|
||||
|
||||
const SCOPE_TYPE_OPTIONS = [
|
||||
{ label: $t('approval.scope.category'), value: 'CATEGORY' },
|
||||
{ label: $t('approval.scope.dept'), value: 'DEPT' },
|
||||
];
|
||||
|
||||
const ASSIGNEE_TYPE_OPTIONS: Array<{ label: string; value: AssigneeType }> = [
|
||||
{ label: $t('approval.assignee.role'), value: 'ROLE' },
|
||||
{ label: $t('approval.assignee.user'), value: 'USER' },
|
||||
];
|
||||
|
||||
const saveForm = ref<FormInstance>();
|
||||
const dialogVisible = ref(false);
|
||||
const isAdd = ref(true);
|
||||
const btnLoading = ref(false);
|
||||
const categoryLoaded = ref(false);
|
||||
const deptLoaded = ref(false);
|
||||
const roleLoaded = ref(false);
|
||||
const accountLoading = ref(false);
|
||||
const deptTreeOptions = ref<any[]>([]);
|
||||
const roleOptions = ref<SelectOption[]>([]);
|
||||
const accountOptions = ref<SelectOption[]>([]);
|
||||
const categoryOptions = ref<Record<Exclude<ResourceType, ''>, SelectOption[]>>({
|
||||
BOT: [],
|
||||
KNOWLEDGE: [],
|
||||
WORKFLOW: [],
|
||||
});
|
||||
|
||||
const formModel = ref<FlowFormModel>(buildDefaultForm());
|
||||
|
||||
const rules = {
|
||||
actionType: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'change' },
|
||||
],
|
||||
name: [{ required: true, message: $t('message.required'), trigger: 'blur' }],
|
||||
priority: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'change' },
|
||||
],
|
||||
resourceType: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'change' },
|
||||
],
|
||||
status: [
|
||||
{ required: true, message: $t('message.required'), trigger: 'change' },
|
||||
],
|
||||
};
|
||||
|
||||
watch(
|
||||
() => formModel.value.resourceType,
|
||||
(resourceType) => {
|
||||
formModel.value.scopes = formModel.value.scopes.map((scope) => {
|
||||
if (scope.scopeType === 'CATEGORY') {
|
||||
return {
|
||||
...scope,
|
||||
scopeValue: undefined,
|
||||
};
|
||||
}
|
||||
return scope;
|
||||
});
|
||||
if (resourceType) {
|
||||
void ensureCategoryOptions();
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
function buildDefaultForm(): FlowFormModel {
|
||||
return {
|
||||
actionType: 'PUBLISH',
|
||||
name: '',
|
||||
priority: 100,
|
||||
remark: '',
|
||||
resourceType: 'BOT',
|
||||
scopes: [],
|
||||
status: 'ENABLED',
|
||||
steps: [{ assigneeType: 'ROLE', stepName: '' }],
|
||||
};
|
||||
}
|
||||
|
||||
async function openDialog(row: any = {}) {
|
||||
isAdd.value = !row?.id;
|
||||
formModel.value = buildDefaultForm();
|
||||
dialogVisible.value = true;
|
||||
await Promise.all([
|
||||
ensureCategoryOptions(),
|
||||
ensureDeptOptions(),
|
||||
ensureRoleOptions(),
|
||||
]);
|
||||
if (!row?.id) {
|
||||
await nextTick();
|
||||
saveForm.value?.clearValidate();
|
||||
return;
|
||||
}
|
||||
btnLoading.value = true;
|
||||
try {
|
||||
const res = await api.get('/api/v1/approvalFlow/detail', {
|
||||
params: {
|
||||
id: row.id,
|
||||
},
|
||||
});
|
||||
if (res.errorCode !== 0) {
|
||||
return;
|
||||
}
|
||||
formModel.value = {
|
||||
actionType: res.data?.actionType || 'PUBLISH',
|
||||
id: res.data?.id,
|
||||
name: res.data?.name || '',
|
||||
priority: Number(res.data?.priority || 100),
|
||||
remark: res.data?.remark || '',
|
||||
resourceType: res.data?.resourceType || 'BOT',
|
||||
scopes: (res.data?.scopes || []).map((item: any) => ({
|
||||
id: item.id,
|
||||
includeChildren: item.includeChildren === 1,
|
||||
scopeType: item.scopeType,
|
||||
scopeValue: item.scopeValue,
|
||||
})),
|
||||
status: res.data?.status || 'ENABLED',
|
||||
steps: (res.data?.steps || []).map((item: any) => ({
|
||||
assigneeTargetCode: item.assigneeTargetCode,
|
||||
assigneeTargetId: item.assigneeTargetId,
|
||||
assigneeTargetName: item.assigneeTargetName,
|
||||
assigneeType: item.assigneeType || 'ROLE',
|
||||
id: item.id,
|
||||
stepName: item.stepName,
|
||||
})),
|
||||
};
|
||||
if (formModel.value.steps.length === 0) {
|
||||
formModel.value.steps = [{ assigneeType: 'ROLE', stepName: '' }];
|
||||
}
|
||||
for (const step of formModel.value.steps) {
|
||||
registerAccountOption(step);
|
||||
}
|
||||
if (formModel.value.steps.some((item) => item.assigneeType === 'USER')) {
|
||||
await searchAccountOptions('');
|
||||
}
|
||||
} finally {
|
||||
btnLoading.value = false;
|
||||
await nextTick();
|
||||
saveForm.value?.clearValidate();
|
||||
}
|
||||
}
|
||||
|
||||
async function ensureCategoryOptions() {
|
||||
if (categoryLoaded.value) {
|
||||
return;
|
||||
}
|
||||
const [botRes, workflowRes, knowledgeRes] = await Promise.all([
|
||||
api.get('/api/v1/botCategory/list', {
|
||||
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||||
}),
|
||||
api.get('/api/v1/workflowCategory/list', {
|
||||
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||||
}),
|
||||
api.get('/api/v1/documentCollectionCategory/list', {
|
||||
params: { sortKey: 'sortNo', sortType: 'asc' },
|
||||
}),
|
||||
]);
|
||||
categoryOptions.value = {
|
||||
BOT: normalizeCategoryOptions(botRes.data, 'categoryName'),
|
||||
KNOWLEDGE: normalizeCategoryOptions(knowledgeRes.data, 'categoryName'),
|
||||
WORKFLOW: normalizeCategoryOptions(workflowRes.data, 'categoryName'),
|
||||
};
|
||||
categoryLoaded.value = true;
|
||||
}
|
||||
|
||||
async function ensureDeptOptions() {
|
||||
if (deptLoaded.value) {
|
||||
return;
|
||||
}
|
||||
const res = await api.get('/api/v1/sysDept/list', {
|
||||
params: {
|
||||
asTree: true,
|
||||
sortKey: 'sortNo',
|
||||
sortType: 'asc',
|
||||
},
|
||||
});
|
||||
deptTreeOptions.value = Array.isArray(res.data) ? res.data : [];
|
||||
deptLoaded.value = true;
|
||||
}
|
||||
|
||||
async function ensureRoleOptions() {
|
||||
if (roleLoaded.value) {
|
||||
return;
|
||||
}
|
||||
const res = await api.get('/api/v1/approvalFlow/assigneeRoleOptions');
|
||||
roleOptions.value = normalizeSelectOptions(res.data);
|
||||
roleLoaded.value = true;
|
||||
}
|
||||
|
||||
async function searchAccountOptions(keyword = '') {
|
||||
accountLoading.value = true;
|
||||
try {
|
||||
const res = await api.get('/api/v1/approvalFlow/assigneeAccountPage', {
|
||||
params: {
|
||||
keyword,
|
||||
pageNumber: 1,
|
||||
pageSize: 20,
|
||||
},
|
||||
});
|
||||
for (const item of normalizeSelectOptions(res?.data?.records)) {
|
||||
if (
|
||||
!accountOptions.value.some(
|
||||
(option) => String(option.id) === String(item.id),
|
||||
)
|
||||
) {
|
||||
accountOptions.value.push(item);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
accountLoading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeCategoryOptions(data: any[] = [], labelKey: string) {
|
||||
return (Array.isArray(data) ? data : []).map((item) => ({
|
||||
id: item.id,
|
||||
label: item[labelKey],
|
||||
}));
|
||||
}
|
||||
|
||||
function normalizeSelectOptions(data: any[] = []) {
|
||||
return (Array.isArray(data) ? data : []).map((item) => ({
|
||||
id: item.id,
|
||||
label: item.name || item.label || item.code || String(item.id),
|
||||
}));
|
||||
}
|
||||
|
||||
function addScope() {
|
||||
formModel.value.scopes.push({
|
||||
includeChildren: false,
|
||||
scopeType: 'CATEGORY',
|
||||
scopeValue: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
function removeScope(index: number) {
|
||||
formModel.value.scopes.splice(index, 1);
|
||||
}
|
||||
|
||||
function addStep() {
|
||||
formModel.value.steps.push({ assigneeType: 'ROLE', stepName: '' });
|
||||
}
|
||||
|
||||
function removeStep(index: number) {
|
||||
if (formModel.value.steps.length <= 1) {
|
||||
return;
|
||||
}
|
||||
formModel.value.steps.splice(index, 1);
|
||||
}
|
||||
|
||||
function getCategoryScopeOptions() {
|
||||
const resourceType = formModel.value.resourceType;
|
||||
if (!resourceType) {
|
||||
return [];
|
||||
}
|
||||
return categoryOptions.value[resourceType];
|
||||
}
|
||||
|
||||
function handleAssigneeTypeChange(step: StepItem) {
|
||||
step.assigneeTargetId = undefined;
|
||||
step.assigneeTargetCode = '';
|
||||
step.assigneeTargetName = '';
|
||||
if (step.assigneeType === 'USER') {
|
||||
void searchAccountOptions('');
|
||||
}
|
||||
}
|
||||
|
||||
function handleAssigneeTargetChange(step: StepItem) {
|
||||
const options =
|
||||
step.assigneeType === 'ROLE' ? roleOptions.value : accountOptions.value;
|
||||
const selected = options.find(
|
||||
(item) => String(item.id) === String(step.assigneeTargetId),
|
||||
);
|
||||
step.assigneeTargetName = selected?.label || '';
|
||||
}
|
||||
|
||||
function registerAccountOption(step?: StepItem) {
|
||||
if (!step?.assigneeTargetId || !step.assigneeTargetName) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
accountOptions.value.some(
|
||||
(item) => String(item.id) === String(step.assigneeTargetId),
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
accountOptions.value.push({
|
||||
id: step.assigneeTargetId,
|
||||
label: step.assigneeTargetName,
|
||||
});
|
||||
}
|
||||
|
||||
async function save() {
|
||||
saveForm.value?.validate(async (valid) => {
|
||||
if (!valid) {
|
||||
return;
|
||||
}
|
||||
btnLoading.value = true;
|
||||
try {
|
||||
const steps = formModel.value.steps
|
||||
.map((step, index) => ({
|
||||
assigneeTargetId: step.assigneeTargetId,
|
||||
assigneeType: step.assigneeType,
|
||||
id: step.id,
|
||||
stepName: step.stepName?.trim(),
|
||||
stepNo: index + 1,
|
||||
}))
|
||||
.filter((step) => step.stepName);
|
||||
if (steps.length === 0) {
|
||||
ElMessage.warning($t('approval.message.needStep'));
|
||||
btnLoading.value = false;
|
||||
return;
|
||||
}
|
||||
if (steps.some((step) => !step.assigneeType || !step.assigneeTargetId)) {
|
||||
ElMessage.warning($t('approval.message.needStepAssignee'));
|
||||
btnLoading.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const scopes = formModel.value.scopes
|
||||
.filter((scope) => scope.scopeValue && scope.scopeType)
|
||||
.map((scope) => ({
|
||||
id: scope.id,
|
||||
includeChildren: scope.includeChildren ? 1 : 0,
|
||||
scopeType: scope.scopeType,
|
||||
scopeValue: scope.scopeValue,
|
||||
}));
|
||||
|
||||
const payload = {
|
||||
actionType: formModel.value.actionType,
|
||||
id: formModel.value.id,
|
||||
name: formModel.value.name.trim(),
|
||||
priority: formModel.value.priority,
|
||||
remark: formModel.value.remark.trim(),
|
||||
resourceType: formModel.value.resourceType,
|
||||
scopes,
|
||||
status: formModel.value.status,
|
||||
steps,
|
||||
};
|
||||
|
||||
const url = isAdd.value
|
||||
? '/api/v1/approvalFlow/save'
|
||||
: '/api/v1/approvalFlow/update';
|
||||
const res = await api.post(url, payload);
|
||||
if (res.errorCode !== 0) {
|
||||
btnLoading.value = false;
|
||||
return;
|
||||
}
|
||||
ElMessage.success($t('approval.message.saveSuccess'));
|
||||
emit('reload');
|
||||
closeDialog();
|
||||
} finally {
|
||||
btnLoading.value = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
formModel.value = buildDefaultForm();
|
||||
dialogVisible.value = false;
|
||||
saveForm.value?.resetFields();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<EasyFlowFormModal
|
||||
v-model:open="dialogVisible"
|
||||
:title="
|
||||
isAdd ? $t('approval.action.addFlow') : $t('approval.action.editFlow')
|
||||
"
|
||||
:before-close="closeDialog"
|
||||
:confirm-loading="btnLoading"
|
||||
:submitting="btnLoading"
|
||||
:confirm-text="$t('button.save')"
|
||||
width="xl"
|
||||
@confirm="save"
|
||||
>
|
||||
<ElForm
|
||||
ref="saveForm"
|
||||
:model="formModel"
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
class="easyflow-modal-form"
|
||||
>
|
||||
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||
<ElFormItem prop="name" :label="$t('approval.fields.flowName')">
|
||||
<ElInput v-model.trim="formModel.name" maxlength="128" />
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="priority" :label="$t('approval.fields.priority')">
|
||||
<ElInputNumber
|
||||
v-model="formModel.priority"
|
||||
:min="0"
|
||||
:step="10"
|
||||
class="!w-full"
|
||||
/>
|
||||
</ElFormItem>
|
||||
<ElFormItem
|
||||
prop="resourceType"
|
||||
:label="$t('approval.fields.resourceType')"
|
||||
>
|
||||
<ElSelect v-model="formModel.resourceType">
|
||||
<ElOption
|
||||
v-for="item in RESOURCE_OPTIONS"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="actionType" :label="$t('approval.fields.actionType')">
|
||||
<ElSelect v-model="formModel.actionType">
|
||||
<ElOption
|
||||
v-for="item in ACTION_OPTIONS"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="status" :label="$t('approval.fields.status')">
|
||||
<ElSelect v-model="formModel.status">
|
||||
<ElOption
|
||||
v-for="item in STATUS_OPTIONS"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
</ElFormItem>
|
||||
<ElFormItem prop="remark" :label="$t('approval.fields.remark')">
|
||||
<ElInput v-model.trim="formModel.remark" maxlength="500" />
|
||||
</ElFormItem>
|
||||
</div>
|
||||
|
||||
<ElDivider>{{ $t('approval.section.scope') }}</ElDivider>
|
||||
<div class="approval-flow-modal__section">
|
||||
<div class="approval-flow-modal__section-meta">
|
||||
<p>{{ $t('approval.helper.scope') }}</p>
|
||||
<ElButton text type="primary" class="w-fit" @click="addScope">
|
||||
{{ $t('approval.action.addScope') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<div class="approval-flow-modal__scope-list">
|
||||
<div
|
||||
v-if="formModel.scopes.length === 0"
|
||||
class="approval-flow-modal__scope-empty"
|
||||
>
|
||||
{{ $t('approval.helper.scopeEmpty') }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-for="(scope, index) in formModel.scopes"
|
||||
:key="`scope-${index}`"
|
||||
class="approval-flow-modal__scope-row"
|
||||
>
|
||||
<ElSelect
|
||||
v-model="scope.scopeType"
|
||||
class="approval-flow-modal__scope-control approval-flow-modal__scope-type"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in SCOPE_TYPE_OPTIONS"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value"
|
||||
/>
|
||||
</ElSelect>
|
||||
|
||||
<ElSelect
|
||||
v-if="scope.scopeType === 'CATEGORY'"
|
||||
v-model="scope.scopeValue"
|
||||
class="approval-flow-modal__scope-control"
|
||||
filterable
|
||||
clearable
|
||||
:placeholder="$t('approval.placeholder.scopeValue')"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in getCategoryScopeOptions()"
|
||||
:key="item.id"
|
||||
:label="item.label"
|
||||
:value="item.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
|
||||
<ElTreeSelect
|
||||
v-else
|
||||
v-model="scope.scopeValue"
|
||||
class="approval-flow-modal__scope-control"
|
||||
clearable
|
||||
check-strictly
|
||||
:data="deptTreeOptions"
|
||||
:props="{
|
||||
label: 'deptName',
|
||||
children: 'children',
|
||||
value: 'id',
|
||||
}"
|
||||
:placeholder="$t('approval.placeholder.scopeValue')"
|
||||
/>
|
||||
|
||||
<div class="approval-flow-modal__scope-switch">
|
||||
<span>{{ $t('approval.fields.includeChildren') }}</span>
|
||||
<ElSwitch v-model="scope.includeChildren" />
|
||||
</div>
|
||||
|
||||
<ElButton text type="danger" @click="removeScope(index)">
|
||||
{{ $t('button.delete') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ElDivider>{{ $t('approval.section.steps') }}</ElDivider>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div
|
||||
v-for="(step, index) in formModel.steps"
|
||||
:key="`step-${index}`"
|
||||
class="grid grid-cols-1 gap-3 rounded-2xl border border-[hsl(var(--divider-faint)/0.66)] bg-[hsl(var(--surface-panel)/0.72)] p-4 md:grid-cols-[72px,minmax(0,1.1fr),148px,minmax(0,1fr),88px]"
|
||||
>
|
||||
<div
|
||||
class="flex items-center text-sm font-medium text-[hsl(var(--text-muted))]"
|
||||
>
|
||||
{{ $t('approval.fields.stepNo', { value: index + 1 }) }}
|
||||
</div>
|
||||
<ElInput
|
||||
v-model.trim="step.stepName"
|
||||
:placeholder="$t('approval.placeholder.stepName')"
|
||||
/>
|
||||
<div class="approval-flow-modal__assignee-type">
|
||||
<button
|
||||
v-for="item in ASSIGNEE_TYPE_OPTIONS"
|
||||
:key="item.value"
|
||||
type="button"
|
||||
class="approval-flow-modal__assignee-type-item"
|
||||
:class="{
|
||||
'is-active': step.assigneeType === item.value,
|
||||
}"
|
||||
:aria-pressed="step.assigneeType === item.value"
|
||||
@click="
|
||||
step.assigneeType !== item.value &&
|
||||
((step.assigneeType = item.value),
|
||||
handleAssigneeTypeChange(step))
|
||||
"
|
||||
>
|
||||
{{ item.label }}
|
||||
</button>
|
||||
</div>
|
||||
<ElSelect
|
||||
v-if="step.assigneeType === 'ROLE'"
|
||||
v-model="step.assigneeTargetId"
|
||||
filterable
|
||||
clearable
|
||||
:placeholder="$t('approval.placeholder.assigneeTarget')"
|
||||
@change="handleAssigneeTargetChange(step)"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in roleOptions"
|
||||
:key="item.id"
|
||||
:label="item.label"
|
||||
:value="item.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
<ElSelect
|
||||
v-else
|
||||
v-model="step.assigneeTargetId"
|
||||
filterable
|
||||
clearable
|
||||
remote
|
||||
reserve-keyword
|
||||
:loading="accountLoading"
|
||||
:placeholder="$t('approval.placeholder.assigneeTarget')"
|
||||
:remote-method="searchAccountOptions"
|
||||
@change="handleAssigneeTargetChange(step)"
|
||||
@visible-change="(visible) => visible && searchAccountOptions('')"
|
||||
>
|
||||
<ElOption
|
||||
v-for="item in accountOptions"
|
||||
:key="item.id"
|
||||
:label="item.label"
|
||||
:value="item.id"
|
||||
/>
|
||||
</ElSelect>
|
||||
<ElButton
|
||||
text
|
||||
type="danger"
|
||||
:disabled="formModel.steps.length <= 1"
|
||||
@click="removeStep(index)"
|
||||
>
|
||||
{{ $t('button.delete') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
|
||||
<ElButton text type="primary" class="w-fit" @click="addStep">
|
||||
{{ $t('approval.action.addStep') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</ElForm>
|
||||
</EasyFlowFormModal>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.approval-flow-modal__section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.approval-flow-modal__section-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.approval-flow-modal__section-meta p {
|
||||
margin: 0;
|
||||
color: hsl(var(--text-muted));
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.approval-flow-modal__scope-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.approval-flow-modal__scope-empty {
|
||||
padding: 4px 0 2px;
|
||||
color: hsl(var(--text-muted));
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.approval-flow-modal__scope-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.approval-flow-modal__scope-row + .approval-flow-modal__scope-row {
|
||||
border-top: 1px solid hsl(var(--divider-faint) / 0.72);
|
||||
}
|
||||
|
||||
.approval-flow-modal__scope-control {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.approval-flow-modal__scope-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
color: hsl(var(--text-muted));
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.approval-flow-modal__assignee-type {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: fit-content;
|
||||
min-height: 40px;
|
||||
padding: 4px;
|
||||
border: 1px solid hsl(var(--divider-faint) / 0.72);
|
||||
border-radius: 14px;
|
||||
background: hsl(var(--surface-contrast-soft) / 0.82);
|
||||
}
|
||||
|
||||
.approval-flow-modal__assignee-type-item {
|
||||
min-width: 64px;
|
||||
height: 32px;
|
||||
padding: 0 14px;
|
||||
border: 0;
|
||||
border-radius: 10px;
|
||||
background: transparent;
|
||||
color: hsl(var(--text-muted));
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
line-height: 32px;
|
||||
transition:
|
||||
background-color 0.18s ease,
|
||||
color 0.18s ease,
|
||||
box-shadow 0.18s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.approval-flow-modal__assignee-type-item:hover {
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.approval-flow-modal__assignee-type-item:focus-visible {
|
||||
outline: 2px solid hsl(var(--primary) / 0.28);
|
||||
outline-offset: 1px;
|
||||
}
|
||||
|
||||
.approval-flow-modal__assignee-type-item.is-active {
|
||||
color: hsl(var(--primary));
|
||||
background: hsl(var(--primary) / 0.12);
|
||||
box-shadow: inset 0 0 0 1px hsl(var(--primary) / 0.14);
|
||||
}
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.approval-flow-modal__scope-row {
|
||||
grid-template-columns: 148px minmax(0, 1fr) 128px 72px;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
1134
easyflow-ui-admin/app/src/views/system/approval/ApprovalManage.vue
Normal file
1134
easyflow-ui-admin/app/src/views/system/approval/ApprovalManage.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,468 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { ElButton, ElTag } from 'element-plus';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const props = defineProps<{
|
||||
snapshot?: null | Record<string, any>;
|
||||
}>();
|
||||
|
||||
const bot = computed(() => props.snapshot || {});
|
||||
const expandedPrompt = ref(false);
|
||||
|
||||
const modelItems = computed(() => [
|
||||
{
|
||||
label: $t('approval.snapshot.modelName'),
|
||||
value: bot.value.modelName,
|
||||
},
|
||||
{
|
||||
label: $t('bot.temperature'),
|
||||
value: bot.value.temperature,
|
||||
},
|
||||
{
|
||||
label: 'Top P',
|
||||
value: bot.value.topP,
|
||||
},
|
||||
{
|
||||
label: 'Top K',
|
||||
value: bot.value.topK,
|
||||
},
|
||||
{
|
||||
label: $t('bot.maxReplyLength'),
|
||||
value: bot.value.maxReplyLength,
|
||||
},
|
||||
{
|
||||
label: $t('approval.snapshot.maxMessageCount'),
|
||||
value: bot.value.maxMessageCount,
|
||||
},
|
||||
]);
|
||||
|
||||
const bindingGroups = computed(() => [
|
||||
{
|
||||
items: Array.isArray(bot.value.knowledgeBindings)
|
||||
? bot.value.knowledgeBindings
|
||||
: [],
|
||||
key: 'knowledge',
|
||||
label: $t('approval.snapshot.knowledgeBindings'),
|
||||
},
|
||||
{
|
||||
items: Array.isArray(bot.value.workflowBindings)
|
||||
? bot.value.workflowBindings
|
||||
: [],
|
||||
key: 'workflow',
|
||||
label: $t('approval.snapshot.workflowBindings'),
|
||||
},
|
||||
{
|
||||
items: Array.isArray(bot.value.pluginBindings)
|
||||
? bot.value.pluginBindings
|
||||
: [],
|
||||
key: 'plugin',
|
||||
label: $t('approval.snapshot.pluginBindings'),
|
||||
},
|
||||
{
|
||||
items: Array.isArray(bot.value.mcpBindings) ? bot.value.mcpBindings : [],
|
||||
key: 'mcp',
|
||||
label: $t('approval.snapshot.mcpBindings'),
|
||||
},
|
||||
]);
|
||||
|
||||
const promptPreview = computed(() => bot.value.systemPrompt || '');
|
||||
|
||||
function formatValue(value?: null | number | string) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return $t('approval.snapshot.notConfigured');
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function getBindingName(groupKey: string, item: Record<string, any>) {
|
||||
switch (groupKey) {
|
||||
case 'knowledge': {
|
||||
return item.knowledgeName || $t('approval.snapshot.unnamedKnowledge');
|
||||
}
|
||||
case 'mcp': {
|
||||
return (
|
||||
item.mcpName || item.mcpToolName || $t('approval.snapshot.unnamedMcp')
|
||||
);
|
||||
}
|
||||
case 'plugin': {
|
||||
return item.pluginItemName || $t('approval.snapshot.unnamedPlugin');
|
||||
}
|
||||
case 'workflow': {
|
||||
return item.workflowName || $t('approval.snapshot.unnamedWorkflow');
|
||||
}
|
||||
default: {
|
||||
return $t('approval.snapshot.notConfigured');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getRetrievalModeLabel(mode?: string) {
|
||||
const normalized = String(mode || '').toLowerCase();
|
||||
if (!normalized) {
|
||||
return '';
|
||||
}
|
||||
const label = $t(`bot.retrievalModes.${normalized}`);
|
||||
return label === `bot.retrievalModes.${normalized}` ? String(mode) : label;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="snapshot-card">
|
||||
<div class="snapshot-card__hero">
|
||||
<div v-if="bot.icon" class="snapshot-card__avatar">
|
||||
<img :src="bot.icon" :alt="bot.title || $t('approval.resource.bot')" />
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="snapshot-card__avatar snapshot-card__avatar--placeholder"
|
||||
>
|
||||
{{ $t('approval.resource.bot').slice(0, 1) }}
|
||||
</div>
|
||||
<div class="snapshot-card__hero-copy">
|
||||
<div class="snapshot-card__title-row">
|
||||
<div class="snapshot-card__title">
|
||||
{{ bot.title || $t('approval.snapshot.untitledBot') }}
|
||||
</div>
|
||||
<div class="snapshot-card__meta-tags">
|
||||
<ElTag round effect="plain" type="primary">
|
||||
{{ bot.alias || $t('approval.snapshot.notConfigured') }}
|
||||
</ElTag>
|
||||
<ElTag
|
||||
round
|
||||
effect="plain"
|
||||
:type="bot.anonymousEnabled ? 'success' : 'info'"
|
||||
>
|
||||
{{
|
||||
bot.anonymousEnabled
|
||||
? $t('approval.snapshot.anonymousEnabled')
|
||||
: $t('approval.snapshot.anonymousDisabled')
|
||||
}}
|
||||
</ElTag>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="bot.description" class="snapshot-card__description">
|
||||
{{ bot.description }}
|
||||
</div>
|
||||
<div class="snapshot-card__submeta">
|
||||
<span>
|
||||
{{ $t('approval.snapshot.department') }}:{{
|
||||
formatValue(bot.deptName)
|
||||
}}
|
||||
</span>
|
||||
<span>
|
||||
{{ $t('approval.snapshot.category') }}:{{
|
||||
formatValue(bot.categoryName)
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="snapshot-card__section">
|
||||
<div class="snapshot-card__section-title">
|
||||
{{ $t('approval.snapshot.botModelConfig') }}
|
||||
</div>
|
||||
<div class="snapshot-card__info-grid">
|
||||
<div
|
||||
v-for="item in modelItems"
|
||||
:key="item.label"
|
||||
class="snapshot-card__info-item"
|
||||
>
|
||||
<div class="snapshot-card__label">{{ item.label }}</div>
|
||||
<div class="snapshot-card__value">{{ formatValue(item.value) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="snapshot-card__section">
|
||||
<div class="snapshot-card__section-head">
|
||||
<div class="snapshot-card__section-title">
|
||||
{{ $t('approval.snapshot.systemPrompt') }}
|
||||
</div>
|
||||
<ElButton
|
||||
v-if="promptPreview"
|
||||
link
|
||||
type="primary"
|
||||
@click="expandedPrompt = !expandedPrompt"
|
||||
>
|
||||
{{
|
||||
expandedPrompt
|
||||
? $t('approval.snapshot.collapsePrompt')
|
||||
: $t('approval.snapshot.expandPrompt')
|
||||
}}
|
||||
</ElButton>
|
||||
</div>
|
||||
<div
|
||||
v-if="promptPreview"
|
||||
class="snapshot-card__prompt"
|
||||
:class="{ 'snapshot-card__prompt--expanded': expandedPrompt }"
|
||||
>
|
||||
{{ promptPreview }}
|
||||
</div>
|
||||
<div v-else class="snapshot-card__empty-text">
|
||||
{{ $t('approval.snapshot.notConfigured') }}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="snapshot-card__section">
|
||||
<div class="snapshot-card__section-title">
|
||||
{{ $t('approval.snapshot.botBindings') }}
|
||||
</div>
|
||||
<div class="snapshot-card__bindings-grid">
|
||||
<div
|
||||
v-for="group in bindingGroups"
|
||||
:key="group.key"
|
||||
class="snapshot-card__binding-group"
|
||||
>
|
||||
<div class="snapshot-card__binding-head">
|
||||
<span>{{ group.label }}</span>
|
||||
<ElTag round effect="plain" type="info">
|
||||
{{ group.items.length }}
|
||||
</ElTag>
|
||||
</div>
|
||||
<div
|
||||
v-if="group.items.length > 0"
|
||||
class="snapshot-card__binding-list"
|
||||
>
|
||||
<div
|
||||
v-for="item in group.items"
|
||||
:key="`${group.key}-${JSON.stringify(item)}`"
|
||||
class="snapshot-card__binding-item"
|
||||
>
|
||||
<span>{{ getBindingName(group.key, item) }}</span>
|
||||
<ElTag
|
||||
v-if="group.key === 'knowledge' && item.retrievalMode"
|
||||
round
|
||||
effect="plain"
|
||||
type="primary"
|
||||
>
|
||||
{{ getRetrievalModeLabel(item.retrievalMode) }}
|
||||
</ElTag>
|
||||
<ElTag
|
||||
v-else-if="group.key === 'mcp' && item.mcpToolName"
|
||||
round
|
||||
effect="plain"
|
||||
type="success"
|
||||
>
|
||||
{{ item.mcpToolName }}
|
||||
</ElTag>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="snapshot-card__empty-text">
|
||||
{{ $t('approval.snapshot.noBindings') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.snapshot-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.snapshot-card__hero,
|
||||
.snapshot-card__section {
|
||||
border: 1px solid hsl(var(--divider-faint) / 0.54);
|
||||
border-radius: 20px;
|
||||
background: hsl(var(--surface-contrast-soft) / 0.9);
|
||||
}
|
||||
|
||||
.snapshot-card__hero {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.snapshot-card__avatar {
|
||||
flex-shrink: 0;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 18px;
|
||||
overflow: hidden;
|
||||
border: 1px solid hsl(var(--divider-faint) / 0.5);
|
||||
background: hsl(var(--surface) / 0.96);
|
||||
}
|
||||
|
||||
.snapshot-card__avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.snapshot-card__avatar--placeholder {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.snapshot-card__hero-copy {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.snapshot-card__title-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.snapshot-card__title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.snapshot-card__description {
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.snapshot-card__submeta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px 16px;
|
||||
font-size: 12px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.snapshot-card__meta-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.snapshot-card__section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.snapshot-card__section-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.snapshot-card__section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.snapshot-card__info-grid,
|
||||
.snapshot-card__bindings-grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.snapshot-card__info-item,
|
||||
.snapshot-card__binding-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 14px 16px;
|
||||
border-radius: 16px;
|
||||
background: hsl(var(--surface-panel) / 0.88);
|
||||
}
|
||||
|
||||
.snapshot-card__label {
|
||||
font-size: 12px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.snapshot-card__value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--foreground));
|
||||
line-height: 1.6;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.snapshot-card__prompt {
|
||||
max-height: 132px;
|
||||
overflow: hidden;
|
||||
padding: 14px 16px;
|
||||
border-radius: 16px;
|
||||
background: hsl(var(--surface-panel) / 0.88);
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
white-space: pre-wrap;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.snapshot-card__prompt--expanded {
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.snapshot-card__binding-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.snapshot-card__binding-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.snapshot-card__binding-item {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.snapshot-card__empty-text {
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.snapshot-card__hero {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.snapshot-card__title-row,
|
||||
.snapshot-card__section-head {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.snapshot-card__meta-tags {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.snapshot-card__info-grid,
|
||||
.snapshot-card__bindings-grid {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,372 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { ElTag } from 'element-plus';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const props = defineProps<{
|
||||
snapshot?: null | Record<string, any>;
|
||||
}>();
|
||||
|
||||
const knowledge = computed(() => props.snapshot || {});
|
||||
|
||||
const visibilityLabelMap: Record<string, string> = {
|
||||
DEPT: $t('documentCollection.visibilityScopeDept'),
|
||||
PRIVATE: $t('documentCollection.visibilityScopePrivate'),
|
||||
PUBLIC: $t('documentCollection.visibilityScopePublic'),
|
||||
};
|
||||
|
||||
const collectionTypeLabelMap: Record<string, string> = {
|
||||
DOCUMENT: $t('documentCollection.collectionTypeDocument'),
|
||||
FAQ: $t('documentCollection.collectionTypeFaq'),
|
||||
};
|
||||
|
||||
const visibilityLabel = computed(() => {
|
||||
const code = String(knowledge.value.visibilityScope || '').toUpperCase();
|
||||
return (
|
||||
knowledge.value.visibilityScopeLabel ||
|
||||
visibilityLabelMap[code] ||
|
||||
$t('approval.snapshot.notConfigured')
|
||||
);
|
||||
});
|
||||
|
||||
const collectionTypeLabel = computed(() => {
|
||||
const code = String(knowledge.value.collectionType || '').toUpperCase();
|
||||
return (
|
||||
knowledge.value.collectionTypeLabel ||
|
||||
collectionTypeLabelMap[code] ||
|
||||
$t('approval.snapshot.notConfigured')
|
||||
);
|
||||
});
|
||||
|
||||
const basicItems = computed(() => [
|
||||
{
|
||||
label: $t('documentCollection.alias'),
|
||||
value: knowledge.value.alias,
|
||||
},
|
||||
{
|
||||
label: $t('approval.snapshot.department'),
|
||||
value: knowledge.value.deptName,
|
||||
},
|
||||
{
|
||||
label: $t('approval.snapshot.category'),
|
||||
value: knowledge.value.categoryName,
|
||||
},
|
||||
{
|
||||
label: $t('documentCollection.englishName'),
|
||||
value: knowledge.value.englishName,
|
||||
},
|
||||
]);
|
||||
|
||||
const configItems = computed(() => [
|
||||
{
|
||||
label: $t('approval.snapshot.vectorStoreType'),
|
||||
value: knowledge.value.vectorStoreType,
|
||||
},
|
||||
{
|
||||
label: $t('approval.snapshot.vectorEmbedModel'),
|
||||
value: knowledge.value.vectorEmbedModelName,
|
||||
},
|
||||
{
|
||||
label: $t('documentCollection.dimensionOfVectorModel'),
|
||||
value: knowledge.value.dimensionOfVectorModel,
|
||||
},
|
||||
{
|
||||
label: $t('approval.snapshot.rerankModel'),
|
||||
value: knowledge.value.rerankModelName,
|
||||
},
|
||||
]);
|
||||
|
||||
function formatValue(value?: null | number | string) {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return $t('approval.snapshot.notConfigured');
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="snapshot-card">
|
||||
<div class="snapshot-card__hero">
|
||||
<div v-if="knowledge.icon" class="snapshot-card__avatar">
|
||||
<img
|
||||
:src="knowledge.icon"
|
||||
:alt="knowledge.title || $t('approval.resource.knowledge')"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="snapshot-card__avatar snapshot-card__avatar--placeholder"
|
||||
>
|
||||
{{ $t('approval.resource.knowledge').slice(0, 1) }}
|
||||
</div>
|
||||
<div class="snapshot-card__hero-copy">
|
||||
<div class="snapshot-card__title-row">
|
||||
<div class="snapshot-card__title">
|
||||
{{ knowledge.title || $t('approval.snapshot.untitledKnowledge') }}
|
||||
</div>
|
||||
<div class="snapshot-card__meta-tags">
|
||||
<ElTag round effect="plain" type="info">
|
||||
{{ visibilityLabel }}
|
||||
</ElTag>
|
||||
<ElTag round effect="plain" type="primary">
|
||||
{{ collectionTypeLabel }}
|
||||
</ElTag>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="knowledge.description" class="snapshot-card__description">
|
||||
{{ knowledge.description }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="snapshot-card__section">
|
||||
<div class="snapshot-card__section-title">
|
||||
{{ $t('approval.snapshot.knowledgeBasic') }}
|
||||
</div>
|
||||
<div class="snapshot-card__info-grid">
|
||||
<div
|
||||
v-for="item in basicItems"
|
||||
:key="item.label"
|
||||
class="snapshot-card__info-item"
|
||||
>
|
||||
<div class="snapshot-card__label">{{ item.label }}</div>
|
||||
<div class="snapshot-card__value">{{ formatValue(item.value) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="snapshot-card__section">
|
||||
<div class="snapshot-card__section-title">
|
||||
{{ $t('approval.snapshot.knowledgeConfig') }}
|
||||
</div>
|
||||
<div class="snapshot-card__toggles">
|
||||
<div class="snapshot-card__toggle">
|
||||
<span>{{ $t('documentCollection.vectorStoreEnable') }}</span>
|
||||
<ElTag
|
||||
round
|
||||
:type="knowledge.vectorStoreEnable ? 'success' : 'info'"
|
||||
effect="plain"
|
||||
>
|
||||
{{
|
||||
knowledge.vectorStoreEnable
|
||||
? $t('approval.snapshot.enabled')
|
||||
: $t('approval.snapshot.disabled')
|
||||
}}
|
||||
</ElTag>
|
||||
</div>
|
||||
<div class="snapshot-card__toggle">
|
||||
<span>{{ $t('documentCollection.searchEngineEnable') }}</span>
|
||||
<ElTag
|
||||
round
|
||||
:type="knowledge.searchEngineEnable ? 'success' : 'info'"
|
||||
effect="plain"
|
||||
>
|
||||
{{
|
||||
knowledge.searchEngineEnable
|
||||
? $t('approval.snapshot.enabled')
|
||||
: $t('approval.snapshot.disabled')
|
||||
}}
|
||||
</ElTag>
|
||||
</div>
|
||||
<div class="snapshot-card__toggle">
|
||||
<span>{{ $t('documentCollection.rerankEnable') }}</span>
|
||||
<ElTag
|
||||
round
|
||||
:type="knowledge.rerankEnable ? 'success' : 'info'"
|
||||
effect="plain"
|
||||
>
|
||||
{{
|
||||
knowledge.rerankEnable
|
||||
? $t('approval.snapshot.enabled')
|
||||
: $t('approval.snapshot.disabled')
|
||||
}}
|
||||
</ElTag>
|
||||
</div>
|
||||
<div class="snapshot-card__toggle">
|
||||
<span>{{ $t('approval.snapshot.canUpdateEmbeddingModel') }}</span>
|
||||
<ElTag
|
||||
round
|
||||
:type="knowledge.canUpdateEmbeddingModel ? 'success' : 'info'"
|
||||
effect="plain"
|
||||
>
|
||||
{{
|
||||
knowledge.canUpdateEmbeddingModel
|
||||
? $t('approval.snapshot.enabled')
|
||||
: $t('approval.snapshot.disabled')
|
||||
}}
|
||||
</ElTag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="snapshot-card__info-grid">
|
||||
<div
|
||||
v-for="item in configItems"
|
||||
:key="item.label"
|
||||
class="snapshot-card__info-item"
|
||||
>
|
||||
<div class="snapshot-card__label">{{ item.label }}</div>
|
||||
<div class="snapshot-card__value">{{ formatValue(item.value) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.snapshot-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.snapshot-card__hero,
|
||||
.snapshot-card__section {
|
||||
border: 1px solid hsl(var(--divider-faint) / 0.54);
|
||||
border-radius: 20px;
|
||||
background: hsl(var(--surface-contrast-soft) / 0.9);
|
||||
}
|
||||
|
||||
.snapshot-card__hero {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.snapshot-card__avatar {
|
||||
flex-shrink: 0;
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 18px;
|
||||
overflow: hidden;
|
||||
border: 1px solid hsl(var(--divider-faint) / 0.5);
|
||||
background: hsl(var(--surface) / 0.96);
|
||||
}
|
||||
|
||||
.snapshot-card__avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.snapshot-card__avatar--placeholder {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.snapshot-card__hero-copy {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.snapshot-card__title-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.snapshot-card__title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.snapshot-card__description {
|
||||
font-size: 13px;
|
||||
line-height: 1.7;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.snapshot-card__meta-tags {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.snapshot-card__section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.snapshot-card__section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.snapshot-card__info-grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.snapshot-card__info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 14px 16px;
|
||||
border-radius: 16px;
|
||||
background: hsl(var(--surface-panel) / 0.88);
|
||||
}
|
||||
|
||||
.snapshot-card__label {
|
||||
font-size: 12px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.snapshot-card__value {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: hsl(var(--foreground));
|
||||
line-height: 1.6;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.snapshot-card__toggles {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.snapshot-card__toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 14px 16px;
|
||||
border-radius: 16px;
|
||||
background: hsl(var(--surface-panel) / 0.88);
|
||||
font-size: 13px;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.snapshot-card__hero {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.snapshot-card__title-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.snapshot-card__meta-tags {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.snapshot-card__info-grid,
|
||||
.snapshot-card__toggles {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,157 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||
|
||||
import { usePreferences } from '@easyflow/preferences';
|
||||
|
||||
import {
|
||||
sanitizeTinyflowDataForReadonlyPreview,
|
||||
Tinyflow,
|
||||
} from '@tinyflow-ai/vue';
|
||||
import { ElEmpty, ElSkeleton } from 'element-plus';
|
||||
|
||||
import { $t } from '#/locales';
|
||||
import { getCustomNode } from '#/views/ai/workflow/customNode';
|
||||
|
||||
import '@tinyflow-ai/vue/dist/index.css';
|
||||
|
||||
const props = defineProps<{
|
||||
content?: null | string;
|
||||
description?: null | string;
|
||||
title?: null | string;
|
||||
}>();
|
||||
|
||||
const { isDark } = usePreferences();
|
||||
const customNodes = ref<null | Record<string, any>>(null);
|
||||
const tinyflowRef = ref<InstanceType<typeof Tinyflow> | null>(null);
|
||||
|
||||
const parsedData = computed(() => {
|
||||
return sanitizeTinyflowDataForReadonlyPreview(props.content || '');
|
||||
});
|
||||
|
||||
const hasSnapshot = computed(() => !!String(props.content || '').trim());
|
||||
const parseFailed = computed(() => parsedData.value === undefined);
|
||||
const canRender = computed(
|
||||
() => !!customNodes.value && parsedData.value && !parseFailed.value,
|
||||
);
|
||||
const tinyflowKey = computed(
|
||||
() => `${isDark.value ? 'dark' : 'light'}:${String(props.content || '')}`,
|
||||
);
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
customNodes.value = await getCustomNode({});
|
||||
} catch {
|
||||
customNodes.value = {};
|
||||
}
|
||||
});
|
||||
|
||||
watch(
|
||||
() => [customNodes.value, parsedData.value, isDark.value],
|
||||
async ([nodes, data]) => {
|
||||
if (!nodes || !data || data === undefined) {
|
||||
return;
|
||||
}
|
||||
await nextTick();
|
||||
requestAnimationFrame(() => {
|
||||
void tinyflowRef.value?.fitView({
|
||||
duration: 0,
|
||||
padding: 0.18,
|
||||
});
|
||||
});
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="workflow-snapshot-preview">
|
||||
<div class="workflow-snapshot-preview__meta">
|
||||
<div class="workflow-snapshot-preview__title">
|
||||
{{ title || $t('approval.message.workflowSnapshotUntitled') }}
|
||||
</div>
|
||||
<div v-if="description" class="workflow-snapshot-preview__description">
|
||||
{{ description }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!customNodes" class="workflow-snapshot-preview__loading">
|
||||
<ElSkeleton animated :rows="6" />
|
||||
</div>
|
||||
|
||||
<ElEmpty
|
||||
v-else-if="!hasSnapshot"
|
||||
:description="$t('approval.message.workflowSnapshotMissing')"
|
||||
class="workflow-snapshot-preview__empty"
|
||||
/>
|
||||
|
||||
<ElEmpty
|
||||
v-else-if="parseFailed"
|
||||
:description="$t('approval.message.workflowSnapshotParseFailed')"
|
||||
class="workflow-snapshot-preview__empty"
|
||||
/>
|
||||
|
||||
<div v-else-if="canRender" class="workflow-snapshot-preview__canvas">
|
||||
<Tinyflow
|
||||
ref="tinyflowRef"
|
||||
:key="tinyflowKey"
|
||||
class-name="workflow-snapshot-preview__tinyflow"
|
||||
:data="parsedData"
|
||||
:theme="isDark ? 'dark' : 'light'"
|
||||
:custom-nodes="customNodes || undefined"
|
||||
:readonly="true"
|
||||
:hide-bottom-dock="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.workflow-snapshot-preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.workflow-snapshot-preview__meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.workflow-snapshot-preview__title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.workflow-snapshot-preview__description {
|
||||
font-size: 13px;
|
||||
line-height: 1.6;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.workflow-snapshot-preview__loading,
|
||||
.workflow-snapshot-preview__empty,
|
||||
.workflow-snapshot-preview__canvas {
|
||||
border: 1px solid hsl(var(--divider-faint) / 0.54);
|
||||
border-radius: 20px;
|
||||
background: hsl(var(--surface-contrast-soft) / 0.9);
|
||||
}
|
||||
|
||||
.workflow-snapshot-preview__loading,
|
||||
.workflow-snapshot-preview__empty {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.workflow-snapshot-preview__canvas {
|
||||
overflow: hidden;
|
||||
min-height: 520px;
|
||||
}
|
||||
|
||||
:deep(.workflow-snapshot-preview__tinyflow) {
|
||||
height: 520px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
@@ -90,6 +90,7 @@ watchDebounced(
|
||||
'svg:talk',
|
||||
'svg:plugin',
|
||||
'svg:workflow',
|
||||
'svg:approval',
|
||||
'svg:knowledge',
|
||||
'svg:resource',
|
||||
'svg:data-center',
|
||||
|
||||
14
easyflow-ui-admin/packages/icons/src/svg/icons/approval.svg
Normal file
14
easyflow-ui-admin/packages/icons/src/svg/icons/approval.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none">
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="1.15"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect x="3.2" y="3.3" width="8.2" height="9.2" rx="1.6" />
|
||||
<path d="M5.4 3.3c.1-.9.8-1.5 1.7-1.5h.4c.9 0 1.6.6 1.7 1.5" />
|
||||
<path d="m5.2 8 1.4 1.4 2.6-2.8" />
|
||||
<path d="M5.3 11.2h3.8" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 419 B |
@@ -21,6 +21,7 @@ const SvgTDesignIcon = createIconifyIcon('svg:tdesign-logo');
|
||||
const SvgTalkIcon = createIconifyIcon('svg:talk');
|
||||
const SvgPluginIcon = createIconifyIcon('svg:plugin');
|
||||
const SvgWorkflowIcon = createIconifyIcon('svg:workflow');
|
||||
const SvgApprovalIcon = createIconifyIcon('svg:approval');
|
||||
const SvgKnowledgeIcon = createIconifyIcon('svg:knowledge');
|
||||
const SvgResourceIcon = createIconifyIcon('svg:resource');
|
||||
const SvgDataCenterIcon = createIconifyIcon('svg:data-center');
|
||||
@@ -36,6 +37,7 @@ const SvgApiIcon = createIconifyIcon('svg:api');
|
||||
|
||||
export {
|
||||
SvgAccountIcon,
|
||||
SvgApprovalIcon,
|
||||
SvgAntdvLogoIcon,
|
||||
SvgApiIcon,
|
||||
SvgAvatar1Icon,
|
||||
|
||||
@@ -139,6 +139,18 @@ export class Tinyflow {
|
||||
return true;
|
||||
}
|
||||
|
||||
async fitView(options?: { duration?: number; padding?: number }) {
|
||||
const flow = this._getFlowInstance();
|
||||
if (!flow) {
|
||||
return false;
|
||||
}
|
||||
await flow.fitView({
|
||||
duration: options?.duration ?? 220,
|
||||
padding: options?.padding ?? 0.2,
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
setTheme(theme: TinyflowTheme) {
|
||||
this.options.theme = theme;
|
||||
if (this.tinyflowEl) {
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
} = $props();
|
||||
|
||||
let { data } = options;
|
||||
let initialViewport = null;
|
||||
|
||||
if (typeof data === 'string') {
|
||||
try {
|
||||
@@ -28,7 +29,12 @@
|
||||
console.error('Invalid JSON data:', data);
|
||||
}
|
||||
}
|
||||
store.init((data as TinyflowData)?.nodes || [], (data as TinyflowData)?.edges || []);
|
||||
initialViewport = (data as TinyflowData)?.viewport || null;
|
||||
store.init(
|
||||
(data as TinyflowData)?.nodes || [],
|
||||
(data as TinyflowData)?.edges || [],
|
||||
initialViewport,
|
||||
);
|
||||
setContext('tinyflow_options', options);
|
||||
</script>
|
||||
|
||||
|
||||
@@ -57,9 +57,11 @@
|
||||
let flowRootEl = $state<HTMLDivElement | null>(null);
|
||||
let inlineNodePickerEl = $state<HTMLDivElement | null>(null);
|
||||
let connectStartPoint = $state<{ x: number; y: number } | null>(null);
|
||||
let canvasLocked = $state(false);
|
||||
const asString = (value: unknown) => (value == null ? '' : String(value));
|
||||
const options = getOptions();
|
||||
const readonly = options.readonly === true;
|
||||
let canvasLocked = $state(readonly);
|
||||
const hideBottomDock = options.hideBottomDock === true;
|
||||
const availableNodes = getAvailableNodes(options);
|
||||
const onRunTest = options.onRunTest;
|
||||
|
||||
@@ -567,6 +569,9 @@
|
||||
};
|
||||
|
||||
function handleGlobalPointerDown(event: PointerEvent) {
|
||||
if (readonly) {
|
||||
return;
|
||||
}
|
||||
if (!nodePickerVisible || !inlineNodePickerEl) {
|
||||
return;
|
||||
}
|
||||
@@ -579,15 +584,19 @@
|
||||
|
||||
onMount(() => {
|
||||
store.updateEdges((edges) => edges.map((edge) => ensureEdgeVisualDefaults(edge)));
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
window.addEventListener('paste', handleGlobalPaste);
|
||||
window.addEventListener('pointerdown', handleGlobalPointerDown);
|
||||
if (!readonly) {
|
||||
window.addEventListener('keydown', handleKeyDown);
|
||||
window.addEventListener('paste', handleGlobalPaste);
|
||||
window.addEventListener('pointerdown', handleGlobalPointerDown);
|
||||
}
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
window.removeEventListener('paste', handleGlobalPaste);
|
||||
window.removeEventListener('pointerdown', handleGlobalPointerDown);
|
||||
if (!readonly) {
|
||||
window.removeEventListener('keydown', handleKeyDown);
|
||||
window.removeEventListener('paste', handleGlobalPaste);
|
||||
window.removeEventListener('pointerdown', handleGlobalPointerDown);
|
||||
}
|
||||
});
|
||||
|
||||
const customNodeTypes = {
|
||||
@@ -630,24 +639,30 @@
|
||||
nodesDraggable={!canvasLocked}
|
||||
nodesConnectable={!canvasLocked}
|
||||
elementsSelectable={!canvasLocked}
|
||||
panOnDrag={!canvasLocked}
|
||||
zoomOnScroll={!canvasLocked}
|
||||
zoomOnDoubleClick={!canvasLocked}
|
||||
ondrop={onDrop}
|
||||
ondragover={onDragOver}
|
||||
panOnDrag={readonly ? true : !canvasLocked}
|
||||
zoomOnScroll={readonly ? true : !canvasLocked}
|
||||
zoomOnDoubleClick={readonly ? true : !canvasLocked}
|
||||
ondrop={readonly ? undefined : onDrop}
|
||||
ondragover={readonly ? undefined : onDragOver}
|
||||
isValidConnection={isValidConnection}
|
||||
onconnectend={onconnectend}
|
||||
onconnectstart={onconnectstart}
|
||||
onconnect={onconnect}
|
||||
onconnectend={readonly ? undefined : onconnectend}
|
||||
onconnectstart={readonly ? undefined : onconnectstart}
|
||||
onconnect={readonly ? undefined : onconnect}
|
||||
connectionRadius={50}
|
||||
connectionLineComponent={FlowConnectionLine}
|
||||
onedgeclick={(e) => {
|
||||
if (readonly) {
|
||||
return;
|
||||
}
|
||||
showEdgePanel = true;
|
||||
currentEdge = e.edge;
|
||||
}}
|
||||
onbeforeconnect={(edge: any) => normalizeEdgeBeforeConnect(edge)}
|
||||
ondelete={onDelete}
|
||||
ondelete={readonly ? undefined : onDelete}
|
||||
onclick={(e) => {
|
||||
if (readonly) {
|
||||
return;
|
||||
}
|
||||
const el = e.target as HTMLElement;
|
||||
if (el.classList.contains("svelte-flow__edge-interaction")
|
||||
|| el.classList.contains('panel-content')
|
||||
@@ -750,6 +765,7 @@
|
||||
<NodePicker nodes={availableNodes} onSelect={handlePickerSelectNode} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if !hideBottomDock}
|
||||
<div class="tf-bottom-dock">
|
||||
<div class="tf-unified-bar">
|
||||
<!-- 缩放百分比选择器 -->
|
||||
@@ -818,6 +834,7 @@
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -2,27 +2,14 @@ import { store } from '#store/stores.svelte';
|
||||
import { genShortId } from '#components/utils/IdGen';
|
||||
import { type Edge, type Node, useSvelteFlow } from '@xyflow/svelte';
|
||||
|
||||
import { sanitizeEdge, sanitizeNode } from '#utils/sanitize';
|
||||
|
||||
interface ClipboardData {
|
||||
tinyflowNodes: Node[];
|
||||
tinyflowEdges?: Edge[];
|
||||
version: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理节点中不可序列化的字段,确保可安全 JSON.stringify
|
||||
*/
|
||||
function sanitizeNode(node: Node): Node {
|
||||
const { id, type, position, data, parentId } = node;
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
position: { x: position.x, y: position.y },
|
||||
parentId: parentId ? parentId : undefined,
|
||||
data: data ? JSON.parse(JSON.stringify(data)) : {},
|
||||
...(parentId !== undefined && { parentId }),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 对 nodes 数组排序,确保每个父节点出现在其所有子节点之前。
|
||||
* 使用 Kahn 算法(拓扑排序)处理任意嵌套层级。
|
||||
@@ -83,22 +70,6 @@ export function sortNodesForSvelteFlow(nodes: Node[]): Node[] {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理边中不可序列化的字段
|
||||
*/
|
||||
function sanitizeEdge(edge: Edge): Edge {
|
||||
const { id, source, target, sourceHandle, targetHandle, type, data } = edge;
|
||||
return {
|
||||
id,
|
||||
source,
|
||||
target,
|
||||
...(sourceHandle !== undefined && { sourceHandle }),
|
||||
...(targetHandle !== undefined && { targetHandle }),
|
||||
...(type !== undefined && { type }),
|
||||
data: data ? JSON.parse(JSON.stringify(data)) : {},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归遍历对象,仅当遇到 { refType: 'ref', ref: string } 时重写 ref 的 id
|
||||
*/
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './types';
|
||||
export * from './Tinyflow';
|
||||
export * from './components/TinyflowComponent.svelte';
|
||||
export * from './utils/sanitize';
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { type Edge, type Node, type Viewport } from '@xyflow/svelte';
|
||||
|
||||
const DEFAULT_VIEWPORT: Viewport = { x: 250, y: 100, zoom: 1 };
|
||||
|
||||
const createStore = () => {
|
||||
let nodesInternal = $state.raw([] as Node[]);
|
||||
let edgesInternal = $state.raw([] as Edge[]);
|
||||
let viewport = $state.raw({ x: 250, y: 100, zoom: 1 } as Viewport);
|
||||
let viewport = $state.raw({ ...DEFAULT_VIEWPORT } as Viewport);
|
||||
|
||||
return {
|
||||
// nodes: nodesInternal,
|
||||
// edges: edgesInternal,
|
||||
// viewport,
|
||||
init: (nodes: Node[], edges: Edge[]) => {
|
||||
init: (nodes: Node[], edges: Edge[], nextViewport?: Viewport | null) => {
|
||||
nodesInternal = nodes;
|
||||
edgesInternal = edges;
|
||||
viewport = nextViewport ? { ...nextViewport } : { ...DEFAULT_VIEWPORT };
|
||||
},
|
||||
|
||||
getNodes: () => nodesInternal,
|
||||
|
||||
@@ -87,6 +87,8 @@ export type TinyflowOptions = {
|
||||
element: string | Element;
|
||||
theme?: TinyflowTheme;
|
||||
data?: TinyflowData | string;
|
||||
readonly?: boolean;
|
||||
hideBottomDock?: boolean;
|
||||
provider?: {
|
||||
llm?: () => SelectItem[] | Promise<SelectItem[]>;
|
||||
knowledge?: () => SelectItem[] | Promise<SelectItem[]>;
|
||||
|
||||
89
easyflow-ui-admin/packages/tinyflow-ui/src/utils/sanitize.ts
Normal file
89
easyflow-ui-admin/packages/tinyflow-ui/src/utils/sanitize.ts
Normal file
@@ -0,0 +1,89 @@
|
||||
import { type Edge, type Node } from '@xyflow/svelte';
|
||||
|
||||
import type { TinyflowData } from '#types';
|
||||
|
||||
/**
|
||||
* 清理节点中不适合序列化或预览的运行时字段。
|
||||
*/
|
||||
export function sanitizeNode(node: Node): Node {
|
||||
const { id, type, position, data, parentId } = node;
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
position: {
|
||||
x: position?.x ?? 0,
|
||||
y: position?.y ?? 0,
|
||||
},
|
||||
data: data ? JSON.parse(JSON.stringify(data)) : {},
|
||||
...(parentId !== undefined && { parentId }),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理边中不适合序列化或预览的运行时字段。
|
||||
*/
|
||||
export function sanitizeEdge(edge: Edge): Edge {
|
||||
const { id, source, target, sourceHandle, targetHandle, type, data } = edge;
|
||||
return {
|
||||
id,
|
||||
source,
|
||||
target,
|
||||
...(sourceHandle !== undefined && { sourceHandle }),
|
||||
...(targetHandle !== undefined && { targetHandle }),
|
||||
...(type !== undefined && { type }),
|
||||
data: data ? JSON.parse(JSON.stringify(data)) : {},
|
||||
};
|
||||
}
|
||||
|
||||
function sanitizeReadonlyNode(node: Node): Node {
|
||||
const sanitized = sanitizeNode(node);
|
||||
const nextData =
|
||||
sanitized.data && typeof sanitized.data === 'object'
|
||||
? {
|
||||
...sanitized.data,
|
||||
expand: false,
|
||||
}
|
||||
: {};
|
||||
return {
|
||||
...sanitized,
|
||||
data: nextData,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 为只读预览重建 tinyflow 数据,移除 viewport 和节点运行时几何状态。
|
||||
*/
|
||||
export function sanitizeTinyflowDataForReadonlyPreview(
|
||||
input: TinyflowData | string,
|
||||
): null | TinyflowData | undefined {
|
||||
let parsed: TinyflowData | undefined;
|
||||
if (typeof input === 'string') {
|
||||
const raw = input.trim();
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
parsed = JSON.parse(raw);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
} else {
|
||||
parsed = input;
|
||||
}
|
||||
|
||||
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const nodes = Array.isArray(parsed.nodes)
|
||||
? parsed.nodes.map((node) => sanitizeReadonlyNode(node as Node))
|
||||
: [];
|
||||
const edges = Array.isArray(parsed.edges)
|
||||
? parsed.edges.map((edge) => sanitizeEdge(edge as Edge))
|
||||
: [];
|
||||
|
||||
return {
|
||||
nodes,
|
||||
edges,
|
||||
};
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
<script setup lang="ts">
|
||||
import { Tinyflow as TinyflowNative, TinyflowOptions } from '@tinyflow-ai/ui';
|
||||
import '@tinyflow-ai/ui/dist/index.css';
|
||||
import { onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { nextTick, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
|
||||
const props = defineProps<
|
||||
{
|
||||
@@ -94,9 +94,19 @@ const focusNode = async (
|
||||
return false;
|
||||
};
|
||||
|
||||
const fitView = async (options?: { duration?: number; padding?: number }) => {
|
||||
if (tinyflow) {
|
||||
await nextTick();
|
||||
return tinyflow.fitView(options);
|
||||
}
|
||||
console.warn('Tinyflow instance is not initialized');
|
||||
return false;
|
||||
};
|
||||
|
||||
defineExpose({
|
||||
getData,
|
||||
getInstance,
|
||||
focusNode,
|
||||
fitView,
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import Tinyflow from './Tinyflow.vue';
|
||||
|
||||
export { sanitizeTinyflowDataForReadonlyPreview } from '@tinyflow-ai/ui';
|
||||
export { Tinyflow };
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
interface BotInfo {
|
||||
alias: string;
|
||||
anonymousEnabled: boolean;
|
||||
currentApprovalInstanceId?: number | string;
|
||||
created: string;
|
||||
createdBy: number;
|
||||
deptId: number;
|
||||
@@ -33,6 +34,10 @@ interface BotInfo {
|
||||
weChatMpToken: string;
|
||||
welcomeMessage?: string;
|
||||
};
|
||||
publishStatus?: string;
|
||||
publishedAt?: string;
|
||||
publishedBy?: number;
|
||||
publishedSnapshotJson?: Record<string, any>;
|
||||
tenantId: number;
|
||||
title: string;
|
||||
categoryId: any;
|
||||
|
||||
Reference in New Issue
Block a user