Restore-ZQ-admin-quality-while-trimming-AI-routes

This commit is contained in:
Codex
2026-06-08 20:39:22 +08:00
parent 2e7099cb6d
commit 4bb3593fc8
10 changed files with 342 additions and 362 deletions
+30 -3
View File
@@ -17,8 +17,33 @@ from core.menu.schema import MenuCreate, MenuUpdate
menu_cache = CacheManager(prefix="menu:")
# 缓存key
MENU_TREE_CACHE_KEY = "tree"
USER_ROUTE_CACHE_PREFIX = "user_route:"
MENU_TREE_CACHE_KEY = "tree:ai_agent_admin"
USER_ROUTE_CACHE_PREFIX = "user_route:ai_agent_admin:"
AI_AGENT_ADMIN_MENU_NAMES = {
"AIPlatform",
"AIAgent",
"AIWorkflow",
"KnowledgeBase",
"Codex",
"SystemConfigManager",
"SystemManagement",
"SystemPermission",
"UserManagement",
"SystemMenu",
"SystemRole",
"Message",
"AnnouncementList",
"AnnouncementManage",
"userCenter",
"userSettings",
"LoginLog",
}
def _filter_ai_agent_admin_menus(menus: List[Menu]) -> List[Menu]:
"""Keep only the lightweight admin and AI modules for this product."""
return [menu for menu in menus if menu.name in AI_AGENT_ADMIN_MENU_NAMES]
class MenuService(BaseService[Menu, MenuCreate, MenuUpdate]):
@@ -239,7 +264,7 @@ class MenuService(BaseService[Menu, MenuCreate, MenuUpdate]):
Menu.is_deleted == False # noqa: E712
).order_by(Menu.order)
)
return list(result.scalars().all())
return _filter_ai_agent_admin_menus(list(result.scalars().all()))
@classmethod
async def build_tree(
@@ -614,6 +639,8 @@ class MenuService(BaseService[Menu, MenuCreate, MenuUpdate]):
)
menus = list(result.scalars().all())
menus = _filter_ai_agent_admin_menus(menus)
# 构建路由树,传递 application_code 用于添加路径前缀
# 开发模式使用 /app-dev/{code} 前缀,正常模式使用 /app/{code} 前缀
# 如果是开发模式且有选中的系统菜单,传递选中列表用于过滤
@@ -3,10 +3,27 @@ import type { DesignPreviewData } from './types';
import { computed, ref, watch } from 'vue';
import { ElButton, ElDialog, ElInput, ElMessage } from 'element-plus';
import { Check, Copy, X } from '@vben/icons';
import {
ElButton,
ElDescriptions,
ElDescriptionsItem,
ElDialog,
ElDivider,
ElInput,
ElMessage,
ElScrollbar,
ElTag,
} from 'element-plus';
const props = defineProps<{
basicInfo?: Record<string, any>;
data?: Record<string, any>;
design?: DesignPreviewData | Record<string, any>;
preview?: DesignPreviewData;
publishData?: Record<string, any>;
settings?: Record<string, any>;
title?: string;
visible: boolean;
}>();
@@ -19,15 +36,56 @@ const emit = defineEmits<{
const draftText = ref('{}');
const dialogTitle = computed(
() => props.preview?.title || props.title || '确认内容',
);
const sourceData = computed(() => {
return (
props.preview ||
props.design ||
props.basicInfo ||
props.settings ||
props.publishData ||
props.data ||
{}
);
});
const payload = computed(() => {
const source = sourceData.value as Record<string, any>;
if ('data' in source && source.data && typeof source.data === 'object') {
return source.data;
}
return source;
});
const dialogTitle = computed(() => {
const source = sourceData.value as Record<string, any>;
return source.title || props.title || 'AI 方案确认';
});
const summaryItems = computed(() => {
const source = payload.value as Record<string, any>;
const keys = ['name', 'title', 'description', 'code', 'type', 'category'];
return keys
.filter((key) => source[key] !== undefined && source[key] !== null)
.map((key) => ({ key, value: String(source[key]) }));
});
const fieldItems = computed(() => {
const source = payload.value as Record<string, any>;
const fields = source.form_fields || source.fields || source.schema?.fields;
return Array.isArray(fields) ? fields.slice(0, 12) : [];
});
const tableItems = computed(() => {
const source = payload.value as Record<string, any>;
const tables = source.table_configs || source.tables || source.data_sources;
return Array.isArray(tables) ? tables.slice(0, 8) : [];
});
watch(
() => [props.visible, props.preview] as const,
() => [props.visible, sourceData.value] as const,
() => {
if (props.visible) {
draftText.value = JSON.stringify(props.preview?.data || {}, null, 2);
draftText.value = JSON.stringify(payload.value || {}, null, 2);
}
},
{ immediate: true },
@@ -48,6 +106,11 @@ function handleConfirm() {
}
}
async function copyJson() {
await navigator.clipboard?.writeText(draftText.value);
ElMessage.success('已复制');
}
function handleVisibleChange(visible: boolean) {
if (!visible) handleClose();
}
@@ -57,25 +120,160 @@ function handleVisibleChange(visible: boolean) {
<ElDialog
:model-value="visible"
:title="dialogTitle"
width="720px"
width="860px"
append-to-body
destroy-on-close
class="ai-design-preview-dialog"
@update:model-value="handleVisibleChange"
>
<div class="space-y-3">
<div class="text-muted-foreground text-sm">
{{ preview?.type || 'design_preview' }}
</div>
<ElInput
v-model="draftText"
type="textarea"
:rows="18"
resize="vertical"
/>
<div class="ai-design-preview">
<section class="preview-panel">
<div class="panel-title">方案概要</div>
<ElDescriptions v-if="summaryItems.length" :column="2" border size="small">
<ElDescriptionsItem
v-for="item in summaryItems"
:key="item.key"
:label="item.key"
>
{{ item.value }}
</ElDescriptionsItem>
</ElDescriptions>
<div v-else class="empty-hint">当前方案未提供概要字段</div>
<ElDivider v-if="fieldItems.length || tableItems.length" />
<template v-if="fieldItems.length">
<div class="panel-subtitle">字段设计</div>
<div class="tag-grid">
<ElTag
v-for="(field, index) in fieldItems"
:key="field.id || field.name || index"
effect="plain"
>
{{ field.label || field.title || field.name || `字段 ${index + 1}` }}
</ElTag>
</div>
</template>
<template v-if="tableItems.length">
<div class="panel-subtitle">数据表配置</div>
<div class="table-list">
<div
v-for="(table, index) in tableItems"
:key="table.id || table.name || index"
class="table-item"
>
<span>{{ table.title || table.name || `数据表 ${index + 1}` }}</span>
<small>{{ table.code || table.table_name || table.type || '' }}</small>
</div>
</div>
</template>
</section>
<section class="editor-panel">
<div class="panel-title-row">
<span class="panel-title">结构化数据</span>
<ElButton size="small" :icon="Copy" @click="copyJson">复制</ElButton>
</div>
<ElScrollbar height="390px">
<ElInput
v-model="draftText"
type="textarea"
:autosize="{ minRows: 18, maxRows: 24 }"
resize="none"
class="json-editor"
/>
</ElScrollbar>
</section>
</div>
<template #footer>
<ElButton @click="handleClose">取消</ElButton>
<ElButton type="primary" @click="handleConfirm">确认</ElButton>
<ElButton :icon="X" @click="handleClose">取消</ElButton>
<ElButton type="primary" :icon="Check" @click="handleConfirm">
确认采用
</ElButton>
</template>
</ElDialog>
</template>
<style scoped>
.ai-design-preview {
display: grid;
grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
gap: 16px;
}
.preview-panel,
.editor-panel {
min-width: 0;
border: 1px solid var(--el-border-color-light);
border-radius: 8px;
padding: 14px;
background: var(--el-bg-color);
}
.panel-title,
.panel-title-row {
font-size: 14px;
font-weight: 600;
margin-bottom: 12px;
}
.panel-title-row {
display: flex;
align-items: center;
justify-content: space-between;
}
.panel-subtitle {
font-size: 13px;
font-weight: 600;
margin: 12px 0 8px;
}
.empty-hint {
color: var(--el-text-color-secondary);
font-size: 13px;
padding: 24px 0;
text-align: center;
}
.tag-grid {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.table-list {
display: grid;
gap: 8px;
}
.table-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
border: 1px solid var(--el-border-color-lighter);
border-radius: 6px;
padding: 8px 10px;
}
.table-item small {
color: var(--el-text-color-secondary);
}
.json-editor :deep(textarea) {
font-family:
ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono',
'Courier New', monospace;
font-size: 12px;
line-height: 1.55;
}
@media (max-width: 900px) {
.ai-design-preview {
grid-template-columns: 1fr;
}
}
</style>
+1 -1
View File
@@ -28,7 +28,6 @@ const modules = import.meta.glob([
'./langs/*/announcement.json',
'./langs/*/apiToken.json',
'./langs/*/authentication.json',
'./langs/*/chat.json',
'./langs/*/common.json',
'./langs/*/loginLog.json',
'./langs/*/menu.json',
@@ -40,6 +39,7 @@ const modules = import.meta.glob([
'./langs/*/system-config.json',
'./langs/*/system.json',
'./langs/*/ui.json',
'./langs/*/ui-config.json',
'./langs/*/user-avatar.json',
'./langs/*/user.json',
]);
+1 -3
View File
@@ -22,16 +22,14 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) {
'../views/_core/announcement/index.vue',
'../views/_core/announcement/list.vue',
'../views/_core/authentication/**/*.vue',
'../views/_core/chat/**/*.vue',
'../views/_core/fallback/**/*.vue',
'../views/_core/file-preview/index.vue',
'../views/_core/login-log/index.vue',
'../views/_core/menu/index.vue',
'../views/_core/message/index.vue',
'../views/_core/mobile-signature/index.vue',
'../views/_core/permission/index.vue',
'../views/_core/role/index.vue',
'../views/_core/system-config/index.vue',
'../views/_core/ui-config/index.vue',
'../views/_core/user/index.vue',
'../views/ai-platform/agent/editor/index.vue',
'../views/ai-platform/agent/index.vue',
@@ -97,28 +97,6 @@ const coreRoutes: RouteRecordRaw[] = [
title: 'OAuth Callback',
},
},
{
name: 'MobileSignature',
path: '/mobile-signature/:token',
component: () => import('#/views/_core/mobile-signature/index.vue'),
meta: {
hideInBreadcrumb: true,
hideInMenu: true,
hideInTab: true,
title: 'Mobile Signature',
},
},
{
name: 'FilePreview',
path: '/file-preview/:id',
component: () => import('#/views/_core/file-preview/index.vue'),
meta: {
hideInBreadcrumb: true,
hideInMenu: true,
hideInTab: true,
title: 'File Preview',
},
},
];
export { coreRoutes, fallbackNotFoundRoute };
+1 -3
View File
@@ -41,16 +41,14 @@ const componentKeys: string[] = Object.keys(
'../../views/_core/announcement/index.vue',
'../../views/_core/announcement/list.vue',
'../../views/_core/authentication/**/*.vue',
'../../views/_core/chat/**/*.vue',
'../../views/_core/fallback/**/*.vue',
'../../views/_core/file-preview/index.vue',
'../../views/_core/login-log/index.vue',
'../../views/_core/menu/index.vue',
'../../views/_core/message/index.vue',
'../../views/_core/mobile-signature/index.vue',
'../../views/_core/permission/index.vue',
'../../views/_core/role/index.vue',
'../../views/_core/system-config/index.vue',
'../../views/_core/ui-config/index.vue',
'../../views/_core/user/index.vue',
'../../views/ai-platform/agent/editor/index.vue',
'../../views/ai-platform/agent/index.vue',
@@ -291,9 +291,12 @@ async function handlePreviewDoc(doc: KnowledgeDocumentListItem) {
docPreviewUrlList.value = [url];
docPreviewVisible.value = true;
} else {
const ext = getDocFileExt(doc);
const query = new URLSearchParams({ name: doc.name || '', ext });
window.open(`/file-preview/${doc.file_id}?${query.toString()}`, '_blank');
try {
const url = await getFileUrl(doc.file_id);
window.open(url, '_blank');
} catch {
ElMessage.error($t('ai-platform.knowledge.chunkPreview.error'));
}
}
}
@@ -1,29 +1,16 @@
<script lang="ts" setup>
import type { WorkflowRunListItem } from '#/api/ai-platform/ai-platform';
import { computed, onMounted, ref } from 'vue';
import { ref } from 'vue';
import { Page } from '@vben/common-ui';
import { Eye, RefreshCw, Search } from '@vben/icons';
import { Eye } from '@vben/icons';
import { $t } from '@vben/locales';
import {
ElButton,
ElCard,
ElForm,
ElFormItem,
ElOption,
ElPagination,
ElSelect,
ElTable,
ElTableColumn,
ElTag,
} from 'element-plus';
import { ElButton, ElTag } from 'element-plus';
import {
getAllWorkflowRunsApi,
getWorkflowListApi,
} from '#/api/ai-platform/ai-platform';
import { getAllWorkflowRunsApi } from '#/api/ai-platform/ai-platform';
import { useZqTable } from '#/components/zq-table';
import RunStatusTag from './components/RunStatusTag.vue';
import {
@@ -31,245 +18,97 @@ import {
formatTime,
getTagLabel,
getTagType,
getStatusOptions,
getTriggerOptions,
useSearchFormSchema,
useZqTableColumns,
} from './data';
import DetailDialog from './modules/detail-dialog.vue';
defineOptions({ name: 'WorkflowRunHistory' });
const detailRef = ref<InstanceType<typeof DetailDialog>>();
const loading = ref(false);
const rows = ref<WorkflowRunListItem[]>([]);
const total = ref(0);
const currentPage = ref(1);
const pageSize = ref(20);
const workflowOptions = ref<Array<{ label: string; value: string }>>([]);
const filters = ref({
workflowId: '',
status: '',
triggerType: '',
});
const statusOptions = getStatusOptions();
const triggerOptions = getTriggerOptions();
const rowIndex = computed(() => (currentPage.value - 1) * pageSize.value);
async function loadWorkflowOptions() {
const res = await getWorkflowListApi({ page: 1, pageSize: 200 });
workflowOptions.value = (res.items || []).map((item) => ({
label: item.name,
value: item.id,
}));
}
const fetchRunList = async (params: any) => {
const res = await getAllWorkflowRunsApi({
page: params.page.currentPage,
pageSize: params.page.pageSize,
workflowId: params.form?.workflowId || undefined,
status: params.form?.status || undefined,
triggerType: params.form?.triggerType || undefined,
});
return {
items: res.items,
total: res.total,
};
};
async function loadRuns() {
loading.value = true;
try {
const res = await getAllWorkflowRunsApi({
page: currentPage.value,
pageSize: pageSize.value,
workflowId: filters.value.workflowId || undefined,
status: filters.value.status || undefined,
triggerType: filters.value.triggerType || undefined,
});
rows.value = res.items || [];
total.value = res.total || 0;
} finally {
loading.value = false;
}
}
function handleSearch() {
currentPage.value = 1;
loadRuns();
}
function handleSizeChange(size: number) {
pageSize.value = size;
currentPage.value = 1;
loadRuns();
}
function handlePageChange(page: number) {
currentPage.value = page;
loadRuns();
}
const [Grid] = useZqTable({
gridOptions: {
columns: useZqTableColumns(),
border: true,
stripe: true,
showIndex: true,
proxyConfig: {
autoLoad: true,
ajax: {
query: fetchRunList,
},
},
pagerConfig: {
enabled: true,
pageSize: 20,
},
toolbarConfig: {
search: true,
refresh: true,
zoom: true,
custom: true,
},
},
formOptions: {
schema: useSearchFormSchema(),
showCollapseButton: true,
submitOnChange: true,
},
});
function openDetail(row: WorkflowRunListItem) {
detailRef.value?.open(row.id);
}
onMounted(() => {
loadWorkflowOptions();
loadRuns();
});
</script>
<template>
<Page auto-content-height>
<DetailDialog ref="detailRef" />
<ElCard shadow="never" class="h-full">
<ElForm :model="filters" inline class="mb-3">
<ElFormItem :label="$t('ai-platform.workflowRuns.filters.workflow')">
<ElSelect
v-model="filters.workflowId"
clearable
filterable
style="width: 220px"
:placeholder="$t('ai-platform.workflowRuns.filters.allWorkflows')"
>
<ElOption
v-for="item in workflowOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</ElSelect>
</ElFormItem>
<ElFormItem :label="$t('ai-platform.workflowRuns.filters.status')">
<ElSelect
v-model="filters.status"
clearable
style="width: 150px"
:placeholder="$t('ai-platform.workflowRuns.filters.allStatus')"
>
<ElOption
v-for="item in statusOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</ElSelect>
</ElFormItem>
<ElFormItem :label="$t('ai-platform.workflowRuns.filters.trigger')">
<ElSelect
v-model="filters.triggerType"
clearable
style="width: 160px"
:placeholder="$t('ai-platform.workflowRuns.filters.allTriggers')"
>
<ElOption
v-for="item in triggerOptions"
:key="item.value"
:label="item.label"
:value="item.value"
/>
</ElSelect>
</ElFormItem>
<ElFormItem>
<ElButton type="primary" :icon="Search" @click="handleSearch">
{{ $t('common.search') }}
</ElButton>
<ElButton :icon="RefreshCw" @click="loadRuns">
{{ $t('common.refresh') }}
</ElButton>
</ElFormItem>
</ElForm>
<Grid>
<template #cell-status="{ row }">
<RunStatusTag :status="row.status" />
</template>
<ElTable
v-loading="loading"
:data="rows"
border
stripe
height="calc(100vh - 285px)"
>
<ElTableColumn type="index" width="60" :index="rowIndex + 1" />
<ElTableColumn
prop="workflow_name"
:label="$t('ai-platform.workflowRuns.columns.workflow')"
min-width="160"
show-overflow-tooltip
/>
<ElTableColumn
:label="$t('ai-platform.workflowRuns.columns.status')"
width="110"
align="center"
<template #cell-trigger_type="{ row }">
<ElTag
:type="getTagType(row.trigger_type, triggerOptions)"
size="small"
>
<template #default="{ row }">
<RunStatusTag :status="row.status" />
</template>
</ElTableColumn>
<ElTableColumn
:label="$t('ai-platform.workflowRuns.columns.trigger')"
width="140"
align="center"
>
<template #default="{ row }">
<ElTag
:type="getTagType(row.trigger_type, triggerOptions)"
size="small"
>
{{ getTagLabel(row.trigger_type, triggerOptions) }}
</ElTag>
</template>
</ElTableColumn>
<ElTableColumn
prop="total_steps"
:label="$t('ai-platform.workflowRuns.columns.steps')"
width="80"
align="center"
/>
<ElTableColumn
prop="total_tokens"
:label="$t('ai-platform.workflowRuns.columns.tokens')"
width="90"
align="center"
/>
<ElTableColumn
:label="$t('ai-platform.workflowRuns.columns.duration')"
width="100"
align="center"
>
<template #default="{ row }">
{{ formatDuration(row.elapsed_time) }}
</template>
</ElTableColumn>
<ElTableColumn
:label="$t('ai-platform.workflowRuns.columns.startedAt')"
width="180"
>
<template #default="{ row }">
{{ formatTime(row.started_at) }}
</template>
</ElTableColumn>
<ElTableColumn
prop="error_message"
:label="$t('ai-platform.workflowRuns.columns.error')"
min-width="180"
show-overflow-tooltip
/>
<ElTableColumn
:label="$t('ai-platform.workflowRuns.columns.actions')"
width="120"
fixed="right"
align="center"
>
<template #default="{ row }">
<ElButton
link
type="primary"
:icon="Eye"
@click.stop="openDetail(row)"
>
{{ $t('common.view') }}
</ElButton>
</template>
</ElTableColumn>
</ElTable>
{{ getTagLabel(row.trigger_type, triggerOptions) }}
</ElTag>
</template>
<div class="mt-4 flex justify-end">
<ElPagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
background
layout="total, sizes, prev, pager, next, jumper"
:page-sizes="[10, 20, 50, 100]"
:total="total"
@current-change="handlePageChange"
@size-change="handleSizeChange"
/>
</div>
</ElCard>
<template #cell-elapsed_time="{ row }">
{{ formatDuration(row.elapsed_time) }}
</template>
<template #cell-started_at="{ row }">
{{ formatTime(row.started_at) }}
</template>
<template #cell-actions="{ row }">
<ElButton link type="primary" :icon="Eye" @click.stop="openDetail(row)">
{{ $t('common.view') }}
</ElButton>
</template>
</Grid>
</Page>
</template>
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { onMounted, ref, watch } from 'vue';
import { ref, watch } from 'vue';
import { $t } from '@vben/locales';
@@ -8,15 +8,9 @@ import {
ElFormItem,
ElInput,
ElInputNumber,
ElOption,
ElRadioButton,
ElRadioGroup,
ElSelect,
ElSwitch,
} from 'element-plus';
import { getFormListApi } from '#/api/online-dev/form-manager';
import SmartInput from '../components/SmartInput.vue';
const props = defineProps<{
@@ -96,30 +90,6 @@ const form = ref({
...props.data,
});
// 表单输入模式:variable(变量输入)或 select(下拉选择)
const formInputMode = ref<'select' | 'variable'>('variable');
// 表单列表
const formList = ref<any[]>([]);
const formLoading = ref(false);
// 加载表单列表
const loadFormList = async () => {
try {
formLoading.value = true;
const res = await getFormListApi({ page: 1, pageSize: 100, status: 'published' });
formList.value = res.items || [];
} catch (error) {
console.error('加载表单列表失败:', error);
} finally {
formLoading.value = false;
}
};
onMounted(() => {
loadFormList();
});
watch(
form,
(val) => {
@@ -166,48 +136,17 @@ const showField = (field: string) => {
</ElFormItem>
<ElFormItem :label="$t('ai-platform.workflow.panels.formData.formCode')">
<div class="flex items-center gap-2 mb-2">
<ElRadioGroup v-model="formInputMode" size="small">
<ElRadioButton value="variable">{{ $t('ai-platform.workflow.panels.formData.variableInput') }}</ElRadioButton>
<ElRadioButton value="select">{{ $t('ai-platform.workflow.panels.formData.selectFromList') }}</ElRadioButton>
</ElRadioGroup>
<SmartInput
v-model="form.form_code"
:current-node-id="nodeId"
:placeholder="
$t('ai-platform.workflow.panels.formData.formCodeInputPlaceholder') +
' {{form_code}}'
"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formData.formCodeInputHint') }}
</div>
<!-- 变量输入模式 -->
<template v-if="formInputMode === 'variable'">
<SmartInput
v-model="form.form_code"
:current-node-id="nodeId"
:placeholder="
$t('ai-platform.workflow.panels.formData.formCodeInputPlaceholder') +
' {{form_code}}'
"
/>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formData.formCodeInputHint') }}
</div>
</template>
<!-- 下拉选择模式 -->
<template v-else>
<ElSelect
v-model="form.form_code"
:placeholder="$t('ai-platform.workflow.panels.formData.selectFormPlaceholder')"
filterable
:loading="formLoading"
class="w-full"
>
<ElOption
v-for="f in formList"
:key="f.code"
:label="`${f.name} (${f.code})`"
:value="f.code"
/>
</ElSelect>
<div class="text-muted-foreground mt-1 text-xs">
{{ $t('ai-platform.workflow.panels.formData.selectedFormHint', { code: form.form_code || '-' }) }}
</div>
</template>
</ElFormItem>
<!-- 记录ID读取/更新/删除 -->
+1 -1
View File
@@ -17,7 +17,7 @@ const defaultPreferences: Preferences = {
contentPaddingTop: 0,
defaultAvatar:
'https://unpkg.com/@vbenjs/static-source@0.1.7/source/avatar-v1.webp',
defaultHomePath: '/page-render/main_home',
defaultHomePath: '/ai-platform/agent',
dynamicTitle: true,
enableCheckUpdates: true,
enablePreferences: true,