feat: 归档L03与L09审批发布能力
- 新增统一审批中心与审批管理页面,支持流程配置、审批详情与角色/用户审批对象 - 接入聊天助手、知识库、工作流的发布与删除审批,并补齐发布态校验与快照展示
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user