feat: 归档L03与L09审批发布能力

- 新增统一审批中心与审批管理页面,支持流程配置、审批详情与角色/用户审批对象

- 接入聊天助手、知识库、工作流的发布与删除审批,并补齐发布态校验与快照展示
This commit is contained in:
2026-04-07 14:41:52 +08:00
parent 7e7c236c2a
commit 3f128e977a
138 changed files with 13035 additions and 346 deletions

View File

@@ -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 {