feat: 收敛AI资源发布审批生命周期

- 统一工作流、知识库、聊天助手的发布、重新发布、下线与删除链路

- 收敛审批编排、生命周期状态机与展示态,补齐审批管理和快照预览

- 调整审批管理权限模型为单入口页面加内部按钮权限
This commit is contained in:
2026-04-09 17:13:54 +08:00
parent 81125ce55c
commit 4e565aef99
68 changed files with 3859 additions and 817 deletions

View File

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