Restore rendered control center baseline
This commit is contained in:
@@ -191,9 +191,6 @@ web/apps/web-ele/src/views/online-dev/*
|
|||||||
!web/apps/web-ele/src/views/online-dev/page-render/
|
!web/apps/web-ele/src/views/online-dev/page-render/
|
||||||
!web/apps/web-ele/src/views/online-dev/page-render/**
|
!web/apps/web-ele/src/views/online-dev/page-render/**
|
||||||
web/apps/web-ele/src/views/dashboard/
|
web/apps/web-ele/src/views/dashboard/
|
||||||
!web/apps/web-ele/src/views/dashboard/
|
|
||||||
web/apps/web-ele/src/views/dashboard/*
|
|
||||||
!web/apps/web-ele/src/views/dashboard/index.vue
|
|
||||||
web/apps/web-ele/src/views/demos/
|
web/apps/web-ele/src/views/demos/
|
||||||
web/apps/web-ele/src/views/zq-smart-table/
|
web/apps/web-ele/src/views/zq-smart-table/
|
||||||
web/apps/web-ele/src/api/online-dev/
|
web/apps/web-ele/src/api/online-dev/
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from core.menu.schema import MenuCreate, MenuUpdate
|
|||||||
menu_cache = CacheManager(prefix="menu:")
|
menu_cache = CacheManager(prefix="menu:")
|
||||||
|
|
||||||
# 缓存key
|
# 缓存key
|
||||||
AI_AGENT_ADMIN_MENU_CACHE_VERSION = "v7"
|
AI_AGENT_ADMIN_MENU_CACHE_VERSION = "v8"
|
||||||
MENU_TREE_CACHE_KEY = f"tree:ai_agent_admin:{AI_AGENT_ADMIN_MENU_CACHE_VERSION}"
|
MENU_TREE_CACHE_KEY = f"tree:ai_agent_admin:{AI_AGENT_ADMIN_MENU_CACHE_VERSION}"
|
||||||
USER_ROUTE_CACHE_PREFIX = f"user_route:ai_agent_admin:{AI_AGENT_ADMIN_MENU_CACHE_VERSION}:"
|
USER_ROUTE_CACHE_PREFIX = f"user_route:ai_agent_admin:{AI_AGENT_ADMIN_MENU_CACHE_VERSION}:"
|
||||||
|
|
||||||
@@ -52,10 +52,10 @@ def _filter_ai_agent_admin_menus(menus: List[Menu]) -> List[Menu]:
|
|||||||
|
|
||||||
|
|
||||||
def _normalize_ai_agent_admin_menu(menu: Menu) -> None:
|
def _normalize_ai_agent_admin_menu(menu: Menu) -> None:
|
||||||
"""Route the retained control center to the lightweight native dashboard."""
|
"""Route the retained control center to the original rendered dashboard."""
|
||||||
if menu.name == "ControlCenter":
|
if menu.name == "ControlCenter":
|
||||||
menu.path = "/dashboard"
|
menu.path = "/page-render/main_home"
|
||||||
menu.component = "dashboard/index"
|
menu.component = "online-dev/page-render/index"
|
||||||
menu.query = None
|
menu.query = None
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,18 @@ from core.role.model import Role
|
|||||||
from core.role.schema import RoleCreate, RoleUpdate
|
from core.role.schema import RoleCreate, RoleUpdate
|
||||||
|
|
||||||
|
|
||||||
|
def _filter_ai_agent_admin_role_menus(menus: List[Any]) -> List[Any]:
|
||||||
|
from core.menu.service import (
|
||||||
|
AI_AGENT_ADMIN_MENU_NAMES,
|
||||||
|
_normalize_ai_agent_admin_menu,
|
||||||
|
)
|
||||||
|
|
||||||
|
filtered = [menu for menu in menus if menu.name in AI_AGENT_ADMIN_MENU_NAMES]
|
||||||
|
for menu in filtered:
|
||||||
|
_normalize_ai_agent_admin_menu(menu)
|
||||||
|
return filtered
|
||||||
|
|
||||||
|
|
||||||
class RoleService(BaseService[Role, RoleCreate, RoleUpdate]):
|
class RoleService(BaseService[Role, RoleCreate, RoleUpdate]):
|
||||||
"""
|
"""
|
||||||
角色服务层
|
角色服务层
|
||||||
@@ -430,26 +442,30 @@ class RoleService(BaseService[Role, RoleCreate, RoleUpdate]):
|
|||||||
# 菜单始终全量替换
|
# 菜单始终全量替换
|
||||||
from core.menu.model import Menu
|
from core.menu.model import Menu
|
||||||
result = await db.execute(select(Menu).where(Menu.id.in_(menu_ids)))
|
result = await db.execute(select(Menu).where(Menu.id.in_(menu_ids)))
|
||||||
menus = list(result.scalars().all())
|
menus = _filter_ai_agent_admin_role_menus(list(result.scalars().all()))
|
||||||
|
allowed_menu_ids = {menu.id for menu in menus}
|
||||||
role.menus = menus
|
role.menus = menus
|
||||||
|
|
||||||
from core.permission.model import Permission
|
from core.permission.model import Permission
|
||||||
|
|
||||||
if loaded_menu_ids:
|
if loaded_menu_ids:
|
||||||
# 增量更新:只替换已加载菜单下的权限
|
# 增量更新:只替换已加载菜单下的权限
|
||||||
loaded_menu_set = set(loaded_menu_ids)
|
loaded_menu_set = set(loaded_menu_ids) & allowed_menu_ids
|
||||||
|
|
||||||
# 保留未加载菜单下的原有权限
|
# 保留未加载菜单下的原有权限
|
||||||
kept_permissions = [
|
kept_permissions = [
|
||||||
p for p in (role.permissions or [])
|
p for p in (role.permissions or [])
|
||||||
if p.menu_id not in loaded_menu_set
|
if p.menu_id in allowed_menu_ids and p.menu_id not in loaded_menu_set
|
||||||
]
|
]
|
||||||
|
|
||||||
# 获取前端提交的权限(已加载菜单下用户选择的权限)
|
# 获取前端提交的权限(已加载菜单下用户选择的权限)
|
||||||
new_permissions = []
|
new_permissions = []
|
||||||
if permission_ids:
|
if permission_ids:
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(Permission).where(Permission.id.in_(permission_ids))
|
select(Permission).where(
|
||||||
|
Permission.id.in_(permission_ids),
|
||||||
|
Permission.menu_id.in_(allowed_menu_ids),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
new_permissions = list(result.scalars().all())
|
new_permissions = list(result.scalars().all())
|
||||||
|
|
||||||
@@ -459,7 +475,10 @@ class RoleService(BaseService[Role, RoleCreate, RoleUpdate]):
|
|||||||
# 兼容旧逻辑:全量替换
|
# 兼容旧逻辑:全量替换
|
||||||
if permission_ids:
|
if permission_ids:
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(Permission).where(Permission.id.in_(permission_ids))
|
select(Permission).where(
|
||||||
|
Permission.id.in_(permission_ids),
|
||||||
|
Permission.menu_id.in_(allowed_menu_ids),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
permissions = list(result.scalars().all())
|
permissions = list(result.scalars().all())
|
||||||
else:
|
else:
|
||||||
@@ -543,17 +562,20 @@ class RoleService(BaseService[Role, RoleCreate, RoleUpdate]):
|
|||||||
|
|
||||||
# 获取所有菜单
|
# 获取所有菜单
|
||||||
result = await db.execute(select(Menu).where(Menu.is_deleted == False)) # noqa: E712
|
result = await db.execute(select(Menu).where(Menu.is_deleted == False)) # noqa: E712
|
||||||
all_menus = list(result.scalars().all())
|
all_menus = _filter_ai_agent_admin_role_menus(list(result.scalars().all()))
|
||||||
|
allowed_menu_ids = {menu.id for menu in all_menus}
|
||||||
|
|
||||||
# 获取该角色已分配的权限ID和菜单ID
|
# 获取该角色已分配的权限ID和菜单ID
|
||||||
role_permission_ids = set(p.id for p in role.permissions) if role.permissions else set()
|
role_permission_ids = set(p.id for p in role.permissions) if role.permissions else set()
|
||||||
role_menu_ids = set(m.id for m in role.menus) if role.menus else set()
|
role_menu_ids = {m.id for m in (role.menus or []) if m.id in allowed_menu_ids}
|
||||||
|
|
||||||
# 获取所有启用的权限
|
# 获取所有启用的权限
|
||||||
result = await db.execute(
|
result = await db.execute(
|
||||||
select(Permission).where(Permission.is_active == True, Permission.is_deleted == False) # noqa: E712
|
select(Permission).where(Permission.is_active == True, Permission.is_deleted == False) # noqa: E712
|
||||||
)
|
)
|
||||||
all_permissions = list(result.scalars().all())
|
all_permissions = [
|
||||||
|
perm for perm in list(result.scalars().all()) if perm.menu_id in allowed_menu_ids
|
||||||
|
]
|
||||||
|
|
||||||
# 权限类型映射
|
# 权限类型映射
|
||||||
PERMISSION_TYPE_MAP = {
|
PERMISSION_TYPE_MAP = {
|
||||||
@@ -637,7 +659,9 @@ class RoleService(BaseService[Role, RoleCreate, RoleUpdate]):
|
|||||||
if application_id:
|
if application_id:
|
||||||
query = query.where(Menu.application_id == application_id)
|
query = query.where(Menu.application_id == application_id)
|
||||||
result = await db.execute(query)
|
result = await db.execute(query)
|
||||||
all_menus = list(result.scalars().all())
|
all_menus = _filter_ai_agent_admin_role_menus(list(result.scalars().all()))
|
||||||
|
allowed_menu_ids = {menu.id for menu in all_menus}
|
||||||
|
role_menu_ids = {menu_id for menu_id in role_menu_ids if menu_id in allowed_menu_ids}
|
||||||
|
|
||||||
# 统计每个菜单的权限数量
|
# 统计每个菜单的权限数量
|
||||||
permission_counts = {}
|
permission_counts = {}
|
||||||
@@ -685,10 +709,20 @@ class RoleService(BaseService[Role, RoleCreate, RoleUpdate]):
|
|||||||
@classmethod
|
@classmethod
|
||||||
async def get_menu_permissions(cls, db: AsyncSession, role: Role, menu_id: str) -> Dict[str, Any]:
|
async def get_menu_permissions(cls, db: AsyncSession, role: Role, menu_id: str) -> Dict[str, Any]:
|
||||||
"""获取指定菜单的权限列表"""
|
"""获取指定菜单的权限列表"""
|
||||||
|
from core.menu.model import Menu
|
||||||
from core.permission.model import Permission
|
from core.permission.model import Permission
|
||||||
|
|
||||||
# 获取该角色已选中的权限ID
|
# 获取该角色已选中的权限ID
|
||||||
role_permission_ids = set(p.id for p in role.permissions) if role.permissions else set()
|
role_permission_ids = set(p.id for p in role.permissions) if role.permissions else set()
|
||||||
|
menu_result = await db.execute(
|
||||||
|
select(Menu).where(Menu.id == menu_id, Menu.is_deleted == False) # noqa: E712
|
||||||
|
)
|
||||||
|
menu = menu_result.scalar_one_or_none()
|
||||||
|
if not menu or not _filter_ai_agent_admin_role_menus([menu]):
|
||||||
|
return {
|
||||||
|
'menu_id': menu_id,
|
||||||
|
'permissions': [],
|
||||||
|
}
|
||||||
|
|
||||||
# 权限类型映射
|
# 权限类型映射
|
||||||
PERMISSION_TYPE_MAP = {
|
PERMISSION_TYPE_MAP = {
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ const baseModules = import.meta.glob([
|
|||||||
const businessModules = import.meta.glob([
|
const businessModules = import.meta.glob([
|
||||||
'./langs/zh-CN/ai-platform.json',
|
'./langs/zh-CN/ai-platform.json',
|
||||||
'./langs/zh-CN/announcement.json',
|
'./langs/zh-CN/announcement.json',
|
||||||
|
'./langs/zh-CN/dashboard-design.json',
|
||||||
'./langs/zh-CN/menu.json',
|
'./langs/zh-CN/menu.json',
|
||||||
'./langs/zh-CN/message.json',
|
'./langs/zh-CN/message.json',
|
||||||
'./langs/zh-CN/permission.json',
|
'./langs/zh-CN/permission.json',
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const overridesPreferences = defineOverridesPreferences({
|
|||||||
enablePreferences: false,
|
enablePreferences: false,
|
||||||
accessMode: 'mixed',
|
accessMode: 'mixed',
|
||||||
authPageLayout: 'panel-center',
|
authPageLayout: 'panel-center',
|
||||||
defaultHomePath: '/dashboard',
|
defaultHomePath: '/page-render/main_home',
|
||||||
layout: 'header-sidebar-nav',
|
layout: 'header-sidebar-nav',
|
||||||
},
|
},
|
||||||
shortcutKeys: {
|
shortcutKeys: {
|
||||||
|
|||||||
@@ -83,8 +83,8 @@ function normalizeBackendRoute<
|
|||||||
if (route.name !== 'ControlCenter') return route;
|
if (route.name !== 'ControlCenter') return route;
|
||||||
|
|
||||||
const normalized = { ...route };
|
const normalized = { ...route };
|
||||||
normalized.path = '/dashboard';
|
normalized.path = '/page-render/main_home';
|
||||||
normalized.component = 'dashboard/index';
|
normalized.component = 'online-dev/page-render/index';
|
||||||
normalized.query = undefined;
|
normalized.query = undefined;
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
@@ -159,7 +159,7 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) {
|
|||||||
'../views/ai-platform/workflow/editor/index.vue',
|
'../views/ai-platform/workflow/editor/index.vue',
|
||||||
'../views/ai-platform/workflow/index.vue',
|
'../views/ai-platform/workflow/index.vue',
|
||||||
'../views/ai-platform/workflow-runs/index.vue',
|
'../views/ai-platform/workflow-runs/index.vue',
|
||||||
'../views/dashboard/index.vue',
|
'../views/online-dev/page-render/index.vue',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const layoutMap: ComponentRecordType = {
|
const layoutMap: ComponentRecordType = {
|
||||||
|
|||||||
@@ -191,7 +191,7 @@ function setupAccessGuard(router: Router) {
|
|||||||
|
|
||||||
if (to.path === subAppRootPath) {
|
if (to.path === subAppRootPath) {
|
||||||
const defaultHome =
|
const defaultHome =
|
||||||
preferences.app.defaultHomePath || '/dashboard';
|
preferences.app.defaultHomePath || '/page-render/main_home';
|
||||||
// 确保路径包含子应用前缀
|
// 确保路径包含子应用前缀
|
||||||
const subAppTargetPath = defaultHome.startsWith(subAppRootPath)
|
const subAppTargetPath = defaultHome.startsWith(subAppRootPath)
|
||||||
? defaultHome
|
? defaultHome
|
||||||
|
|||||||
@@ -1,12 +1,6 @@
|
|||||||
import type { RouteRecordRaw } from 'vue-router';
|
import type { RouteRecordRaw } from 'vue-router';
|
||||||
|
|
||||||
const routes: RouteRecordRaw[] = [
|
const routes: RouteRecordRaw[] = [
|
||||||
{
|
|
||||||
meta: { hideInMenu: true },
|
|
||||||
name: 'LegacyMainHomeRedirect',
|
|
||||||
path: '/page-render/main_home',
|
|
||||||
redirect: '/dashboard',
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
meta: { hideInMenu: true },
|
meta: { hideInMenu: true },
|
||||||
name: 'AIKnowledgeLegacyRedirect',
|
name: 'AIKnowledgeLegacyRedirect',
|
||||||
|
|||||||
@@ -109,10 +109,11 @@ const componentKeys: string[] = Object.keys(
|
|||||||
'../../../ai-platform/agent/index.vue',
|
'../../../ai-platform/agent/index.vue',
|
||||||
'../../../ai-platform/knowledge/detail/index.vue',
|
'../../../ai-platform/knowledge/detail/index.vue',
|
||||||
'../../../ai-platform/knowledge/index.vue',
|
'../../../ai-platform/knowledge/index.vue',
|
||||||
|
'../../../ai-platform/model/index.vue',
|
||||||
'../../../ai-platform/workflow/editor/index.vue',
|
'../../../ai-platform/workflow/editor/index.vue',
|
||||||
'../../../ai-platform/workflow/index.vue',
|
'../../../ai-platform/workflow/index.vue',
|
||||||
'../../../ai-platform/workflow-runs/index.vue',
|
'../../../ai-platform/workflow-runs/index.vue',
|
||||||
'../../../dashboard/index.vue',
|
'../../../online-dev/page-render/index.vue',
|
||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
.filter((item) => !item.includes('/modules/'))
|
.filter((item) => !item.includes('/modules/'))
|
||||||
@@ -120,7 +121,7 @@ const componentKeys: string[] = Object.keys(
|
|||||||
const path = v
|
const path = v
|
||||||
.replace('../../../_core/', '/_core/')
|
.replace('../../../_core/', '/_core/')
|
||||||
.replace('../../../ai-platform/', '/ai-platform/')
|
.replace('../../../ai-platform/', '/ai-platform/')
|
||||||
.replace('../../../dashboard/', '/dashboard/');
|
.replace('../../../online-dev/', '/online-dev/');
|
||||||
return path.endsWith('.vue') ? path.slice(0, -4) : path;
|
return path.endsWith('.vue') ? path.slice(0, -4) : path;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -95,6 +95,26 @@ function filterTreeByApp(nodes: MenuNode[], appId?: string): MenuNode[] {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function filterAiAgentAdminTree(nodes: MenuNode[]): MenuNode[] {
|
||||||
|
return nodes
|
||||||
|
.filter((node) => AI_AGENT_ADMIN_MENU_NAMES.has(node.name))
|
||||||
|
.map((node) => ({
|
||||||
|
...node,
|
||||||
|
children: node.children
|
||||||
|
? filterAiAgentAdminTree(node.children as MenuNode[])
|
||||||
|
: [],
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function collectMenuIds(nodes: MenuNode[], ids: Set<string>) {
|
||||||
|
nodes.forEach((node) => {
|
||||||
|
ids.add(node.id);
|
||||||
|
if (node.children?.length) {
|
||||||
|
collectMenuIds(node.children as MenuNode[], ids);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const treeData = computed(() =>
|
const treeData = computed(() =>
|
||||||
filterTreeByApp(allTreeData.value, selectedAppId.value),
|
filterTreeByApp(allTreeData.value, selectedAppId.value),
|
||||||
);
|
);
|
||||||
@@ -110,6 +130,29 @@ const appList = ref<ApplicationListItem[]>([]);
|
|||||||
const selectedAppId = ref<string | undefined>(undefined);
|
const selectedAppId = ref<string | undefined>(undefined);
|
||||||
const loadingApps = ref(false);
|
const loadingApps = ref(false);
|
||||||
|
|
||||||
|
const AI_AGENT_ADMIN_MENU_NAMES = new Set([
|
||||||
|
'ControlCenter',
|
||||||
|
'AIPlatform',
|
||||||
|
'AIAgent',
|
||||||
|
'AIModelConfig',
|
||||||
|
'AIWorkflow',
|
||||||
|
'AIWorkflowRuns',
|
||||||
|
'KnowledgeBase',
|
||||||
|
'Codex',
|
||||||
|
'SystemConfigManager',
|
||||||
|
'SystemManagement',
|
||||||
|
'SystemPermission',
|
||||||
|
'UserManagement',
|
||||||
|
'SystemMenu',
|
||||||
|
'SystemRole',
|
||||||
|
'Message',
|
||||||
|
'MessageList',
|
||||||
|
'AnnouncementList',
|
||||||
|
'AnnouncementManage',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const LEGACY_APP_KEYWORDS = ['审批中心', '车辆管理'];
|
||||||
|
|
||||||
const resourceScopeConfigRef = ref<InstanceType<typeof ResourceScopeConfig>>();
|
const resourceScopeConfigRef = ref<InstanceType<typeof ResourceScopeConfig>>();
|
||||||
const layoutContainerRef = ref<HTMLElement | null>(null);
|
const layoutContainerRef = ref<HTMLElement | null>(null);
|
||||||
const scrollAreaHeight = ref(400);
|
const scrollAreaHeight = ref(400);
|
||||||
@@ -134,7 +177,12 @@ async function loadApps() {
|
|||||||
try {
|
try {
|
||||||
loadingApps.value = true;
|
loadingApps.value = true;
|
||||||
const res = await getApplicationListApi({ pageSize: 100 });
|
const res = await getApplicationListApi({ pageSize: 100 });
|
||||||
appList.value = res.items || [];
|
appList.value = (res.items || []).filter(
|
||||||
|
(app) =>
|
||||||
|
!LEGACY_APP_KEYWORDS.some(
|
||||||
|
(keyword) => app.name.includes(keyword) || app.code.includes(keyword),
|
||||||
|
),
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error($t('role.permissions.loadAppsFailed'), error);
|
console.error($t('role.permissions.loadAppsFailed'), error);
|
||||||
ElMessage.error($t('role.permissions.loadAppsFailed'));
|
ElMessage.error($t('role.permissions.loadAppsFailed'));
|
||||||
@@ -223,11 +271,15 @@ async function loadMenuTree() {
|
|||||||
const data = await getRoleMenusApi(props.role.id);
|
const data = await getRoleMenusApi(props.role.id);
|
||||||
|
|
||||||
// 使用后端返回的菜单树结构
|
// 使用后端返回的菜单树结构
|
||||||
allTreeData.value = data.menu_tree || [];
|
allTreeData.value = filterAiAgentAdminTree(data.menu_tree || []);
|
||||||
|
|
||||||
// 初始化已选菜单
|
// 初始化已选菜单
|
||||||
const selectedMenuIdsList = data.selected_menu_ids || [];
|
const selectedMenuIdsList = data.selected_menu_ids || [];
|
||||||
selectedMenuIds.value = new Set(selectedMenuIdsList);
|
const visibleMenuIds = new Set<string>();
|
||||||
|
collectMenuIds(allTreeData.value, visibleMenuIds);
|
||||||
|
selectedMenuIds.value = new Set(
|
||||||
|
selectedMenuIdsList.filter((id: string) => visibleMenuIds.has(id)),
|
||||||
|
);
|
||||||
|
|
||||||
// 清空权限缓存和选中状态
|
// 清空权限缓存和选中状态
|
||||||
menuPermissionsCache.value = {};
|
menuPermissionsCache.value = {};
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ const appDynamicTitle = ref(true);
|
|||||||
const appWatermark = ref(false);
|
const appWatermark = ref(false);
|
||||||
const appWatermarkContent = ref('');
|
const appWatermarkContent = ref('');
|
||||||
const appEnableCheckUpdates = ref(true);
|
const appEnableCheckUpdates = ref(true);
|
||||||
const appDefaultHomePath = ref('/dashboard');
|
const appDefaultHomePath = ref('/page-render/main_home');
|
||||||
const appEnablePreferences = ref(true);
|
const appEnablePreferences = ref(true);
|
||||||
|
|
||||||
const footerEnable = ref(true);
|
const footerEnable = ref(true);
|
||||||
@@ -106,7 +106,7 @@ function getDefaultConfig() {
|
|||||||
defaultHomePath:
|
defaultHomePath:
|
||||||
overrides.app?.defaultHomePath ||
|
overrides.app?.defaultHomePath ||
|
||||||
preferences.app.defaultHomePath ||
|
preferences.app.defaultHomePath ||
|
||||||
'/dashboard',
|
'/page-render/main_home',
|
||||||
enablePreferences: overrides.app?.enablePreferences ?? true,
|
enablePreferences: overrides.app?.enablePreferences ?? true,
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
@@ -233,7 +233,7 @@ async function loadConfig() {
|
|||||||
|
|
||||||
// 获取带子应用前缀的首页路径
|
// 获取带子应用前缀的首页路径
|
||||||
function getDefaultHomePathWithPrefix(): string {
|
function getDefaultHomePathWithPrefix(): string {
|
||||||
const path = appDefaultHomePath.value || '/dashboard';
|
const path = appDefaultHomePath.value || '/page-render/main_home';
|
||||||
const appCode = appContextStore.appCode;
|
const appCode = appContextStore.appCode;
|
||||||
// 如果是子应用模式且路径不包含子应用前缀,自动添加
|
// 如果是子应用模式且路径不包含子应用前缀,自动添加
|
||||||
if (appCode && !path.startsWith(`/app/${appCode}`)) {
|
if (appCode && !path.startsWith(`/app/${appCode}`)) {
|
||||||
|
|||||||
@@ -1,802 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import type {
|
|
||||||
AgentListItem,
|
|
||||||
KnowledgeBaseListItem,
|
|
||||||
ModelListItem,
|
|
||||||
WorkflowListItem,
|
|
||||||
WorkflowRunListItem,
|
|
||||||
} from '#/api/ai-platform/ai-platform';
|
|
||||||
import type { UserAnnouncement } from '#/api/core/announcement';
|
|
||||||
|
|
||||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue';
|
|
||||||
import { useRouter } from 'vue-router';
|
|
||||||
|
|
||||||
import { Page } from '@vben/common-ui';
|
|
||||||
import {
|
|
||||||
Bot,
|
|
||||||
BrainCircuit,
|
|
||||||
CheckCircle2,
|
|
||||||
Cpu,
|
|
||||||
Database,
|
|
||||||
GitMerge,
|
|
||||||
HardDrive,
|
|
||||||
LayoutDashboard,
|
|
||||||
Mail,
|
|
||||||
Megaphone,
|
|
||||||
MessageSquare,
|
|
||||||
Network,
|
|
||||||
Plus,
|
|
||||||
RefreshCw,
|
|
||||||
Server,
|
|
||||||
ShieldCheck,
|
|
||||||
Sparkles,
|
|
||||||
Settings,
|
|
||||||
Workflow,
|
|
||||||
} from '@vben/icons';
|
|
||||||
import { useUserStore } from '@vben/stores';
|
|
||||||
|
|
||||||
import {
|
|
||||||
ElButton,
|
|
||||||
ElCard,
|
|
||||||
ElEmpty,
|
|
||||||
ElProgress,
|
|
||||||
ElSkeleton,
|
|
||||||
ElTag,
|
|
||||||
} from 'element-plus';
|
|
||||||
|
|
||||||
import {
|
|
||||||
getAgentListApi,
|
|
||||||
getAllWorkflowRunsApi,
|
|
||||||
getKnowledgeBaseListApi,
|
|
||||||
getModelListApi,
|
|
||||||
getWorkflowListApi,
|
|
||||||
} from '#/api/ai-platform/ai-platform';
|
|
||||||
import { getUserAnnouncementListApi } from '#/api/core/announcement';
|
|
||||||
import { getUnreadCountApi } from '#/api/core/message';
|
|
||||||
import { getServerHealthRealtimeApi } from '#/api/core/server-health';
|
|
||||||
|
|
||||||
defineOptions({ name: 'ControlCenterDashboard' });
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const userStore = useUserStore();
|
|
||||||
|
|
||||||
const loading = ref(true);
|
|
||||||
const refreshing = ref(false);
|
|
||||||
const now = ref(new Date());
|
|
||||||
|
|
||||||
const agents = ref<AgentListItem[]>([]);
|
|
||||||
const workflows = ref<WorkflowListItem[]>([]);
|
|
||||||
const knowledgeBases = ref<KnowledgeBaseListItem[]>([]);
|
|
||||||
const models = ref<ModelListItem[]>([]);
|
|
||||||
const waitingRuns = ref<WorkflowRunListItem[]>([]);
|
|
||||||
const announcements = ref<UserAnnouncement[]>([]);
|
|
||||||
const unreadMessages = ref(0);
|
|
||||||
const realtime = ref<null | {
|
|
||||||
cpu_percent: number;
|
|
||||||
disk_io?: { read_speed?: number; write_speed?: number };
|
|
||||||
memory_percent: number;
|
|
||||||
network_io?: { download_speed?: number; upload_speed?: number };
|
|
||||||
}>(null);
|
|
||||||
|
|
||||||
let clockTimer: ReturnType<typeof setInterval> | undefined;
|
|
||||||
|
|
||||||
const userName = computed(
|
|
||||||
() =>
|
|
||||||
userStore.userInfo?.realName ||
|
|
||||||
userStore.userInfo?.username ||
|
|
||||||
'管理员',
|
|
||||||
);
|
|
||||||
|
|
||||||
const greeting = computed(() => {
|
|
||||||
const hour = now.value.getHours();
|
|
||||||
if (hour < 6) return '夜深了';
|
|
||||||
if (hour < 11) return '早上好';
|
|
||||||
if (hour < 14) return '中午好';
|
|
||||||
if (hour < 18) return '下午好';
|
|
||||||
return '晚上好';
|
|
||||||
});
|
|
||||||
|
|
||||||
const todayText = computed(() =>
|
|
||||||
new Intl.DateTimeFormat('zh-CN', {
|
|
||||||
dateStyle: 'full',
|
|
||||||
}).format(now.value),
|
|
||||||
);
|
|
||||||
|
|
||||||
const timeText = computed(() =>
|
|
||||||
new Intl.DateTimeFormat('zh-CN', {
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit',
|
|
||||||
second: '2-digit',
|
|
||||||
}).format(now.value),
|
|
||||||
);
|
|
||||||
|
|
||||||
const aiCards = computed(() => [
|
|
||||||
{
|
|
||||||
action: '创建智能体',
|
|
||||||
count: agents.value.length,
|
|
||||||
desc: '角色、工具、知识与协作策略',
|
|
||||||
icon: Bot,
|
|
||||||
path: '/ai-platform/agent',
|
|
||||||
title: '智能体',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
action: '设计流程',
|
|
||||||
count: workflows.value.length,
|
|
||||||
desc: 'LLM 节点、条件分支和人机确认',
|
|
||||||
icon: Workflow,
|
|
||||||
path: '/ai-platform/workflow',
|
|
||||||
title: '流程编排',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
action: '维护知识',
|
|
||||||
count: knowledgeBases.value.length,
|
|
||||||
desc: '文档索引、切片检索和问答标注',
|
|
||||||
icon: Database,
|
|
||||||
path: '/ai-platform/knowledge-base',
|
|
||||||
title: '知识库',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
action: '开始协作',
|
|
||||||
count: waitingRuns.value.length,
|
|
||||||
desc: 'Codex 与多角色智能体协同处理',
|
|
||||||
icon: BrainCircuit,
|
|
||||||
path: '/agent-chat/codex',
|
|
||||||
title: 'Codex 协作',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
action: '配置 LLM',
|
|
||||||
count: models.value.length,
|
|
||||||
desc: 'Provider、聊天模型、向量模型与 Rerank',
|
|
||||||
icon: Settings,
|
|
||||||
path: '/ai-platform/model',
|
|
||||||
title: '模型配置',
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const opsCards = computed(() => [
|
|
||||||
{
|
|
||||||
label: '未读消息',
|
|
||||||
path: '/message/list',
|
|
||||||
value: unreadMessages.value,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '待确认流程',
|
|
||||||
path: '/ai-confirmation-center',
|
|
||||||
value: waitingRuns.value.length,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: '已发布智能体',
|
|
||||||
path: '/ai-platform/agent',
|
|
||||||
value: agents.value.filter((item) => item.status === 'published').length,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
function goto(path: string) {
|
|
||||||
router.push(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatBytes(value?: number) {
|
|
||||||
if (!value) return '0 B/s';
|
|
||||||
const units = ['B/s', 'KB/s', 'MB/s', 'GB/s'];
|
|
||||||
let size = value;
|
|
||||||
let index = 0;
|
|
||||||
while (size >= 1024 && index < units.length - 1) {
|
|
||||||
size /= 1024;
|
|
||||||
index += 1;
|
|
||||||
}
|
|
||||||
return `${size.toFixed(size >= 10 || index === 0 ? 0 : 1)} ${units[index]}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function refreshData() {
|
|
||||||
refreshing.value = true;
|
|
||||||
try {
|
|
||||||
const [
|
|
||||||
agentResult,
|
|
||||||
workflowResult,
|
|
||||||
knowledgeResult,
|
|
||||||
modelResult,
|
|
||||||
waitingResult,
|
|
||||||
announcementResult,
|
|
||||||
unreadResult,
|
|
||||||
realtimeResult,
|
|
||||||
] = await Promise.allSettled([
|
|
||||||
getAgentListApi({ page: 1, pageSize: 12 }),
|
|
||||||
getWorkflowListApi({ page: 1, pageSize: 12 }),
|
|
||||||
getKnowledgeBaseListApi({ page: 1, pageSize: 12 }),
|
|
||||||
getModelListApi({ page: 1, pageSize: 12 }),
|
|
||||||
getAllWorkflowRunsApi({ page: 1, pageSize: 8, status: 'waiting' }),
|
|
||||||
getUserAnnouncementListApi({ page: 1, pageSize: 5 }),
|
|
||||||
getUnreadCountApi(),
|
|
||||||
getServerHealthRealtimeApi(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
if (agentResult.status === 'fulfilled') {
|
|
||||||
agents.value = agentResult.value.items || [];
|
|
||||||
}
|
|
||||||
if (workflowResult.status === 'fulfilled') {
|
|
||||||
workflows.value = workflowResult.value.items || [];
|
|
||||||
}
|
|
||||||
if (knowledgeResult.status === 'fulfilled') {
|
|
||||||
knowledgeBases.value = knowledgeResult.value.items || [];
|
|
||||||
}
|
|
||||||
if (modelResult.status === 'fulfilled') {
|
|
||||||
models.value = modelResult.value.items || [];
|
|
||||||
}
|
|
||||||
if (waitingResult.status === 'fulfilled') {
|
|
||||||
waitingRuns.value = waitingResult.value.items || [];
|
|
||||||
}
|
|
||||||
if (announcementResult.status === 'fulfilled') {
|
|
||||||
announcements.value = announcementResult.value.items || [];
|
|
||||||
}
|
|
||||||
if (unreadResult.status === 'fulfilled') {
|
|
||||||
unreadMessages.value = unreadResult.value.total || 0;
|
|
||||||
}
|
|
||||||
if (realtimeResult.status === 'fulfilled') {
|
|
||||||
realtime.value = realtimeResult.value;
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
loading.value = false;
|
|
||||||
refreshing.value = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
refreshData();
|
|
||||||
clockTimer = setInterval(() => {
|
|
||||||
now.value = new Date();
|
|
||||||
}, 1000);
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
if (clockTimer) clearInterval(clockTimer);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<Page auto-content-height class="control-center">
|
|
||||||
<template #title>
|
|
||||||
<div class="flex items-center justify-between gap-3">
|
|
||||||
<div>
|
|
||||||
<div class="flex items-center gap-2 text-base font-semibold">
|
|
||||||
<LayoutDashboard class="h-5 w-5 text-[var(--el-color-primary)]" />
|
|
||||||
控制中心
|
|
||||||
</div>
|
|
||||||
<div class="mt-1 text-xs text-[var(--el-text-color-secondary)]">
|
|
||||||
AI Agent Admin 轻量工作台
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ElButton :loading="refreshing" :icon="RefreshCw" @click="refreshData">
|
|
||||||
刷新
|
|
||||||
</ElButton>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<ElSkeleton v-if="loading" animated :rows="8" />
|
|
||||||
|
|
||||||
<div v-else class="dashboard-grid">
|
|
||||||
<section class="welcome-panel">
|
|
||||||
<div class="welcome-copy">
|
|
||||||
<div class="avatar">{{ userName.slice(0, 1) }}</div>
|
|
||||||
<div>
|
|
||||||
<h2>{{ greeting }},{{ userName }}</h2>
|
|
||||||
<p>聚焦智能体、流程编排、知识检索和 Codex 协作。</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="time-panel">
|
|
||||||
<strong>{{ timeText }}</strong>
|
|
||||||
<span>{{ todayText }}</span>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="ai-entry-grid">
|
|
||||||
<ElCard
|
|
||||||
v-for="item in aiCards"
|
|
||||||
:key="item.title"
|
|
||||||
shadow="hover"
|
|
||||||
class="entry-card"
|
|
||||||
:body-style="{ padding: '16px' }"
|
|
||||||
@click="goto(item.path)"
|
|
||||||
>
|
|
||||||
<div class="entry-head">
|
|
||||||
<div class="entry-icon">
|
|
||||||
<component :is="item.icon" class="h-5 w-5" />
|
|
||||||
</div>
|
|
||||||
<ElButton link type="primary" @click.stop="goto(item.path)">
|
|
||||||
{{ item.action }}
|
|
||||||
</ElButton>
|
|
||||||
</div>
|
|
||||||
<div class="entry-title">{{ item.title }}</div>
|
|
||||||
<div class="entry-desc">{{ item.desc }}</div>
|
|
||||||
<div class="entry-count">{{ item.count }}</div>
|
|
||||||
</ElCard>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="main-column">
|
|
||||||
<ElCard shadow="never" class="panel-card">
|
|
||||||
<template #header>
|
|
||||||
<div class="panel-title">
|
|
||||||
<Sparkles class="h-4 w-4" />
|
|
||||||
AI 协作状态
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="ops-grid">
|
|
||||||
<button
|
|
||||||
v-for="item in opsCards"
|
|
||||||
:key="item.label"
|
|
||||||
class="ops-item"
|
|
||||||
type="button"
|
|
||||||
@click="goto(item.path)"
|
|
||||||
>
|
|
||||||
<span>{{ item.label }}</span>
|
|
||||||
<strong>{{ item.value }}</strong>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="agent-list">
|
|
||||||
<div
|
|
||||||
v-for="agent in agents.slice(0, 6)"
|
|
||||||
:key="agent.id"
|
|
||||||
class="agent-row"
|
|
||||||
@click="goto('/ai-platform/agent')"
|
|
||||||
>
|
|
||||||
<Bot class="h-4 w-4" />
|
|
||||||
<div>
|
|
||||||
<strong>{{ agent.name }}</strong>
|
|
||||||
<span>{{ agent.description || agent.code }}</span>
|
|
||||||
</div>
|
|
||||||
<ElTag
|
|
||||||
size="small"
|
|
||||||
:type="agent.status === 'published' ? 'success' : 'info'"
|
|
||||||
>
|
|
||||||
{{ agent.status === 'published' ? '已发布' : '草稿' }}
|
|
||||||
</ElTag>
|
|
||||||
</div>
|
|
||||||
<ElEmpty
|
|
||||||
v-if="agents.length === 0"
|
|
||||||
description="暂无智能体"
|
|
||||||
:image-size="80"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</ElCard>
|
|
||||||
|
|
||||||
<ElCard shadow="never" class="panel-card">
|
|
||||||
<template #header>
|
|
||||||
<div class="panel-title">
|
|
||||||
<GitMerge class="h-4 w-4" />
|
|
||||||
待处理流程
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div
|
|
||||||
v-for="run in waitingRuns"
|
|
||||||
:key="run.id"
|
|
||||||
class="run-row"
|
|
||||||
@click="goto('/ai-confirmation-center')"
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<strong>{{ run.workflow_name }}</strong>
|
|
||||||
<span>{{ run.trigger_type || 'workflow' }}</span>
|
|
||||||
</div>
|
|
||||||
<ElTag type="warning" size="small">等待确认</ElTag>
|
|
||||||
</div>
|
|
||||||
<ElEmpty
|
|
||||||
v-if="waitingRuns.length === 0"
|
|
||||||
description="暂无待确认流程"
|
|
||||||
:image-size="80"
|
|
||||||
/>
|
|
||||||
</ElCard>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<aside class="side-column">
|
|
||||||
<ElCard shadow="never" class="panel-card">
|
|
||||||
<template #header>
|
|
||||||
<div class="panel-title">
|
|
||||||
<Server class="h-4 w-4" />
|
|
||||||
服务器信息
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="health-list">
|
|
||||||
<div class="health-item">
|
|
||||||
<div>
|
|
||||||
<Cpu class="h-4 w-4" />
|
|
||||||
<span>CPU</span>
|
|
||||||
</div>
|
|
||||||
<ElProgress
|
|
||||||
:percentage="Math.round(realtime?.cpu_percent || 0)"
|
|
||||||
:stroke-width="8"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="health-item">
|
|
||||||
<div>
|
|
||||||
<ShieldCheck class="h-4 w-4" />
|
|
||||||
<span>内存</span>
|
|
||||||
</div>
|
|
||||||
<ElProgress
|
|
||||||
:percentage="Math.round(realtime?.memory_percent || 0)"
|
|
||||||
:stroke-width="8"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="io-row">
|
|
||||||
<HardDrive class="h-4 w-4" />
|
|
||||||
<span>磁盘写入</span>
|
|
||||||
<strong>{{ formatBytes(realtime?.disk_io?.write_speed) }}</strong>
|
|
||||||
</div>
|
|
||||||
<div class="io-row">
|
|
||||||
<Network class="h-4 w-4" />
|
|
||||||
<span>网络下载</span>
|
|
||||||
<strong>{{
|
|
||||||
formatBytes(realtime?.network_io?.download_speed)
|
|
||||||
}}</strong>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ElCard>
|
|
||||||
|
|
||||||
<ElCard shadow="never" class="panel-card">
|
|
||||||
<template #header>
|
|
||||||
<div class="panel-title">
|
|
||||||
<Megaphone class="h-4 w-4" />
|
|
||||||
最新公告
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div
|
|
||||||
v-for="item in announcements"
|
|
||||||
:key="item.id"
|
|
||||||
class="notice-row"
|
|
||||||
@click="goto('/message/announcement-list')"
|
|
||||||
>
|
|
||||||
<Mail class="h-4 w-4" />
|
|
||||||
<div>
|
|
||||||
<strong>{{ item.title }}</strong>
|
|
||||||
<span>{{ item.summary || item.publisher_name }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ElEmpty
|
|
||||||
v-if="announcements.length === 0"
|
|
||||||
description="暂无公告"
|
|
||||||
:image-size="80"
|
|
||||||
/>
|
|
||||||
</ElCard>
|
|
||||||
|
|
||||||
<ElCard shadow="never" class="panel-card">
|
|
||||||
<template #header>
|
|
||||||
<div class="panel-title">
|
|
||||||
<MessageSquare class="h-4 w-4" />
|
|
||||||
快捷入口
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div class="quick-actions">
|
|
||||||
<ElButton type="primary" :icon="Plus" @click="goto('/ai-platform/agent')">
|
|
||||||
创建智能体
|
|
||||||
</ElButton>
|
|
||||||
<ElButton :icon="Workflow" @click="goto('/ai-platform/workflow')">
|
|
||||||
流程编排
|
|
||||||
</ElButton>
|
|
||||||
<ElButton :icon="Settings" @click="goto('/ai-platform/model')">
|
|
||||||
模型配置
|
|
||||||
</ElButton>
|
|
||||||
<ElButton :icon="Database" @click="goto('/ai-platform/knowledge-base')">
|
|
||||||
知识库
|
|
||||||
</ElButton>
|
|
||||||
<ElButton @click="goto('/system/user')">用户管理</ElButton>
|
|
||||||
<ElButton @click="goto('/system/role')">角色权限</ElButton>
|
|
||||||
</div>
|
|
||||||
</ElCard>
|
|
||||||
</aside>
|
|
||||||
|
|
||||||
<section class="knowledge-strip">
|
|
||||||
<CheckCircle2 class="h-4 w-4" />
|
|
||||||
<span>
|
|
||||||
当前只保留基础管理与 AI 协作主线,在线开发、应用搭建、组织扩展等重型入口已从主流程移除。
|
|
||||||
</span>
|
|
||||||
</section>
|
|
||||||
</div>
|
|
||||||
</Page>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.control-center {
|
|
||||||
--dashboard-border: var(--el-border-color-lighter);
|
|
||||||
--dashboard-soft: var(--el-fill-color-lighter);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dashboard-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: minmax(0, 1fr) 360px;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-panel,
|
|
||||||
.ai-entry-grid,
|
|
||||||
.knowledge-strip {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-panel {
|
|
||||||
display: flex;
|
|
||||||
min-height: 112px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 16px;
|
|
||||||
border: 1px solid var(--dashboard-border);
|
|
||||||
border-radius: 8px;
|
|
||||||
background: linear-gradient(
|
|
||||||
135deg,
|
|
||||||
rgba(64, 158, 255, 0.1),
|
|
||||||
rgba(103, 194, 58, 0.08)
|
|
||||||
);
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-copy {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar {
|
|
||||||
display: flex;
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: var(--el-color-primary);
|
|
||||||
color: white;
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-copy h2 {
|
|
||||||
margin: 0;
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: 650;
|
|
||||||
}
|
|
||||||
|
|
||||||
.welcome-copy p,
|
|
||||||
.time-panel span {
|
|
||||||
margin: 6px 0 0;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.time-panel {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
|
||||||
.time-panel strong {
|
|
||||||
display: block;
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-entry-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entry-card {
|
|
||||||
cursor: pointer;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entry-head,
|
|
||||||
.panel-title,
|
|
||||||
.io-row,
|
|
||||||
.notice-row,
|
|
||||||
.run-row,
|
|
||||||
.agent-row,
|
|
||||||
.health-item > div {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entry-head {
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entry-icon {
|
|
||||||
display: flex;
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 8px;
|
|
||||||
background: rgba(64, 158, 255, 0.12);
|
|
||||||
color: var(--el-color-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.entry-title {
|
|
||||||
margin-top: 12px;
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 650;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entry-desc {
|
|
||||||
min-height: 36px;
|
|
||||||
margin-top: 6px;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
font-size: 12px;
|
|
||||||
line-height: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.entry-count {
|
|
||||||
margin-top: 12px;
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.main-column,
|
|
||||||
.side-column {
|
|
||||||
display: flex;
|
|
||||||
min-width: 0;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-card {
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.panel-title {
|
|
||||||
gap: 8px;
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: 650;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ops-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
gap: 10px;
|
|
||||||
margin-bottom: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ops-item {
|
|
||||||
display: flex;
|
|
||||||
min-height: 72px;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
border: 1px solid var(--dashboard-border);
|
|
||||||
border-radius: 8px;
|
|
||||||
background: var(--dashboard-soft);
|
|
||||||
text-align: left;
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ops-item span,
|
|
||||||
.agent-row span,
|
|
||||||
.run-row span,
|
|
||||||
.notice-row span {
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ops-item strong {
|
|
||||||
margin-top: 6px;
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
font-size: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.agent-list,
|
|
||||||
.health-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.agent-row,
|
|
||||||
.run-row,
|
|
||||||
.notice-row {
|
|
||||||
gap: 10px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
border-radius: 8px;
|
|
||||||
padding: 10px;
|
|
||||||
transition: background-color 0.16s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.agent-row:hover,
|
|
||||||
.run-row:hover,
|
|
||||||
.notice-row:hover {
|
|
||||||
background: var(--el-fill-color-light);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.agent-row > div,
|
|
||||||
.notice-row > div,
|
|
||||||
.run-row > div {
|
|
||||||
min-width: 0;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.agent-row strong,
|
|
||||||
.notice-row strong,
|
|
||||||
.run-row strong {
|
|
||||||
display: block;
|
|
||||||
overflow: hidden;
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
font-size: 13px;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.health-item {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: 72px minmax(0, 1fr);
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.health-item > div,
|
|
||||||
.io-row {
|
|
||||||
gap: 8px;
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
font-size: 13px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.io-row {
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.io-row span {
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.io-row strong {
|
|
||||||
color: var(--el-text-color-primary);
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-actions {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.quick-actions :deep(.el-button) {
|
|
||||||
width: 100%;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.knowledge-strip {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
border: 1px solid var(--dashboard-border);
|
|
||||||
border-radius: 8px;
|
|
||||||
background: var(--el-fill-color-blank);
|
|
||||||
color: var(--el-text-color-secondary);
|
|
||||||
padding: 12px 14px;
|
|
||||||
font-size: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1200px) {
|
|
||||||
.dashboard-grid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-entry-grid {
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
|
||||||
.welcome-panel,
|
|
||||||
.welcome-copy {
|
|
||||||
align-items: flex-start;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.time-panel {
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ai-entry-grid,
|
|
||||||
.ops-grid,
|
|
||||||
.quick-actions {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -15,7 +15,15 @@ const pageConfig = ref<string>('');
|
|||||||
const pageName = ref('');
|
const pageName = ref('');
|
||||||
const pageCode = ref('');
|
const pageCode = ref('');
|
||||||
|
|
||||||
const MAIN_HOME_AI_LINKS = [
|
type QuickLink = null | {
|
||||||
|
bgColor: string;
|
||||||
|
icon: string;
|
||||||
|
id: string;
|
||||||
|
path: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const MAIN_HOME_AI_LINKS: QuickLink[] = [
|
||||||
{
|
{
|
||||||
bgColor: '',
|
bgColor: '',
|
||||||
icon: 'lucide:square-user',
|
icon: 'lucide:square-user',
|
||||||
@@ -37,6 +45,13 @@ const MAIN_HOME_AI_LINKS = [
|
|||||||
path: '/ai-platform/knowledge-base',
|
path: '/ai-platform/knowledge-base',
|
||||||
title: '知识库',
|
title: '知识库',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
bgColor: '',
|
||||||
|
icon: 'lucide:settings',
|
||||||
|
id: 'ai-model',
|
||||||
|
path: '/ai-platform/model',
|
||||||
|
title: '模型配置',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
bgColor: '',
|
bgColor: '',
|
||||||
icon: 'lucide:code',
|
icon: 'lucide:code',
|
||||||
@@ -45,11 +60,72 @@ const MAIN_HOME_AI_LINKS = [
|
|||||||
title: 'Codex',
|
title: 'Codex',
|
||||||
},
|
},
|
||||||
null,
|
null,
|
||||||
null,
|
|
||||||
null,
|
|
||||||
null,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const MAIN_HOME_ADMIN_LINKS: QuickLink[] = [
|
||||||
|
{
|
||||||
|
bgColor: '',
|
||||||
|
icon: 'lucide:users',
|
||||||
|
id: 'system-user',
|
||||||
|
path: '/system/user',
|
||||||
|
title: '用户管理',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bgColor: '',
|
||||||
|
icon: 'lucide:shield-check',
|
||||||
|
id: 'system-role',
|
||||||
|
path: '/system/role',
|
||||||
|
title: '角色权限',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bgColor: '',
|
||||||
|
icon: 'lucide:key-round',
|
||||||
|
id: 'system-permission',
|
||||||
|
path: '/system/new-permission',
|
||||||
|
title: '权限管理',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bgColor: '',
|
||||||
|
icon: 'lucide:menu',
|
||||||
|
id: 'system-menu',
|
||||||
|
path: '/system/menu',
|
||||||
|
title: '菜单管理',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bgColor: '',
|
||||||
|
icon: 'lucide:megaphone',
|
||||||
|
id: 'announcement',
|
||||||
|
path: '/message/announcement',
|
||||||
|
title: '公告管理',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
bgColor: '',
|
||||||
|
icon: 'lucide:message-square-text',
|
||||||
|
id: 'message-list',
|
||||||
|
path: '/message/list',
|
||||||
|
title: '消息中心',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function toQuickLinksWidget(
|
||||||
|
widget: Record<string, any>,
|
||||||
|
title: string,
|
||||||
|
menus: QuickLink[],
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
...widget,
|
||||||
|
title,
|
||||||
|
type: 'quick-links',
|
||||||
|
props: {
|
||||||
|
...widget.props,
|
||||||
|
columns: 3,
|
||||||
|
menus,
|
||||||
|
rows: 2,
|
||||||
|
title,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeMainHomeConfig(config: Record<string, any>) {
|
function normalizeMainHomeConfig(config: Record<string, any>) {
|
||||||
if (pageCode.value !== 'main_home' || !Array.isArray(config.widgets)) {
|
if (pageCode.value !== 'main_home' || !Array.isArray(config.widgets)) {
|
||||||
return config;
|
return config;
|
||||||
@@ -58,32 +134,29 @@ function normalizeMainHomeConfig(config: Record<string, any>) {
|
|||||||
return {
|
return {
|
||||||
...config,
|
...config,
|
||||||
widgets: config.widgets.map((widget: Record<string, any>) => {
|
widgets: config.widgets.map((widget: Record<string, any>) => {
|
||||||
if (widget.type !== 'quick-links') return widget;
|
if (widget.type === 'approval-center') {
|
||||||
|
return toQuickLinksWidget(widget, 'AI 协作', MAIN_HOME_AI_LINKS);
|
||||||
return {
|
}
|
||||||
...widget,
|
if (widget.type === 'my-apps') {
|
||||||
props: {
|
return toQuickLinksWidget(widget, '基础管理', MAIN_HOME_ADMIN_LINKS);
|
||||||
...widget.props,
|
}
|
||||||
menus: MAIN_HOME_AI_LINKS,
|
if (widget.type === 'quick-links') {
|
||||||
},
|
return toQuickLinksWidget(widget, '快捷入口', MAIN_HOME_AI_LINKS);
|
||||||
};
|
}
|
||||||
|
return widget;
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取页面编码
|
|
||||||
function getPageCode(): string {
|
function getPageCode(): string {
|
||||||
// 优先从 query 获取
|
|
||||||
if (route.query.pageCode) {
|
if (route.query.pageCode) {
|
||||||
return route.query.pageCode as string;
|
return route.query.pageCode as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 其次从 params 获取
|
|
||||||
if (route.params.code) {
|
if (route.params.code) {
|
||||||
return route.params.code as string;
|
return route.params.code as string;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 最后从路径中提取(路径格式:/page-render/xxx)
|
|
||||||
const pathParts = route.path.split('/').filter(Boolean);
|
const pathParts = route.path.split('/').filter(Boolean);
|
||||||
const length = pathParts.length;
|
const length = pathParts.length;
|
||||||
if (length >= 2 && pathParts[length - 2] === 'page-render') {
|
if (length >= 2 && pathParts[length - 2] === 'page-render') {
|
||||||
@@ -93,7 +166,6 @@ function getPageCode(): string {
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载页面数据
|
|
||||||
async function loadPageData() {
|
async function loadPageData() {
|
||||||
const code = getPageCode();
|
const code = getPageCode();
|
||||||
if (!code) {
|
if (!code) {
|
||||||
@@ -123,7 +195,6 @@ onMounted(() => {
|
|||||||
loadPageData();
|
loadPageData();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 监听路由变化
|
|
||||||
watch(
|
watch(
|
||||||
() => route.fullPath,
|
() => route.fullPath,
|
||||||
() => {
|
() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user