feat: 收敛AI资源发布审批生命周期
- 统一工作流、知识库、聊天助手的发布、重新发布、下线与删除链路 - 收敛审批编排、生命周期状态机与展示态,补齐审批管理和快照预览 - 调整审批管理权限模型为单入口页面加内部按钮权限
This commit is contained in:
@@ -61,6 +61,14 @@ export const submitBotPublishApproval = (id: string) => {
|
||||
);
|
||||
};
|
||||
|
||||
/** 提交 Bot 下线审批 */
|
||||
export const submitBotOfflineApproval = (id: string) => {
|
||||
return api.post<RequestResult<number | string>>(
|
||||
'/api/v1/bot/submitOfflineApproval',
|
||||
{ id },
|
||||
);
|
||||
};
|
||||
|
||||
/** 提交 Bot 删除审批 */
|
||||
export const submitBotDeleteApproval = (id: string) => {
|
||||
return api.post<RequestResult<number | string>>(
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface ActionButton {
|
||||
permission?: string;
|
||||
placement?: ActionPlacement;
|
||||
tone?: ActionTone;
|
||||
visible?: ((row: any) => boolean) | boolean;
|
||||
onClick: (row: any) => void;
|
||||
}
|
||||
|
||||
@@ -77,6 +78,13 @@ function hasPermission(permission?: string) {
|
||||
return !permission || hasAccessByCodes([permission]);
|
||||
}
|
||||
|
||||
function isActionVisible(action: ActionButton, row: any) {
|
||||
if (typeof action.visible === 'function') {
|
||||
return action.visible(row);
|
||||
}
|
||||
return action.visible !== false;
|
||||
}
|
||||
|
||||
const resolvedPrimaryAction = computed(() => {
|
||||
if (!props.primaryAction || !hasPermission(props.primaryAction.permission)) {
|
||||
return undefined;
|
||||
@@ -109,24 +117,6 @@ const resolvedActions = computed<ResolvedActionButton[]>(() => {
|
||||
}));
|
||||
});
|
||||
|
||||
const inlineActions = computed(() => {
|
||||
return resolvedActions.value.filter(
|
||||
(action) => action.placement === 'inline',
|
||||
);
|
||||
});
|
||||
|
||||
const menuActions = computed(() => {
|
||||
return resolvedActions.value.filter((action) => action.placement === 'menu');
|
||||
});
|
||||
|
||||
const showFooter = computed(() => {
|
||||
return Boolean(
|
||||
resolvedPrimaryAction.value ||
|
||||
inlineActions.value.length > 0 ||
|
||||
menuActions.value.length > 0,
|
||||
);
|
||||
});
|
||||
|
||||
function handlePrimaryAction(item: any) {
|
||||
resolvedPrimaryAction.value?.onClick(item);
|
||||
}
|
||||
@@ -139,6 +129,24 @@ function handleActionClick(event: Event, action: ActionButton, item: any) {
|
||||
function resolveActionText(action: ActionButton, item: any) {
|
||||
return typeof action.text === 'function' ? action.text(item) : action.text;
|
||||
}
|
||||
|
||||
function resolveInlineActions(item: any) {
|
||||
return resolvedActions.value.filter(
|
||||
(action) => action.placement === 'inline' && isActionVisible(action, item),
|
||||
);
|
||||
}
|
||||
|
||||
function resolveMenuActions(item: any) {
|
||||
return resolvedActions.value.filter(
|
||||
(action) => action.placement === 'menu' && isActionVisible(action, item),
|
||||
);
|
||||
}
|
||||
|
||||
function hasVisibleActions(item: any) {
|
||||
return (
|
||||
resolveInlineActions(item).length > 0 || resolveMenuActions(item).length > 0
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -198,7 +206,7 @@ function resolveActionText(action: ActionButton, item: any) {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="showFooter" #footer>
|
||||
<template v-if="resolvedPrimaryAction || hasVisibleActions(item)" #footer>
|
||||
<div class="card-footer">
|
||||
<div v-if="resolvedPrimaryAction" class="card-primary-hint">
|
||||
<div class="primary-label">
|
||||
@@ -214,12 +222,12 @@ function resolveActionText(action: ActionButton, item: any) {
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="inlineActions.length > 0 || menuActions.length > 0"
|
||||
v-if="hasVisibleActions(item)"
|
||||
class="card-actions"
|
||||
@click.stop
|
||||
>
|
||||
<ElButton
|
||||
v-for="(action, actionIndex) in inlineActions"
|
||||
v-for="(action, actionIndex) in resolveInlineActions(item)"
|
||||
:key="`${item.id ?? index}-inline-${actionIndex}`"
|
||||
:icon="typeof action.icon === 'string' ? undefined : action.icon"
|
||||
size="small"
|
||||
@@ -235,7 +243,7 @@ function resolveActionText(action: ActionButton, item: any) {
|
||||
</ElButton>
|
||||
|
||||
<ElDropdown
|
||||
v-if="menuActions.length > 0"
|
||||
v-if="resolveMenuActions(item).length > 0"
|
||||
trigger="click"
|
||||
placement="bottom-end"
|
||||
>
|
||||
@@ -248,7 +256,7 @@ function resolveActionText(action: ActionButton, item: any) {
|
||||
<template #dropdown>
|
||||
<ElDropdownMenu>
|
||||
<ElDropdownItem
|
||||
v-for="(action, actionIndex) in menuActions"
|
||||
v-for="(action, actionIndex) in resolveMenuActions(item)"
|
||||
:key="`${item.id ?? index}-menu-${actionIndex}`"
|
||||
:class="{
|
||||
'card-menu-item--danger': action.tone === 'danger',
|
||||
|
||||
@@ -65,10 +65,16 @@
|
||||
"publishStatusDraft": "Draft",
|
||||
"publishStatusPublishPending": "Publish Pending",
|
||||
"publishStatusPublished": "Published",
|
||||
"publishStatusOfflinePending": "Offline Pending",
|
||||
"publishStatusOffline": "Offline",
|
||||
"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.",
|
||||
"submitPublishApprovalConfirm": "Publish the current workflow now?",
|
||||
"submitRepublishApprovalConfirm": "Republish the current workflow now?",
|
||||
"submitOfflineApprovalConfirm": "Take the current workflow offline?",
|
||||
"submitDeleteApprovalConfirm": "Delete the current workflow?",
|
||||
"offlineImpactBoundBotsIntro": "This workflow is currently bound to the following bots:",
|
||||
"offlineImpactBoundBotsFooter": "After the workflow goes offline, the system will automatically remove it from these bots.",
|
||||
"publishPendingHint": "There is already an approval in progress for this workflow.",
|
||||
"deletePendingHint": "There is already an approval in progress for this workflow.",
|
||||
"check": "Check",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
},
|
||||
"action": {
|
||||
"publish": "Publish",
|
||||
"offline": "Offline",
|
||||
"delete": "Delete",
|
||||
"addFlow": "New Flow",
|
||||
"editFlow": "Edit Flow",
|
||||
@@ -79,7 +80,7 @@
|
||||
"actedAt": "Acted At",
|
||||
"comment": "Comment",
|
||||
"eventType": "Event Type",
|
||||
"operatorId": "Operator ID",
|
||||
"operatorId": "Operator Account",
|
||||
"operatorName": "Operator Name",
|
||||
"createdAt": "Created At",
|
||||
"eventInfo": "Event Info",
|
||||
|
||||
@@ -18,10 +18,16 @@
|
||||
"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.",
|
||||
"publishStatusOfflinePending": "Offline Pending",
|
||||
"publishStatusOfflinePendingDesc": "The current release stays available until approval completes, but it is hidden from new binding candidates.",
|
||||
"publishStatusOffline": "Offline",
|
||||
"publishStatusOfflineDesc": "The current release is offline. External chat, Public API, and new bindings are unavailable.",
|
||||
"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.",
|
||||
"submitPublishApprovalConfirm": "Publish the current assistant now?",
|
||||
"submitRepublishApprovalConfirm": "Republish the current assistant now?",
|
||||
"submitOfflineApprovalConfirm": "Take the current assistant offline?",
|
||||
"submitDeleteApprovalConfirm": "Delete the current assistant?",
|
||||
"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.",
|
||||
|
||||
@@ -41,7 +41,9 @@
|
||||
"markAsResolved": "MarkAsResolved",
|
||||
"optimizing": "Optimizing",
|
||||
"regenerate": "Regenerate",
|
||||
"publish": "Publish",
|
||||
"republish": "Republish",
|
||||
"offline": "Offline",
|
||||
"hide": "Hide",
|
||||
"more": "Mode",
|
||||
"submitDeleteApproval": "Submit Delete Approval",
|
||||
|
||||
@@ -38,10 +38,18 @@
|
||||
"publishStatusDraft": "Draft",
|
||||
"publishStatusPublishPending": "Publish Pending",
|
||||
"publishStatusPublished": "Published",
|
||||
"publishStatusOfflinePending": "Offline Pending",
|
||||
"publishStatusOffline": "Offline",
|
||||
"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.",
|
||||
"submitPublishApprovalConfirm": "Publish the current knowledge base now?",
|
||||
"submitRepublishApprovalConfirm": "Republish the current knowledge base now?",
|
||||
"submitOfflineApprovalConfirm": "Take the current knowledge base offline?",
|
||||
"submitDeleteApprovalConfirm": "Delete the current knowledge base?",
|
||||
"offlineImpactBoundBotsIntro": "This knowledge base is currently bound to the following bots:",
|
||||
"offlineImpactBoundBotsFooter": "After the knowledge base goes offline, the system will automatically remove it from these bots.",
|
||||
"offlineImpactWorkflowBlockedIntro": "This knowledge base is still used by the following workflows:",
|
||||
"offlineImpactWorkflowBlockedFooter": "Please update those workflow nodes before taking the knowledge base offline.",
|
||||
"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",
|
||||
|
||||
@@ -65,10 +65,16 @@
|
||||
"publishStatusDraft": "草稿",
|
||||
"publishStatusPublishPending": "发布审批中",
|
||||
"publishStatusPublished": "已发布",
|
||||
"publishStatusOfflinePending": "下线审批中",
|
||||
"publishStatusOffline": "已下线",
|
||||
"publishStatusDeletePending": "删除审批中",
|
||||
"publishStatusLabel": "发布状态",
|
||||
"submitPublishApprovalConfirm": "提交后会进入发布审批,审批通过后新版本才会正式对外可用。",
|
||||
"submitDeleteApprovalConfirm": "提交后会进入删除审批,审批通过后将执行真实删除。",
|
||||
"submitPublishApprovalConfirm": "确认发布当前工作流吗?",
|
||||
"submitRepublishApprovalConfirm": "确认重新发布当前工作流吗?",
|
||||
"submitOfflineApprovalConfirm": "确认下线当前工作流吗?",
|
||||
"submitDeleteApprovalConfirm": "确认删除当前工作流吗?",
|
||||
"offlineImpactBoundBotsIntro": "当前工作流被以下聊天助手绑定:",
|
||||
"offlineImpactBoundBotsFooter": "下线成功后,系统会自动从这些聊天助手中解绑该工作流。",
|
||||
"publishPendingHint": "当前工作流已有进行中的审批,请等待处理完成。",
|
||||
"deletePendingHint": "当前工作流已有进行中的审批,请等待处理完成。",
|
||||
"check": "检查",
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
},
|
||||
"action": {
|
||||
"publish": "发布",
|
||||
"offline": "下线",
|
||||
"delete": "删除",
|
||||
"addFlow": "新建流程",
|
||||
"editFlow": "编辑流程",
|
||||
@@ -79,7 +80,7 @@
|
||||
"actedAt": "处理时间",
|
||||
"comment": "处理意见",
|
||||
"eventType": "事件类型",
|
||||
"operatorId": "操作人ID",
|
||||
"operatorId": "操作人账号",
|
||||
"operatorName": "操作人名称",
|
||||
"createdAt": "创建时间",
|
||||
"eventInfo": "事件信息",
|
||||
|
||||
@@ -18,10 +18,16 @@
|
||||
"publishStatusPublishPendingDesc": "审批通过后,聊天助手会切换为新的正式版本。",
|
||||
"publishStatusPublished": "已发布",
|
||||
"publishStatusPublishedDesc": "当前正式版本已可对外使用,编辑中的草稿不会立即影响线上。",
|
||||
"publishStatusOfflinePending": "下线审批中",
|
||||
"publishStatusOfflinePendingDesc": "审批完成前当前正式版本仍可访问,但不会继续作为新的绑定候选。",
|
||||
"publishStatusOffline": "已下线",
|
||||
"publishStatusOfflineDesc": "当前正式版本已下线,外链聊天、Public API 和新的资源绑定都不可用。",
|
||||
"publishStatusDeletePending": "删除审批中",
|
||||
"publishStatusDeletePendingDesc": "当前正式版本仍可访问,但不会继续作为新的绑定候选。",
|
||||
"submitPublishApprovalConfirm": "提交后会进入发布审批,审批通过后聊天助手才会正式对外可用。",
|
||||
"submitDeleteApprovalConfirm": "提交后会进入删除审批,审批通过后将执行真实删除。",
|
||||
"submitPublishApprovalConfirm": "确认发布当前聊天助手吗?",
|
||||
"submitRepublishApprovalConfirm": "确认重新发布当前聊天助手吗?",
|
||||
"submitOfflineApprovalConfirm": "确认下线当前聊天助手吗?",
|
||||
"submitDeleteApprovalConfirm": "确认删除当前聊天助手吗?",
|
||||
"publishPendingHint": "当前聊天助手已有进行中的审批,请等待处理完成。",
|
||||
"deletePendingHint": "当前聊天助手已有进行中的审批,请等待处理完成。",
|
||||
"publishRequiredHint": "当前还没有正式发布版本,请先提交发布审批。",
|
||||
|
||||
@@ -41,7 +41,9 @@
|
||||
"markAsResolved": "标记已处理",
|
||||
"optimizing": "正在优化中...",
|
||||
"regenerate": "重新生成",
|
||||
"publish": "发布",
|
||||
"republish": "重新发布",
|
||||
"offline": "下线",
|
||||
"hide": "隐藏",
|
||||
"more": "更多",
|
||||
"submitDeleteApproval": "提交删除审批",
|
||||
|
||||
@@ -38,10 +38,18 @@
|
||||
"publishStatusDraft": "草稿",
|
||||
"publishStatusPublishPending": "发布审批中",
|
||||
"publishStatusPublished": "已发布",
|
||||
"publishStatusOfflinePending": "下线审批中",
|
||||
"publishStatusOffline": "已下线",
|
||||
"publishStatusDeletePending": "删除审批中",
|
||||
"publishStatusLabel": "发布状态",
|
||||
"submitPublishApprovalConfirm": "提交后会进入发布审批,审批通过后该知识库才可作为正式版本被聊天助手引用。",
|
||||
"submitDeleteApprovalConfirm": "提交后会进入删除审批,审批通过后将执行真实删除。",
|
||||
"submitPublishApprovalConfirm": "确认发布当前知识库吗?",
|
||||
"submitRepublishApprovalConfirm": "确认重新发布当前知识库吗?",
|
||||
"submitOfflineApprovalConfirm": "确认下线当前知识库吗?",
|
||||
"submitDeleteApprovalConfirm": "确认删除当前知识库吗?",
|
||||
"offlineImpactBoundBotsIntro": "当前知识库被以下聊天助手绑定:",
|
||||
"offlineImpactBoundBotsFooter": "下线成功后,系统会自动从这些聊天助手中解绑该知识库。",
|
||||
"offlineImpactWorkflowBlockedIntro": "当前知识库仍被以下工作流使用:",
|
||||
"offlineImpactWorkflowBlockedFooter": "请先在工作流中调整相关知识库节点后再下线。",
|
||||
"publishPendingHint": "当前知识库已有进行中的审批,请等待处理完成。",
|
||||
"deletePendingHint": "当前知识库已有进行中的审批,请等待处理完成。",
|
||||
"createdModifyTime": "创建/更新时间",
|
||||
|
||||
@@ -243,6 +243,10 @@ function setupAccessGuard(router: Router) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 页面菜单与按钮权限码是两套数据源。每次重新构建动态菜单时,
|
||||
// 同步刷新一次 accessCodes,避免后端权限模型调整后页面仍持有旧按钮权限。
|
||||
await authStore.fetchAccessCodes();
|
||||
|
||||
// 生成路由表
|
||||
// 当前登录用户拥有的角色标识列表
|
||||
const userRoles = userInfo.roles ?? [];
|
||||
|
||||
@@ -3,6 +3,74 @@ import type { RouteRecordRaw } from 'vue-router';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
const routes: RouteRecordRaw[] = [
|
||||
{
|
||||
name: 'ApprovalFlowPage',
|
||||
path: '/sys/approval/flow',
|
||||
redirect: {
|
||||
path: '/sys/approval',
|
||||
query: {
|
||||
tab: 'flow',
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
title: $t('approval.tab.flow'),
|
||||
hideInMenu: true,
|
||||
hideInBreadcrumb: true,
|
||||
hideInTab: true,
|
||||
activePath: '/sys/approval',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ApprovalPendingPage',
|
||||
path: '/sys/approval/pending',
|
||||
redirect: {
|
||||
path: '/sys/approval',
|
||||
query: {
|
||||
tab: 'pending',
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
title: $t('approval.tab.pending'),
|
||||
hideInMenu: true,
|
||||
hideInBreadcrumb: true,
|
||||
hideInTab: true,
|
||||
activePath: '/sys/approval',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ApprovalProcessedPage',
|
||||
path: '/sys/approval/processed',
|
||||
redirect: {
|
||||
path: '/sys/approval',
|
||||
query: {
|
||||
tab: 'processed',
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
title: $t('approval.tab.processed'),
|
||||
hideInMenu: true,
|
||||
hideInBreadcrumb: true,
|
||||
hideInTab: true,
|
||||
activePath: '/sys/approval',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ApprovalInitiatedPage',
|
||||
path: '/sys/approval/initiated',
|
||||
redirect: {
|
||||
path: '/sys/approval',
|
||||
query: {
|
||||
tab: 'initiated',
|
||||
},
|
||||
},
|
||||
meta: {
|
||||
title: $t('approval.tab.initiated'),
|
||||
hideInMenu: true,
|
||||
hideInBreadcrumb: true,
|
||||
hideInTab: true,
|
||||
activePath: '/sys/approval',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'ApprovalDetail',
|
||||
path: '/sys/approval/detail/:id',
|
||||
|
||||
@@ -154,6 +154,12 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
return userInfo;
|
||||
}
|
||||
|
||||
async function fetchAccessCodes() {
|
||||
const accessCodes = await getAccessCodesApi();
|
||||
accessStore.setAccessCodes(accessCodes);
|
||||
return accessCodes;
|
||||
}
|
||||
|
||||
function $reset() {
|
||||
loginLoading.value = false;
|
||||
}
|
||||
@@ -162,6 +168,7 @@ export const useAuthStore = defineStore('auth', () => {
|
||||
$reset,
|
||||
authDevLogin,
|
||||
authLogin,
|
||||
fetchAccessCodes,
|
||||
fetchUserInfo,
|
||||
loginLoading,
|
||||
logout,
|
||||
|
||||
@@ -26,7 +26,11 @@ import {
|
||||
} from 'element-plus';
|
||||
import { tryit } from 'radash';
|
||||
|
||||
import { submitBotDeleteApproval, submitBotPublishApproval } from '#/api';
|
||||
import {
|
||||
submitBotDeleteApproval,
|
||||
submitBotOfflineApproval,
|
||||
submitBotPublishApproval,
|
||||
} from '#/api';
|
||||
import { api } from '#/api/request';
|
||||
import defaultAvatar from '#/assets/ai/bot/defaultBotAvatar.png';
|
||||
import HeaderSearch from '#/components/headerSearch/HeaderSearch.vue';
|
||||
@@ -34,9 +38,12 @@ import CardList from '#/components/page/CardList.vue';
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
import PageSide from '#/components/page/PageSide.vue';
|
||||
import {
|
||||
canAiResourceDelete,
|
||||
canAiResourceOffline,
|
||||
canAiResourcePublish,
|
||||
canAiResourceRepublish,
|
||||
isAiResourceApprovalPending,
|
||||
isAiResourcePublished,
|
||||
normalizeAiPublishStatus,
|
||||
resolveAiResourceDisplayStatus,
|
||||
} from '#/views/ai/shared/publish-status';
|
||||
import { useDictStore } from '#/store';
|
||||
|
||||
@@ -105,35 +112,57 @@ const actions: ActionButton[] = [
|
||||
{
|
||||
icon: Promotion,
|
||||
text: (row: BotInfo) =>
|
||||
isAiResourcePublished(row.publishStatus)
|
||||
canAiResourceRepublish(row.displayPublishStatus, row.publishStatus)
|
||||
? $t('button.republish')
|
||||
: $t('button.submitPublishApproval'),
|
||||
: $t('button.publish'),
|
||||
permission: '/api/v1/bot/save',
|
||||
placement: 'inline',
|
||||
visible: (row: BotInfo) =>
|
||||
canAiResourcePublish(row.displayPublishStatus, row.publishStatus) ||
|
||||
canAiResourceRepublish(row.displayPublishStatus, row.publishStatus),
|
||||
onClick(row: BotInfo) {
|
||||
handleSubmitPublishApproval(row);
|
||||
handlePublishAction(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: Promotion,
|
||||
text: $t('button.offline'),
|
||||
permission: '/api/v1/bot/save',
|
||||
placement: 'menu',
|
||||
visible: (row: BotInfo) => canAiResourceOffline(row.displayPublishStatus, row.publishStatus),
|
||||
onClick(row: BotInfo) {
|
||||
handleOfflineAction(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: Delete,
|
||||
text: $t('button.submitDeleteApproval'),
|
||||
text: $t('button.delete'),
|
||||
tone: 'danger',
|
||||
permission: '/api/v1/bot/remove',
|
||||
placement: 'menu',
|
||||
visible: (row: BotInfo) => canAiResourceDelete(row.displayPublishStatus, row.publishStatus),
|
||||
onClick(row: BotInfo) {
|
||||
handleSubmitDeleteApproval(row);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const handleSubmitPublishApproval = async (bot: BotInfo) => {
|
||||
if (isAiResourceApprovalPending(bot.publishStatus)) {
|
||||
function isRepublishAction(bot: BotInfo) {
|
||||
return canAiResourceRepublish(bot.displayPublishStatus, bot.publishStatus);
|
||||
}
|
||||
|
||||
const handlePublishAction = async (bot: BotInfo) => {
|
||||
if (
|
||||
isAiResourceApprovalPending(bot.displayPublishStatus, bot.publishStatus)
|
||||
) {
|
||||
ElMessage.warning($t('bot.publishPendingHint'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
$t('bot.submitPublishApprovalConfirm'),
|
||||
isRepublishAction(bot)
|
||||
? $t('bot.submitRepublishApprovalConfirm')
|
||||
: $t('bot.submitPublishApprovalConfirm'),
|
||||
$t('message.noticeTitle'),
|
||||
{
|
||||
confirmButtonText: $t('button.confirm'),
|
||||
@@ -150,8 +179,36 @@ const handleSubmitPublishApproval = async (bot: BotInfo) => {
|
||||
pageDataRef.value?.reload?.();
|
||||
}
|
||||
};
|
||||
const handleOfflineAction = async (bot: BotInfo) => {
|
||||
if (
|
||||
isAiResourceApprovalPending(bot.displayPublishStatus, bot.publishStatus)
|
||||
) {
|
||||
ElMessage.warning($t('bot.publishPendingHint'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
$t('bot.submitOfflineApprovalConfirm'),
|
||||
$t('message.noticeTitle'),
|
||||
{
|
||||
confirmButtonText: $t('button.confirm'),
|
||||
cancelButtonText: $t('button.cancel'),
|
||||
type: 'warning',
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const res = await submitBotOfflineApproval(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)) {
|
||||
if (
|
||||
isAiResourceApprovalPending(bot.displayPublishStatus, bot.publishStatus)
|
||||
) {
|
||||
ElMessage.warning($t('bot.deletePendingHint'));
|
||||
return;
|
||||
}
|
||||
@@ -174,8 +231,11 @@ const handleSubmitDeleteApproval = async (bot: BotInfo) => {
|
||||
pageDataRef.value?.reload?.();
|
||||
}
|
||||
};
|
||||
function resolvePublishStatusMeta(status?: string) {
|
||||
switch (normalizeAiPublishStatus(status)) {
|
||||
function resolvePublishStatusMetaByInstance(
|
||||
displayPublishStatus?: string,
|
||||
publishStatus?: string,
|
||||
) {
|
||||
switch (resolveAiResourceDisplayStatus(displayPublishStatus, publishStatus)) {
|
||||
case 'PUBLISHED':
|
||||
return {
|
||||
label: $t('bot.publishStatusPublished'),
|
||||
@@ -186,6 +246,16 @@ function resolvePublishStatusMeta(status?: string) {
|
||||
label: $t('bot.publishStatusPublishPending'),
|
||||
type: 'warning' as const,
|
||||
};
|
||||
case 'OFFLINE_PENDING':
|
||||
return {
|
||||
label: $t('bot.publishStatusOfflinePending'),
|
||||
type: 'warning' as const,
|
||||
};
|
||||
case 'OFFLINE':
|
||||
return {
|
||||
label: $t('bot.publishStatusOffline'),
|
||||
type: 'info' as const,
|
||||
};
|
||||
case 'DELETE_PENDING':
|
||||
return {
|
||||
label: $t('bot.publishStatusDeletePending'),
|
||||
@@ -378,9 +448,9 @@ const getSideList = async () => {
|
||||
size="small"
|
||||
effect="plain"
|
||||
round
|
||||
:type="resolvePublishStatusMeta(item.publishStatus).type"
|
||||
:type="resolvePublishStatusMetaByInstance(item.displayPublishStatus, item.publishStatus).type"
|
||||
>
|
||||
{{ resolvePublishStatusMeta(item.publishStatus).label }}
|
||||
{{ resolvePublishStatusMetaByInstance(item.displayPublishStatus, item.publishStatus).label }}
|
||||
</ElTag>
|
||||
</template>
|
||||
</CardList>
|
||||
|
||||
@@ -42,6 +42,7 @@ import { tryit } from 'radash';
|
||||
import {
|
||||
getPerQuestions,
|
||||
submitBotDeleteApproval,
|
||||
submitBotOfflineApproval,
|
||||
submitBotPublishApproval,
|
||||
updateBotApi,
|
||||
updateBotOptions,
|
||||
@@ -56,10 +57,13 @@ import CommonSelectDataModal from '#/components/commonSelectModal/CommonSelectDa
|
||||
import DictSelect from '#/components/dict/DictSelect.vue';
|
||||
import UploadAvatar from '#/components/upload/UploadAvatar.vue';
|
||||
import {
|
||||
canAiResourceDelete,
|
||||
canAiResourceOffline,
|
||||
canAiResourcePublish,
|
||||
canAiResourceRepublish,
|
||||
isAiResourceApprovalPending,
|
||||
isAiResourceExternallyVisible,
|
||||
isAiResourcePublished,
|
||||
normalizeAiPublishStatus,
|
||||
resolveAiResourceDisplayStatus,
|
||||
} from '#/views/ai/shared/publish-status';
|
||||
|
||||
interface SelectedMcpTool {
|
||||
@@ -163,18 +167,42 @@ const publicChatUrl = computed(() => {
|
||||
const publicChatEmbedUrl = computed(() => {
|
||||
return buildPublicChatUrl(true);
|
||||
});
|
||||
const botDisplayPublishStatus = computed(() =>
|
||||
resolveAiResourceDisplayStatus(
|
||||
botInfo.value?.displayPublishStatus,
|
||||
botInfo.value?.publishStatus,
|
||||
),
|
||||
);
|
||||
const botApprovalPending = computed(() =>
|
||||
isAiResourceApprovalPending(
|
||||
botInfo.value?.displayPublishStatus,
|
||||
botInfo.value?.publishStatus,
|
||||
),
|
||||
);
|
||||
const publishStatusMeta = computed<{
|
||||
description: string;
|
||||
label: string;
|
||||
type: 'danger' | 'info' | 'success' | 'warning';
|
||||
}>(() => {
|
||||
switch (normalizeAiPublishStatus(botInfo.value?.publishStatus)) {
|
||||
switch (botDisplayPublishStatus.value) {
|
||||
case 'PUBLISHED':
|
||||
return {
|
||||
label: $t('bot.publishStatusPublished'),
|
||||
type: 'success',
|
||||
description: $t('bot.publishStatusPublishedDesc'),
|
||||
};
|
||||
case 'OFFLINE_PENDING':
|
||||
return {
|
||||
label: $t('bot.publishStatusOfflinePending'),
|
||||
type: 'warning',
|
||||
description: $t('bot.publishStatusOfflinePendingDesc'),
|
||||
};
|
||||
case 'OFFLINE':
|
||||
return {
|
||||
label: $t('bot.publishStatusOffline'),
|
||||
type: 'info',
|
||||
description: $t('bot.publishStatusOfflineDesc'),
|
||||
};
|
||||
case 'PUBLISH_PENDING':
|
||||
return {
|
||||
label: $t('bot.publishStatusPublishPending'),
|
||||
@@ -199,9 +227,44 @@ const canUsePublicAccess = computed(() =>
|
||||
isAiResourceExternallyVisible(botInfo.value?.publishStatus),
|
||||
);
|
||||
const publishPrimaryActionLabel = computed(() =>
|
||||
isAiResourcePublished(botInfo.value?.publishStatus)
|
||||
canAiResourceRepublish(
|
||||
botInfo.value?.displayPublishStatus,
|
||||
botInfo.value?.publishStatus,
|
||||
)
|
||||
? $t('button.republish')
|
||||
: $t('button.submitPublishApproval'),
|
||||
: $t('button.publish'),
|
||||
);
|
||||
const canShowPublishPrimaryAction = computed(() =>
|
||||
canAiResourcePublish(
|
||||
botInfo.value?.displayPublishStatus,
|
||||
botInfo.value?.publishStatus,
|
||||
) ||
|
||||
canAiResourceRepublish(
|
||||
botInfo.value?.displayPublishStatus,
|
||||
botInfo.value?.publishStatus,
|
||||
),
|
||||
);
|
||||
const secondaryActionLabel = computed(() => {
|
||||
if (
|
||||
canAiResourceOffline(
|
||||
botInfo.value?.displayPublishStatus,
|
||||
botInfo.value?.publishStatus,
|
||||
)
|
||||
) {
|
||||
return $t('button.offline');
|
||||
}
|
||||
if (
|
||||
canAiResourceDelete(
|
||||
botInfo.value?.displayPublishStatus,
|
||||
botInfo.value?.publishStatus,
|
||||
)
|
||||
) {
|
||||
return $t('button.delete');
|
||||
}
|
||||
return '';
|
||||
});
|
||||
const canShowSecondaryAction = computed(() =>
|
||||
Boolean(secondaryActionLabel.value),
|
||||
);
|
||||
const iframeCode = computed(() => {
|
||||
if (!publicChatEmbedUrl.value) {
|
||||
@@ -820,17 +883,24 @@ const handleDeletePresetQuestion = (item: any) => {
|
||||
const handlePublishWx = () => {
|
||||
publishWxRef.value.openDialog(botId.value, botInfo.value?.options || {});
|
||||
};
|
||||
const handleSubmitPublishApproval = async () => {
|
||||
const handleLifecycleAction = async () => {
|
||||
if (!botInfo.value) {
|
||||
return;
|
||||
}
|
||||
if (isAiResourceApprovalPending(botInfo.value.publishStatus)) {
|
||||
if (
|
||||
botApprovalPending.value
|
||||
) {
|
||||
ElMessage.warning($t('bot.publishPendingHint'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
$t('bot.submitPublishApprovalConfirm'),
|
||||
canAiResourceRepublish(
|
||||
botInfo.value.displayPublishStatus,
|
||||
botInfo.value.publishStatus,
|
||||
)
|
||||
? $t('bot.submitRepublishApprovalConfirm')
|
||||
: $t('bot.submitPublishApprovalConfirm'),
|
||||
$t('message.noticeTitle'),
|
||||
{
|
||||
confirmButtonText: $t('button.confirm'),
|
||||
@@ -849,17 +919,32 @@ const handleSubmitPublishApproval = async () => {
|
||||
ElMessage.error(res.message || $t('message.saveFailMessage'));
|
||||
}
|
||||
};
|
||||
const handleSubmitDeleteApproval = async () => {
|
||||
const handleSecondaryAction = async () => {
|
||||
if (!botInfo.value) {
|
||||
return;
|
||||
}
|
||||
if (isAiResourceApprovalPending(botInfo.value.publishStatus)) {
|
||||
ElMessage.warning($t('bot.deletePendingHint'));
|
||||
if (
|
||||
botApprovalPending.value
|
||||
) {
|
||||
ElMessage.warning($t('bot.publishPendingHint'));
|
||||
return;
|
||||
}
|
||||
const canOffline = canAiResourceOffline(
|
||||
botInfo.value.displayPublishStatus,
|
||||
botInfo.value.publishStatus,
|
||||
);
|
||||
const canDelete = canAiResourceDelete(
|
||||
botInfo.value.displayPublishStatus,
|
||||
botInfo.value.publishStatus,
|
||||
);
|
||||
if (!canOffline && !canDelete) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
$t('bot.submitDeleteApprovalConfirm'),
|
||||
canOffline
|
||||
? $t('bot.submitOfflineApprovalConfirm')
|
||||
: $t('bot.submitDeleteApprovalConfirm'),
|
||||
$t('message.noticeTitle'),
|
||||
{
|
||||
confirmButtonText: $t('button.confirm'),
|
||||
@@ -870,7 +955,9 @@ const handleSubmitDeleteApproval = async () => {
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const res = await submitBotDeleteApproval(String(botInfo.value.id));
|
||||
const res = canOffline
|
||||
? await submitBotOfflineApproval(String(botInfo.value.id))
|
||||
: await submitBotDeleteApproval(String(botInfo.value.id));
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message || $t('message.saveOkMessage'));
|
||||
getBotDetail();
|
||||
@@ -1485,19 +1572,21 @@ const handleBasicInfoChange = async (
|
||||
</div>
|
||||
<div class="publish-summary-actions">
|
||||
<ElButton
|
||||
v-if="canShowPublishPrimaryAction"
|
||||
type="primary"
|
||||
:disabled="!hasSavePermission"
|
||||
@click="handleSubmitPublishApproval"
|
||||
@click="handleLifecycleAction"
|
||||
>
|
||||
{{ publishPrimaryActionLabel }}
|
||||
</ElButton>
|
||||
<ElButton
|
||||
v-if="canShowSecondaryAction"
|
||||
plain
|
||||
type="danger"
|
||||
:type="secondaryActionLabel === $t('button.delete') ? 'danger' : 'default'"
|
||||
:disabled="!hasSavePermission"
|
||||
@click="handleSubmitDeleteApproval"
|
||||
@click="handleSecondaryAction"
|
||||
>
|
||||
{{ $t('button.submitDeleteApproval') }}
|
||||
{{ secondaryActionLabel }}
|
||||
</ElButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -46,9 +46,16 @@ import PageSide from '#/components/page/PageSide.vue';
|
||||
import DocumentCollectionModal from '#/views/ai/documentCollection/DocumentCollectionModal.vue';
|
||||
import AiResourceCornerMeta from '#/views/ai/shared/AiResourceCornerMeta.vue';
|
||||
import {
|
||||
buildOfflineImpactMessage,
|
||||
type OfflineImpactCheck,
|
||||
} from '#/views/ai/shared/offline-impact';
|
||||
import {
|
||||
canAiResourceDelete,
|
||||
canAiResourceOffline,
|
||||
canAiResourcePublish,
|
||||
canAiResourceRepublish,
|
||||
isAiResourceApprovalPending,
|
||||
isAiResourcePublished,
|
||||
normalizeAiPublishStatus,
|
||||
resolveAiResourceDisplayStatus,
|
||||
} from '#/views/ai/shared/publish-status';
|
||||
|
||||
const router = useRouter();
|
||||
@@ -186,24 +193,41 @@ const actions: ActionButton[] = [
|
||||
{
|
||||
icon: Promotion,
|
||||
text: (row) =>
|
||||
isAiResourcePublished(row.publishStatus)
|
||||
canAiResourceRepublish(row.displayPublishStatus, row.publishStatus)
|
||||
? $t('button.republish')
|
||||
: $t('button.submitPublishApproval'),
|
||||
: $t('button.publish'),
|
||||
permission: '/api/v1/documentCollection/save',
|
||||
placement: 'inline',
|
||||
visible: (row) =>
|
||||
canAiResourcePublish(row.displayPublishStatus, row.publishStatus) ||
|
||||
canAiResourceRepublish(row.displayPublishStatus, row.publishStatus),
|
||||
onClick(row) {
|
||||
if (!ensureManageKnowledgeItem(row)) {
|
||||
return;
|
||||
}
|
||||
submitPublishApproval(row);
|
||||
submitPublishAction(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
text: $t('button.submitDeleteApproval'),
|
||||
icon: Promotion,
|
||||
text: $t('button.offline'),
|
||||
permission: '/api/v1/documentCollection/save',
|
||||
placement: 'menu',
|
||||
visible: (row) => canAiResourceOffline(row.displayPublishStatus, row.publishStatus),
|
||||
onClick(row) {
|
||||
if (!ensureManageKnowledgeItem(row)) {
|
||||
return;
|
||||
}
|
||||
submitOfflineAction(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
text: $t('button.delete'),
|
||||
icon: Delete,
|
||||
tone: 'danger',
|
||||
permission: '/api/v1/documentCollection/remove',
|
||||
placement: 'menu',
|
||||
visible: (row) => canAiResourceDelete(row.displayPublishStatus, row.publishStatus),
|
||||
onClick(row) {
|
||||
if (!ensureManageKnowledgeItem(row)) {
|
||||
return;
|
||||
@@ -216,14 +240,22 @@ const actions: ActionButton[] = [
|
||||
onMounted(() => {
|
||||
getCategoryList();
|
||||
});
|
||||
const submitPublishApproval = async (item: any) => {
|
||||
if (isAiResourceApprovalPending(item.publishStatus)) {
|
||||
function isRepublishAction(item: any) {
|
||||
return canAiResourceRepublish(item.displayPublishStatus, item.publishStatus);
|
||||
}
|
||||
|
||||
const submitPublishAction = async (item: any) => {
|
||||
if (
|
||||
isAiResourceApprovalPending(item.displayPublishStatus, item.publishStatus)
|
||||
) {
|
||||
ElMessage.warning($t('documentCollection.publishPendingHint'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
$t('documentCollection.submitPublishApprovalConfirm'),
|
||||
isRepublishAction(item)
|
||||
? $t('documentCollection.submitRepublishApprovalConfirm')
|
||||
: $t('documentCollection.submitPublishApprovalConfirm'),
|
||||
$t('message.noticeTitle'),
|
||||
{
|
||||
confirmButtonText: $t('button.confirm'),
|
||||
@@ -245,8 +277,71 @@ const submitPublishApproval = async (item: any) => {
|
||||
reloadKnowledgeList();
|
||||
}
|
||||
};
|
||||
const submitOfflineAction = async (item: any) => {
|
||||
if (
|
||||
isAiResourceApprovalPending(item.displayPublishStatus, item.publishStatus)
|
||||
) {
|
||||
ElMessage.warning($t('documentCollection.publishPendingHint'));
|
||||
return;
|
||||
}
|
||||
const impactRes = await api.get<{
|
||||
data: OfflineImpactCheck;
|
||||
errorCode: number;
|
||||
}>(
|
||||
'/api/v1/documentCollection/offlineImpactCheck',
|
||||
{
|
||||
params: { id: item.id },
|
||||
},
|
||||
);
|
||||
if (impactRes.errorCode !== 0) {
|
||||
return;
|
||||
}
|
||||
if (impactRes.data?.hasWorkflowUsages) {
|
||||
await ElMessageBox.alert(
|
||||
buildOfflineImpactMessage(
|
||||
$t('documentCollection.offlineImpactWorkflowBlockedIntro'),
|
||||
impactRes.data.workflowUsages,
|
||||
$t('documentCollection.offlineImpactWorkflowBlockedFooter'),
|
||||
),
|
||||
$t('message.noticeTitle'),
|
||||
{
|
||||
confirmButtonText: $t('button.confirm'),
|
||||
type: 'warning',
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
impactRes.data?.hasBotBindings
|
||||
? buildOfflineImpactMessage(
|
||||
$t('documentCollection.offlineImpactBoundBotsIntro'),
|
||||
impactRes.data.botBindings,
|
||||
$t('documentCollection.offlineImpactBoundBotsFooter'),
|
||||
)
|
||||
: $t('documentCollection.submitOfflineApprovalConfirm'),
|
||||
$t('message.noticeTitle'),
|
||||
{
|
||||
confirmButtonText: $t('button.confirm'),
|
||||
cancelButtonText: $t('button.cancel'),
|
||||
type: 'warning',
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const res = await api.post('/api/v1/documentCollection/submitOfflineApproval', {
|
||||
id: item.id,
|
||||
});
|
||||
if (res.errorCode === 0) {
|
||||
ElMessage.success(res.message || $t('message.saveOkMessage'));
|
||||
reloadKnowledgeList();
|
||||
}
|
||||
};
|
||||
const submitDeleteApproval = async (item: any) => {
|
||||
if (isAiResourceApprovalPending(item.publishStatus)) {
|
||||
if (
|
||||
isAiResourceApprovalPending(item.displayPublishStatus, item.publishStatus)
|
||||
) {
|
||||
ElMessage.warning($t('documentCollection.deletePendingHint'));
|
||||
return;
|
||||
}
|
||||
@@ -274,14 +369,29 @@ const submitDeleteApproval = async (item: any) => {
|
||||
reloadKnowledgeList();
|
||||
}
|
||||
};
|
||||
function resolvePublishStatusMeta(status?: string) {
|
||||
switch (normalizeAiPublishStatus(status)) {
|
||||
function resolvePublishStatusMeta(
|
||||
displayPublishStatus?: string,
|
||||
publishStatus?: string,
|
||||
) {
|
||||
switch (resolveAiResourceDisplayStatus(displayPublishStatus, publishStatus)) {
|
||||
case 'DELETE_PENDING': {
|
||||
return {
|
||||
label: $t('documentCollection.publishStatusDeletePending'),
|
||||
tone: 'danger',
|
||||
};
|
||||
}
|
||||
case 'OFFLINE_PENDING': {
|
||||
return {
|
||||
label: $t('documentCollection.publishStatusOfflinePending'),
|
||||
tone: 'pending',
|
||||
};
|
||||
}
|
||||
case 'OFFLINE': {
|
||||
return {
|
||||
label: $t('documentCollection.publishStatusOffline'),
|
||||
tone: 'draft',
|
||||
};
|
||||
}
|
||||
case 'PUBLISH_PENDING': {
|
||||
return {
|
||||
label: $t('documentCollection.publishStatusPublishPending'),
|
||||
@@ -553,11 +663,11 @@ function changeCategory(category: any) {
|
||||
<template #publish>
|
||||
<div
|
||||
class="knowledge-publish-chip"
|
||||
:class="`knowledge-publish-chip--${resolvePublishStatusMeta(item.publishStatus).tone}`"
|
||||
:class="`knowledge-publish-chip--${resolvePublishStatusMeta(item.displayPublishStatus, item.publishStatus).tone}`"
|
||||
>
|
||||
<span class="knowledge-publish-chip__dot"></span>
|
||||
<span>{{
|
||||
resolvePublishStatusMeta(item.publishStatus).label
|
||||
resolvePublishStatusMeta(item.displayPublishStatus, item.publishStatus).label
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,22 +1,87 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import { EasyFlowPanelModal } from '@easyflow/common-ui';
|
||||
|
||||
import { ElImage } from 'element-plus';
|
||||
import { ElButton, ElEmpty, ElImage, ElScrollbar } from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import { $t } from '#/locales';
|
||||
|
||||
defineExpose({
|
||||
openDialog,
|
||||
});
|
||||
const dialogVisible = ref(false);
|
||||
const data = ref<any>();
|
||||
function openDialog(row: any) {
|
||||
const docPreviewLoading = ref(false);
|
||||
const docPreviewContent = ref('');
|
||||
const docPreviewTruncated = ref(false);
|
||||
const docPreviewError = ref('');
|
||||
let previewRequestId = 0;
|
||||
|
||||
const isDocument = computed(() => data.value?.resourceType === 3);
|
||||
const fileName = computed(() => {
|
||||
const resourceName = data.value?.resourceName || '';
|
||||
const suffix = data.value?.suffix || '';
|
||||
return suffix ? `${resourceName}.${suffix}` : resourceName;
|
||||
});
|
||||
const previewWidth = computed(() => (isDocument.value ? 'xl' : 'md'));
|
||||
|
||||
async function openDialog(row: any) {
|
||||
data.value = row;
|
||||
dialogVisible.value = true;
|
||||
resetDocumentPreview();
|
||||
if (row?.resourceType === 3) {
|
||||
await loadDocumentPreview(row);
|
||||
}
|
||||
}
|
||||
function closeDialog() {
|
||||
dialogVisible.value = false;
|
||||
}
|
||||
|
||||
function resetDocumentPreview() {
|
||||
docPreviewLoading.value = false;
|
||||
docPreviewContent.value = '';
|
||||
docPreviewTruncated.value = false;
|
||||
docPreviewError.value = '';
|
||||
}
|
||||
|
||||
async function loadDocumentPreview(row: any) {
|
||||
if (!row?.id) {
|
||||
docPreviewError.value = '当前素材缺少预览标识,请下载后查看';
|
||||
return;
|
||||
}
|
||||
const currentRequestId = ++previewRequestId;
|
||||
docPreviewLoading.value = true;
|
||||
try {
|
||||
const res = await api.get('/api/v1/resource/previewContent', {
|
||||
params: { id: row.id },
|
||||
});
|
||||
if (currentRequestId !== previewRequestId) {
|
||||
return;
|
||||
}
|
||||
docPreviewContent.value = res.data?.content || '';
|
||||
docPreviewTruncated.value = !!res.data?.truncated;
|
||||
if (!docPreviewContent.value) {
|
||||
docPreviewError.value = '暂未提取到可预览内容,请下载后查看';
|
||||
}
|
||||
} catch {
|
||||
if (currentRequestId !== previewRequestId) {
|
||||
return;
|
||||
}
|
||||
docPreviewError.value = '文档预览加载失败,请下载后查看';
|
||||
} finally {
|
||||
if (currentRequestId === previewRequestId) {
|
||||
docPreviewLoading.value = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function openSourceFile() {
|
||||
if (data.value?.resourceUrl) {
|
||||
window.open(data.value.resourceUrl, '_blank');
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -24,25 +89,119 @@ function closeDialog() {
|
||||
v-model:open="dialogVisible"
|
||||
:title="$t('message.preview')"
|
||||
:before-close="closeDialog"
|
||||
width="md"
|
||||
:width="previewWidth"
|
||||
:show-footer="false"
|
||||
>
|
||||
<div class="flex justify-center">
|
||||
<div class="resource-preview flex justify-center">
|
||||
<ElImage
|
||||
v-if="data.resourceType === 0"
|
||||
style="width: 200px"
|
||||
:preview-src-list="[data.resourceUrl]"
|
||||
:src="data.resourceUrl"
|
||||
/>
|
||||
<video v-if="data.resourceType === 1" controls width="640" height="360">
|
||||
<video
|
||||
v-else-if="data.resourceType === 1"
|
||||
controls
|
||||
width="640"
|
||||
height="360"
|
||||
>
|
||||
<source :src="data.resourceUrl" type="video/mp4" />
|
||||
{{ $t('message.notVideo') }}
|
||||
</video>
|
||||
<audio v-if="data.resourceType === 2" controls :src="data.resourceUrl">
|
||||
<audio
|
||||
v-else-if="data.resourceType === 2"
|
||||
controls
|
||||
class="mt-8 w-full max-w-[640px]"
|
||||
:src="data.resourceUrl"
|
||||
>
|
||||
{{ $t('message.notAudio') }}
|
||||
</audio>
|
||||
<div
|
||||
v-else-if="isDocument"
|
||||
v-loading="docPreviewLoading"
|
||||
:element-loading-text="$t('message.loading')"
|
||||
class="resource-preview__document bg-background border-border w-full rounded-xl border"
|
||||
>
|
||||
<div
|
||||
class="resource-preview__toolbar border-border flex items-center justify-between gap-3 border-b px-5 py-4"
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<div class="truncate text-sm font-medium">{{ fileName }}</div>
|
||||
<div
|
||||
v-if="docPreviewTruncated"
|
||||
class="text-muted-foreground mt-1 text-xs"
|
||||
>
|
||||
内容较长,当前仅展示前 20000 个字符
|
||||
</div>
|
||||
</div>
|
||||
<ElButton link type="primary" @click="openSourceFile">
|
||||
{{ $t('button.download') }}
|
||||
</ElButton>
|
||||
</div>
|
||||
<div class="resource-preview__body">
|
||||
<ElEmpty
|
||||
v-if="docPreviewError"
|
||||
:description="docPreviewError"
|
||||
class="resource-preview__empty"
|
||||
>
|
||||
<ElButton link type="primary" @click="openSourceFile">
|
||||
{{ $t('button.download') }}
|
||||
</ElButton>
|
||||
</ElEmpty>
|
||||
<ElScrollbar
|
||||
v-else
|
||||
class="resource-preview__scrollbar"
|
||||
wrap-class="resource-preview__scrollbar-wrap"
|
||||
>
|
||||
<pre class="resource-preview__content">{{
|
||||
docPreviewContent
|
||||
}}</pre>
|
||||
</ElScrollbar>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</EasyFlowPanelModal>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.resource-preview {
|
||||
min-height: 220px;
|
||||
}
|
||||
|
||||
.resource-preview__document {
|
||||
min-height: 540px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.resource-preview__toolbar {
|
||||
min-height: 72px;
|
||||
}
|
||||
|
||||
.resource-preview__body {
|
||||
height: 468px;
|
||||
}
|
||||
|
||||
.resource-preview__scrollbar {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
:deep(.resource-preview__scrollbar-wrap) {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.resource-preview__content {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
font-family:
|
||||
'SFMono-Regular', 'JetBrains Mono', 'Fira Code', Consolas, 'Liberation Mono',
|
||||
monospace;
|
||||
font-size: 13px;
|
||||
line-height: 1.75;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.resource-preview__empty {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
38
easyflow-ui-admin/app/src/views/ai/shared/offline-impact.ts
Normal file
38
easyflow-ui-admin/app/src/views/ai/shared/offline-impact.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { h } from 'vue';
|
||||
|
||||
export interface OfflineImpactBinding {
|
||||
id?: number | string;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface OfflineImpactCheck {
|
||||
canProceed: boolean;
|
||||
hasBotBindings: boolean;
|
||||
hasWorkflowUsages: boolean;
|
||||
botBindings: OfflineImpactBinding[];
|
||||
workflowUsages: OfflineImpactBinding[];
|
||||
message?: string;
|
||||
}
|
||||
|
||||
function resolveTitle(item: OfflineImpactBinding) {
|
||||
return item.title || String(item.id || '');
|
||||
}
|
||||
|
||||
export function joinOfflineImpactTitles(items: OfflineImpactBinding[] = []) {
|
||||
return items.map(resolveTitle).filter(Boolean).join('、');
|
||||
}
|
||||
|
||||
export function buildOfflineImpactMessage(
|
||||
intro: string,
|
||||
items: OfflineImpactBinding[] = [],
|
||||
footer?: string,
|
||||
) {
|
||||
return h('div', [
|
||||
h('p', intro),
|
||||
h(
|
||||
'ul',
|
||||
items.map((item) => h('li', { key: String(item.id || item.title || '') }, resolveTitle(item))),
|
||||
),
|
||||
footer ? h('p', footer) : null,
|
||||
]);
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
export type AiPublishStatus =
|
||||
| 'DELETE_PENDING'
|
||||
| 'DRAFT'
|
||||
| 'OFFLINE'
|
||||
| 'OFFLINE_PENDING'
|
||||
| 'PUBLISHED'
|
||||
| 'PUBLISH_PENDING';
|
||||
|
||||
@@ -13,6 +15,8 @@ export function normalizeAiPublishStatus(
|
||||
switch (value) {
|
||||
case 'PUBLISHED':
|
||||
case 'PUBLISH_PENDING':
|
||||
case 'OFFLINE':
|
||||
case 'OFFLINE_PENDING':
|
||||
case 'DELETE_PENDING':
|
||||
return value;
|
||||
default:
|
||||
@@ -20,6 +24,17 @@ export function normalizeAiPublishStatus(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析用于页面展示的发布状态。
|
||||
* 已发布资源若存在当前审批实例,则视为“发布审批中”,用于统一状态文案与动作禁用。
|
||||
*/
|
||||
export function resolveAiResourceDisplayStatus(
|
||||
displayValue?: null | string,
|
||||
_fallbackValue?: null | string,
|
||||
): AiPublishStatus {
|
||||
return normalizeAiPublishStatus(displayValue);
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前资源是否已有正式线上版本。
|
||||
*/
|
||||
@@ -27,12 +42,23 @@ export function isAiResourcePublished(value?: null | string) {
|
||||
return normalizeAiPublishStatus(value) === 'PUBLISHED';
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前资源是否处于已下线状态。
|
||||
*/
|
||||
export function isAiResourceOffline(value?: null | string) {
|
||||
return normalizeAiPublishStatus(value) === 'OFFLINE';
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前资源是否允许对外可见。
|
||||
*/
|
||||
export function isAiResourceExternallyVisible(value?: null | string) {
|
||||
const normalized = normalizeAiPublishStatus(value);
|
||||
return normalized === 'PUBLISHED' || normalized === 'DELETE_PENDING';
|
||||
return (
|
||||
normalized === 'PUBLISHED' ||
|
||||
normalized === 'DELETE_PENDING' ||
|
||||
normalized === 'OFFLINE_PENDING'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -45,7 +71,68 @@ export function isAiResourceSelectableForBot(value?: null | string) {
|
||||
/**
|
||||
* 当前资源是否处于审批处理中。
|
||||
*/
|
||||
export function isAiResourceApprovalPending(value?: null | string) {
|
||||
const normalized = normalizeAiPublishStatus(value);
|
||||
return normalized === 'PUBLISH_PENDING' || normalized === 'DELETE_PENDING';
|
||||
export function isAiResourceApprovalPending(
|
||||
displayValue?: null | string,
|
||||
_fallbackValue?: null | string,
|
||||
) {
|
||||
const normalized = resolveAiResourceDisplayStatus(displayValue);
|
||||
return (
|
||||
normalized === 'PUBLISH_PENDING' ||
|
||||
normalized === 'OFFLINE_PENDING' ||
|
||||
normalized === 'DELETE_PENDING'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前资源是否允许发起发布。
|
||||
*/
|
||||
export function canAiResourcePublish(
|
||||
displayValue?: null | string,
|
||||
_fallbackValue?: null | string,
|
||||
) {
|
||||
if (isAiResourceApprovalPending(displayValue)) {
|
||||
return false;
|
||||
}
|
||||
const normalized = resolveAiResourceDisplayStatus(displayValue);
|
||||
return normalized === 'DRAFT' || normalized === 'OFFLINE';
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前资源是否允许重新发布。
|
||||
*/
|
||||
export function canAiResourceRepublish(
|
||||
displayValue?: null | string,
|
||||
_fallbackValue?: null | string,
|
||||
) {
|
||||
if (isAiResourceApprovalPending(displayValue)) {
|
||||
return false;
|
||||
}
|
||||
return resolveAiResourceDisplayStatus(displayValue) === 'PUBLISHED';
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前资源是否允许发起下线。
|
||||
*/
|
||||
export function canAiResourceOffline(
|
||||
displayValue?: null | string,
|
||||
_fallbackValue?: null | string,
|
||||
) {
|
||||
if (isAiResourceApprovalPending(displayValue)) {
|
||||
return false;
|
||||
}
|
||||
return resolveAiResourceDisplayStatus(displayValue) === 'PUBLISHED';
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前资源是否允许发起删除。
|
||||
*/
|
||||
export function canAiResourceDelete(
|
||||
displayValue?: null | string,
|
||||
_fallbackValue?: null | string,
|
||||
) {
|
||||
if (isAiResourceApprovalPending(displayValue)) {
|
||||
return false;
|
||||
}
|
||||
const normalized = resolveAiResourceDisplayStatus(displayValue);
|
||||
return normalized === 'DRAFT' || normalized === 'OFFLINE';
|
||||
}
|
||||
|
||||
@@ -26,8 +26,9 @@ import { $t } from '#/locales';
|
||||
import { router } from '#/router';
|
||||
import { getIconByValue } from '#/views/ai/model/modelUtils/defaultIcon';
|
||||
import {
|
||||
canAiResourceRepublish,
|
||||
isAiResourceApprovalPending,
|
||||
normalizeAiPublishStatus,
|
||||
resolveAiResourceDisplayStatus,
|
||||
} from '#/views/ai/shared/publish-status';
|
||||
import ExecResult from '#/views/ai/workflow/components/ExecResult.vue';
|
||||
import SingleRun from '#/views/ai/workflow/components/SingleRun.vue';
|
||||
@@ -251,18 +252,26 @@ const updatePluginNode = ref<any>(null);
|
||||
const pageLoading = ref(false);
|
||||
const chainInfo = ref<any>(null);
|
||||
const publishActionText = computed(() => {
|
||||
switch (normalizeAiPublishStatus(workflowInfo.value?.publishStatus)) {
|
||||
switch (
|
||||
resolveAiResourceDisplayStatus(
|
||||
workflowInfo.value?.displayPublishStatus,
|
||||
workflowInfo.value?.publishStatus,
|
||||
)
|
||||
) {
|
||||
case 'DELETE_PENDING': {
|
||||
return $t('aiWorkflow.publishStatusDeletePending');
|
||||
}
|
||||
case 'OFFLINE_PENDING': {
|
||||
return $t('aiWorkflow.publishStatusOfflinePending');
|
||||
}
|
||||
case 'PUBLISH_PENDING': {
|
||||
return $t('aiWorkflow.publishStatusPublishPending');
|
||||
}
|
||||
case 'PUBLISHED': {
|
||||
return `${$t('aiWorkflow.publishStatusPublished')} · ${$t('button.republish')}`;
|
||||
return $t('button.republish');
|
||||
}
|
||||
default: {
|
||||
return `${$t('aiWorkflow.publishStatusDraft')} · ${$t('button.submitPublishApproval')}`;
|
||||
return $t('button.publish');
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -272,7 +281,10 @@ const publishActionDisabled = computed(
|
||||
saveLoading.value ||
|
||||
checkLoading.value ||
|
||||
publishLoading.value ||
|
||||
isAiResourceApprovalPending(workflowInfo.value?.publishStatus),
|
||||
isAiResourceApprovalPending(
|
||||
workflowInfo.value?.displayPublishStatus,
|
||||
workflowInfo.value?.publishStatus,
|
||||
),
|
||||
);
|
||||
|
||||
function syncNavTitle(title: string) {
|
||||
@@ -498,17 +510,27 @@ function closeCheckIssues() {
|
||||
async function handleCheck() {
|
||||
await runCheck('PRE_EXECUTE');
|
||||
}
|
||||
async function handlePublish() {
|
||||
async function handlePublishAction() {
|
||||
if (publishLoading.value) {
|
||||
return;
|
||||
}
|
||||
if (isAiResourceApprovalPending(workflowInfo.value?.publishStatus)) {
|
||||
if (
|
||||
isAiResourceApprovalPending(
|
||||
workflowInfo.value?.displayPublishStatus,
|
||||
workflowInfo.value?.publishStatus,
|
||||
)
|
||||
) {
|
||||
ElMessage.warning($t('aiWorkflow.publishPendingHint'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
$t('aiWorkflow.submitPublishApprovalConfirm'),
|
||||
canAiResourceRepublish(
|
||||
workflowInfo.value?.displayPublishStatus,
|
||||
workflowInfo.value?.publishStatus,
|
||||
)
|
||||
? $t('aiWorkflow.submitRepublishApprovalConfirm')
|
||||
: $t('aiWorkflow.submitPublishApprovalConfirm'),
|
||||
$t('message.noticeTitle'),
|
||||
{
|
||||
confirmButtonText: $t('button.confirm'),
|
||||
@@ -680,8 +702,8 @@ function onAsyncExecute(info: any) {
|
||||
:loading="publishLoading"
|
||||
:disabled="publishActionDisabled"
|
||||
class="workflow-publish-button"
|
||||
:class="`workflow-publish-button--${normalizeAiPublishStatus(workflowInfo?.publishStatus)}`"
|
||||
@click="handlePublish"
|
||||
:class="`workflow-publish-button--${resolveAiResourceDisplayStatus(workflowInfo?.displayPublishStatus, workflowInfo?.publishStatus)}`"
|
||||
@click="handlePublishAction"
|
||||
>
|
||||
{{ publishActionText }}
|
||||
</ElButton>
|
||||
@@ -809,12 +831,24 @@ function onAsyncExecute(info: any) {
|
||||
border-color: hsl(var(--warning) / 24%);
|
||||
}
|
||||
|
||||
:deep(.workflow-publish-button--OFFLINE_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--OFFLINE.el-button) {
|
||||
color: hsl(var(--foreground) / 78%);
|
||||
background: hsl(var(--muted) / 62%);
|
||||
border-color: hsl(var(--foreground) / 14%);
|
||||
}
|
||||
|
||||
:deep(.workflow-publish-button--DELETE_PENDING.el-button) {
|
||||
color: hsl(var(--destructive));
|
||||
background: hsl(var(--destructive) / 16%);
|
||||
|
||||
@@ -50,9 +50,16 @@ import { router } from '#/router';
|
||||
import { useDictStore } from '#/store';
|
||||
import AiResourceCornerMeta from '#/views/ai/shared/AiResourceCornerMeta.vue';
|
||||
import {
|
||||
buildOfflineImpactMessage,
|
||||
type OfflineImpactCheck,
|
||||
} from '#/views/ai/shared/offline-impact';
|
||||
import {
|
||||
canAiResourceDelete,
|
||||
canAiResourceOffline,
|
||||
canAiResourcePublish,
|
||||
canAiResourceRepublish,
|
||||
isAiResourceApprovalPending,
|
||||
isAiResourcePublished,
|
||||
normalizeAiPublishStatus,
|
||||
resolveAiResourceDisplayStatus,
|
||||
} from '#/views/ai/shared/publish-status';
|
||||
|
||||
import WorkflowModal from './WorkflowModal.vue';
|
||||
@@ -175,21 +182,35 @@ const actions: ActionButton[] = [
|
||||
{
|
||||
icon: Promotion,
|
||||
text: (row: any) =>
|
||||
isAiResourcePublished(row.publishStatus)
|
||||
canAiResourceRepublish(row.displayPublishStatus, row.publishStatus)
|
||||
? $t('button.republish')
|
||||
: $t('button.submitPublishApproval'),
|
||||
: $t('button.publish'),
|
||||
permission: '/api/v1/workflow/save',
|
||||
placement: 'inline',
|
||||
visible: (row: any) =>
|
||||
canAiResourcePublish(row.displayPublishStatus, row.publishStatus) ||
|
||||
canAiResourceRepublish(row.displayPublishStatus, row.publishStatus),
|
||||
onClick: (row: any) => {
|
||||
submitPublishApproval(row);
|
||||
submitPublishAction(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: Promotion,
|
||||
text: $t('button.offline'),
|
||||
permission: '/api/v1/workflow/save',
|
||||
placement: 'menu',
|
||||
visible: (row: any) => canAiResourceOffline(row.displayPublishStatus, row.publishStatus),
|
||||
onClick: (row: any) => {
|
||||
submitOfflineAction(row);
|
||||
},
|
||||
},
|
||||
{
|
||||
icon: Delete,
|
||||
text: $t('button.submitDeleteApproval'),
|
||||
text: $t('button.delete'),
|
||||
tone: 'danger',
|
||||
permission: '/api/v1/workflow/remove',
|
||||
placement: 'menu',
|
||||
visible: (row: any) => canAiResourceDelete(row.displayPublishStatus, row.publishStatus),
|
||||
onClick: (row: any) => {
|
||||
submitDeleteApproval(row);
|
||||
},
|
||||
@@ -282,14 +303,22 @@ function showDialog(row: any, importMode = false) {
|
||||
function resolveNavTitle(row: any) {
|
||||
return row?.title || row?.name || '';
|
||||
}
|
||||
async function submitPublishApproval(row: any) {
|
||||
if (isAiResourceApprovalPending(row.publishStatus)) {
|
||||
function isRepublishAction(row: any) {
|
||||
return canAiResourceRepublish(row.displayPublishStatus, row.publishStatus);
|
||||
}
|
||||
|
||||
async function submitPublishAction(row: any) {
|
||||
if (
|
||||
isAiResourceApprovalPending(row.displayPublishStatus, row.publishStatus)
|
||||
) {
|
||||
ElMessage.warning($t('aiWorkflow.publishPendingHint'));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
$t('aiWorkflow.submitPublishApprovalConfirm'),
|
||||
isRepublishAction(row)
|
||||
? $t('aiWorkflow.submitRepublishApprovalConfirm')
|
||||
: $t('aiWorkflow.submitPublishApprovalConfirm'),
|
||||
$t('message.noticeTitle'),
|
||||
{
|
||||
confirmButtonText: $t('button.confirm'),
|
||||
@@ -300,7 +329,56 @@ async function submitPublishApproval(row: any) {
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const res = await api.post('/api/v1/workflow/submitPublishApproval', {
|
||||
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 submitOfflineAction(row: any) {
|
||||
if (
|
||||
isAiResourceApprovalPending(row.displayPublishStatus, row.publishStatus)
|
||||
) {
|
||||
ElMessage.warning($t('aiWorkflow.publishPendingHint'));
|
||||
return;
|
||||
}
|
||||
const impactRes = await api.get<{
|
||||
data: OfflineImpactCheck;
|
||||
errorCode: number;
|
||||
}>(
|
||||
'/api/v1/workflow/offlineImpactCheck',
|
||||
{
|
||||
params: { id: row.id },
|
||||
},
|
||||
);
|
||||
if (impactRes.errorCode !== 0) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await ElMessageBox.confirm(
|
||||
impactRes.data?.hasBotBindings
|
||||
? buildOfflineImpactMessage(
|
||||
$t('aiWorkflow.offlineImpactBoundBotsIntro'),
|
||||
impactRes.data.botBindings,
|
||||
$t('aiWorkflow.offlineImpactBoundBotsFooter'),
|
||||
)
|
||||
: $t('aiWorkflow.submitOfflineApprovalConfirm'),
|
||||
$t('message.noticeTitle'),
|
||||
{
|
||||
confirmButtonText: $t('button.confirm'),
|
||||
cancelButtonText: $t('button.cancel'),
|
||||
type: 'warning',
|
||||
},
|
||||
);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const res = await api.post('/api/v1/workflow/submitOfflineApproval', {
|
||||
id: row.id,
|
||||
});
|
||||
if (res.errorCode === 0) {
|
||||
@@ -309,7 +387,9 @@ async function submitPublishApproval(row: any) {
|
||||
}
|
||||
}
|
||||
async function submitDeleteApproval(row: any) {
|
||||
if (isAiResourceApprovalPending(row.publishStatus)) {
|
||||
if (
|
||||
isAiResourceApprovalPending(row.displayPublishStatus, row.publishStatus)
|
||||
) {
|
||||
ElMessage.warning($t('aiWorkflow.deletePendingHint'));
|
||||
return;
|
||||
}
|
||||
@@ -334,14 +414,29 @@ async function submitDeleteApproval(row: any) {
|
||||
pageDataRef.value?.reload?.();
|
||||
}
|
||||
}
|
||||
function resolvePublishStatusMeta(status?: string) {
|
||||
switch (normalizeAiPublishStatus(status)) {
|
||||
function resolvePublishStatusMetaByInstance(
|
||||
displayPublishStatus?: string,
|
||||
publishStatus?: string,
|
||||
) {
|
||||
switch (resolveAiResourceDisplayStatus(displayPublishStatus, publishStatus)) {
|
||||
case 'DELETE_PENDING': {
|
||||
return {
|
||||
label: $t('aiWorkflow.publishStatusDeletePending'),
|
||||
tone: 'danger',
|
||||
};
|
||||
}
|
||||
case 'OFFLINE_PENDING': {
|
||||
return {
|
||||
label: $t('aiWorkflow.publishStatusOfflinePending'),
|
||||
tone: 'pending',
|
||||
};
|
||||
}
|
||||
case 'OFFLINE': {
|
||||
return {
|
||||
label: $t('aiWorkflow.publishStatusOffline'),
|
||||
tone: 'draft',
|
||||
};
|
||||
}
|
||||
case 'PUBLISH_PENDING': {
|
||||
return {
|
||||
label: $t('aiWorkflow.publishStatusPublishPending'),
|
||||
@@ -572,11 +667,11 @@ function handleHeaderButtonClick(data: any) {
|
||||
<template #publish>
|
||||
<div
|
||||
class="workflow-publish-chip"
|
||||
:class="`workflow-publish-chip--${resolvePublishStatusMeta(item.publishStatus).tone}`"
|
||||
:class="`workflow-publish-chip--${resolvePublishStatusMetaByInstance(item.displayPublishStatus, item.publishStatus).tone}`"
|
||||
>
|
||||
<span class="workflow-publish-chip__dot"></span>
|
||||
<span>{{
|
||||
resolvePublishStatusMeta(item.publishStatus).label
|
||||
resolvePublishStatusMetaByInstance(item.displayPublishStatus, item.publishStatus).label
|
||||
}}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -36,6 +36,7 @@ const resourceLabelMap: Record<string, string> = {
|
||||
|
||||
const actionLabelMap: Record<string, string> = {
|
||||
DELETE: $t('approval.action.delete'),
|
||||
OFFLINE: $t('approval.action.offline'),
|
||||
PUBLISH: $t('approval.action.publish'),
|
||||
};
|
||||
|
||||
@@ -138,18 +139,28 @@ 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})`;
|
||||
function formatAccountDisplay(
|
||||
name?: string,
|
||||
account?: null | string,
|
||||
fallbackId?: null | number | string,
|
||||
) {
|
||||
if (name && account) {
|
||||
return `${name}(${account})`;
|
||||
}
|
||||
if (name) {
|
||||
return name;
|
||||
}
|
||||
return id || '-';
|
||||
if (account) {
|
||||
return account;
|
||||
}
|
||||
return fallbackId || '-';
|
||||
}
|
||||
|
||||
function formatOperatorId(id?: null | number | string) {
|
||||
return id || '-';
|
||||
function formatOperatorId(
|
||||
account?: null | string,
|
||||
fallbackId?: null | number | string,
|
||||
) {
|
||||
return account || fallbackId || '-';
|
||||
}
|
||||
|
||||
function formatOperatorName(name?: null | string) {
|
||||
@@ -276,7 +287,13 @@ function formatEventInfo(row: Record<string, any>) {
|
||||
{{ detail.id || '-' }}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="$t('approval.fields.applicant')">
|
||||
{{ formatAccountDisplay(detail.applicantName, detail.applicantId) }}
|
||||
{{
|
||||
formatAccountDisplay(
|
||||
detail.applicantName,
|
||||
detail.applicantAccount,
|
||||
detail.applicantId,
|
||||
)
|
||||
}}
|
||||
</ElDescriptionsItem>
|
||||
<ElDescriptionsItem :label="$t('approval.fields.submittedAt')">
|
||||
{{ detail.submittedAt || '-' }}
|
||||
@@ -347,7 +364,7 @@ function formatEventInfo(row: Record<string, any>) {
|
||||
</ElTableColumn>
|
||||
<ElTableColumn :label="$t('approval.fields.operatorId')" width="140">
|
||||
<template #default="{ row }">
|
||||
{{ formatOperatorId(row.operatorId) }}
|
||||
{{ formatOperatorId(row.operatorAccount, row.operatorId) }}
|
||||
</template>
|
||||
</ElTableColumn>
|
||||
<ElTableColumn
|
||||
|
||||
@@ -31,7 +31,7 @@ defineExpose({
|
||||
});
|
||||
|
||||
type ResourceType = '' | 'BOT' | 'KNOWLEDGE' | 'WORKFLOW';
|
||||
type ActionType = '' | 'DELETE' | 'PUBLISH';
|
||||
type ActionType = '' | 'DELETE' | 'OFFLINE' | 'PUBLISH';
|
||||
type AssigneeType = 'ROLE' | 'USER';
|
||||
type ScopeType = 'CATEGORY' | 'DEPT';
|
||||
type FlowStatus = 'DISABLED' | 'ENABLED';
|
||||
@@ -77,6 +77,7 @@ const RESOURCE_OPTIONS = [
|
||||
|
||||
const ACTION_OPTIONS = [
|
||||
{ label: $t('approval.action.publish'), value: 'PUBLISH' },
|
||||
{ label: $t('approval.action.offline'), value: 'OFFLINE' },
|
||||
{ label: $t('approval.action.delete'), value: 'DELETE' },
|
||||
];
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
} from 'element-plus';
|
||||
|
||||
import { api } from '#/api/request';
|
||||
import { hasPermission } from '#/api/common/hasPermission';
|
||||
import ListPageShell from '#/components/page/ListPageShell.vue';
|
||||
import PageData from '#/components/page/PageData.vue';
|
||||
import { $t } from '#/locales';
|
||||
@@ -40,6 +41,7 @@ const RESOURCE_OPTIONS = [
|
||||
|
||||
const ACTION_OPTIONS = [
|
||||
{ label: $t('approval.action.publish'), value: 'PUBLISH' },
|
||||
{ label: $t('approval.action.offline'), value: 'OFFLINE' },
|
||||
{ label: $t('approval.action.delete'), value: 'DELETE' },
|
||||
];
|
||||
|
||||
@@ -66,21 +68,25 @@ const TAB_CONFIG = [
|
||||
label: $t('approval.tab.flow'),
|
||||
name: 'flow',
|
||||
path: '/sys/approval/flow',
|
||||
permission: '/page/approval/flow',
|
||||
},
|
||||
{
|
||||
label: $t('approval.tab.pending'),
|
||||
name: 'pending',
|
||||
path: '/sys/approval/pending',
|
||||
permission: '/page/approval/pending',
|
||||
},
|
||||
{
|
||||
label: $t('approval.tab.processed'),
|
||||
name: 'processed',
|
||||
path: '/sys/approval/processed',
|
||||
permission: '/page/approval/processed',
|
||||
},
|
||||
{
|
||||
label: $t('approval.tab.initiated'),
|
||||
name: 'initiated',
|
||||
path: '/sys/approval/initiated',
|
||||
permission: '/page/approval/initiated',
|
||||
},
|
||||
] as const;
|
||||
|
||||
@@ -115,14 +121,23 @@ const pendingBadgeText = computed(() =>
|
||||
const hasApprovalRootMenu = computed(
|
||||
() => !!accessStore.getMenuByPath('/sys/approval'),
|
||||
);
|
||||
const hasLegacyTabMenus = computed(() =>
|
||||
TAB_CONFIG.some((item) => !!accessStore.getMenuByPath(item.path)),
|
||||
);
|
||||
const hasButtonTabPermissions = computed(() =>
|
||||
TAB_CONFIG.some((item) => hasPermission([item.permission])),
|
||||
);
|
||||
const visibleTabs = computed(() => {
|
||||
const matchedTabs = TAB_CONFIG.filter((item) =>
|
||||
accessStore.getMenuByPath(item.path),
|
||||
);
|
||||
if (matchedTabs.length > 0) {
|
||||
return matchedTabs;
|
||||
if (!hasApprovalRootMenu.value) {
|
||||
return [];
|
||||
}
|
||||
return hasApprovalRootMenu.value ? [...TAB_CONFIG] : [];
|
||||
if (hasButtonTabPermissions.value) {
|
||||
return TAB_CONFIG.filter((item) => hasPermission([item.permission]));
|
||||
}
|
||||
if (hasLegacyTabMenus.value) {
|
||||
return TAB_CONFIG.filter((item) => accessStore.getMenuByPath(item.path));
|
||||
}
|
||||
return [];
|
||||
});
|
||||
const visibleTabNames = computed(() =>
|
||||
visibleTabs.value.map((item) => item.name),
|
||||
@@ -139,7 +154,7 @@ const instanceStatusOptions = computed(() => {
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
void syncRouteTab(route.path);
|
||||
void syncRouteTab();
|
||||
});
|
||||
|
||||
watch(activeTab, () => {
|
||||
@@ -153,9 +168,9 @@ watch(activeTab, () => {
|
||||
}
|
||||
});
|
||||
watch(
|
||||
[() => route.path, visibleTabNames],
|
||||
async ([path]) => {
|
||||
await syncRouteTab(path);
|
||||
[() => route.fullPath, visibleTabNames],
|
||||
async () => {
|
||||
await syncRouteTab();
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
@@ -177,19 +192,55 @@ function resolveTabNameByPath(path: string): ApprovalTabName {
|
||||
return 'flow';
|
||||
}
|
||||
|
||||
async function syncRouteTab(path: string) {
|
||||
function resolveTabNameByRoute() {
|
||||
const queryTab = String(route.query.tab || '');
|
||||
if (
|
||||
queryTab === 'flow' ||
|
||||
queryTab === 'pending' ||
|
||||
queryTab === 'processed' ||
|
||||
queryTab === 'initiated'
|
||||
) {
|
||||
return queryTab as ApprovalTabName;
|
||||
}
|
||||
return resolveTabNameByPath(route.path);
|
||||
}
|
||||
|
||||
async function syncRouteTab() {
|
||||
if (visibleTabs.value.length === 0) {
|
||||
activeTab.value = 'flow';
|
||||
pendingBadgeCount.value = 0;
|
||||
return;
|
||||
}
|
||||
const expectedTab = resolveTabNameByPath(path);
|
||||
const fallbackTab = visibleTabs.value[0];
|
||||
const queryTab = String(route.query.tab || '');
|
||||
|
||||
// 进入审批管理根路径时,默认落到当前可见页签中排序第一个的页签。
|
||||
if (route.path === '/sys/approval' && !queryTab && fallbackTab) {
|
||||
activeTab.value = fallbackTab.name;
|
||||
if (fallbackTab.name !== 'flow') {
|
||||
await router.replace({
|
||||
path: '/sys/approval',
|
||||
query: { tab: fallbackTab.name },
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const expectedTab = resolveTabNameByRoute();
|
||||
if (visibleTabNames.value.includes(expectedTab)) {
|
||||
activeTab.value = expectedTab;
|
||||
} else {
|
||||
if (fallbackTab && path !== fallbackTab.path) {
|
||||
await router.replace(fallbackTab.path);
|
||||
const fallbackQuery =
|
||||
fallbackTab?.name === 'flow' ? {} : { tab: fallbackTab?.name };
|
||||
const currentQueryTab = String(route.query.tab || '');
|
||||
if (
|
||||
fallbackTab &&
|
||||
(route.path !== '/sys/approval' || currentQueryTab !== String(fallbackTab.name))
|
||||
) {
|
||||
await router.replace({
|
||||
path: '/sys/approval',
|
||||
query: fallbackQuery,
|
||||
});
|
||||
return;
|
||||
}
|
||||
activeTab.value = fallbackTab?.name || 'flow';
|
||||
@@ -206,10 +257,16 @@ function handleTabChange(name: number | string) {
|
||||
return;
|
||||
}
|
||||
const target = visibleTabs.value.find((item) => item.name === name);
|
||||
if (!target || route.path === target.path) {
|
||||
const nextQuery = name === 'flow' ? {} : { tab: name };
|
||||
const currentQueryTab = String(route.query.tab || '');
|
||||
const currentTab = currentQueryTab || 'flow';
|
||||
if (!target || currentTab === name) {
|
||||
return;
|
||||
}
|
||||
router.push(target.path);
|
||||
router.replace({
|
||||
path: '/sys/approval',
|
||||
query: nextQuery,
|
||||
});
|
||||
}
|
||||
|
||||
function reloadCurrentTab() {
|
||||
|
||||
@@ -611,6 +611,9 @@ function getTabKey(tab: RouteLocationNormalized | RouteRecordNormalized) {
|
||||
meta: { fullPathKey } = {},
|
||||
query = {},
|
||||
} = tab as RouteLocationNormalized;
|
||||
if (path === '/sys/approval') {
|
||||
return path;
|
||||
}
|
||||
// pageKey可能是数组(查询参数重复时可能出现)
|
||||
const pageKey = Array.isArray(query.pageKey)
|
||||
? query.pageKey[0]
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
interface BotInfo {
|
||||
alias: string;
|
||||
anonymousEnabled: boolean;
|
||||
approvalPending?: boolean;
|
||||
currentApprovalInstanceId?: number | string;
|
||||
currentApprovalActionType?: string;
|
||||
displayPublishStatus?: string;
|
||||
created: string;
|
||||
createdBy: number;
|
||||
deptId: number;
|
||||
|
||||
Reference in New Issue
Block a user