Files
vue3-element-admin/src/views/workspace/apps/index.vue
Ray.Hao f443eb6bbe feat: 状态展示统一为圆点
- 新增状态圆点组件,用户管理、角色与字典的状态改用圆点
- 用户管理操作列放宽到 200,登录页移动端版本号紧随项目名
- 应用中心分组按内容高度排列
- 函数注释统一为展开式多行
- 格式校验脚本忽略不存在的目录
2026-10-29 08:13:50 +08:00

373 lines
8.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- 应用中心 -->
<template>
<div class="page-container">
<el-card class="page-content" shadow="never">
<section v-for="group in appGroups" :key="group.category.key" class="app-section">
<header class="app-section__header">
<span class="app-section__name">{{ group.category.name }}</span>
<span class="app-section__count">{{ group.apps.length }}</span>
<span class="app-section__desc">{{ group.category.description }}</span>
</header>
<div class="app-grid">
<div
v-for="app in group.apps"
:key="app.name"
class="app-card"
:class="[`app-card--${app.theme}`, { 'is-disabled': !app.ssoReady && !app.url }]"
@click="handleAppClick(app)"
>
<div class="app-card__icon">
<el-icon :size="22">
<component :is="app.icon" />
</el-icon>
</div>
<div class="app-card__body">
<div class="app-card__name">{{ app.name }}</div>
<div class="app-card__desc">{{ app.description }}</div>
</div>
<el-icon v-if="app.ssoReady || app.url" class="app-card__arrow">
<ArrowRight />
</el-icon>
</div>
</div>
</section>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ArrowRight } from "@element-plus/icons-vue";
import { ElMessage } from "element-plus";
import { useRouter } from "vue-router";
import { OIDC_AUTHORIZATION_URL } from "@/api/auth";
import { appConfig } from "@/settings";
defineOptions({ name: "AppCenter" });
/**
* 应用分类标识
*/
type CategoryKey = "business" | "smart" | "account";
/**
* 应用分类
*/
interface AppCategory {
key: CategoryKey;
name: string;
description: string;
}
/**
* 应用卡片
*/
interface AppCard {
/** 应用名称 */
name: string;
/** 一句话说明 */
description: string;
/** 入口地址:站内路径走路由跳转,http(s) 地址新标签打开 */
url: string;
/** Element Plus 图标组件名 */
icon: string;
/** 卡片主题色,对应 Element Plus 的状态色 */
theme: "primary" | "success" | "warning" | "info";
/** 是否已接入统一认证;未接入且没有入口的卡片置灰不可跳转 */
ssoReady: boolean;
/** 所属分类 */
category: CategoryKey;
}
/** 分类清单,数组顺序即页面分组顺序 */
const categories: AppCategory[] = [
{
key: "business",
name: "业务系统",
description: "已上线的系统,账号与权限由认证中心下发",
},
{
key: "smart",
name: "智能与多端",
description: "智能助手与移动端、桌面端客户端",
},
{
key: "account",
name: "我的账号",
description: "账号与统一认证身份的关联",
},
];
/** 应用清单:接入统一认证的应用补上 url 并置 ssoReady 为 true */
const apps: AppCard[] = [
{
name: "youlai-admin",
description: "Vue3 + Element Plus + 多语言中后台管理系统",
url: OIDC_AUTHORIZATION_URL,
icon: "Monitor",
theme: "primary",
ssoReady: true,
category: "business",
},
{
name: "youlai-mall",
description: "Spring Cloud 微服务商城,覆盖商品、订单与支付",
url: "",
icon: "ShoppingCart",
theme: "success",
ssoReady: false,
category: "business",
},
{
name: "youlai-cms",
description: "内容管理系统,模型与字段可自定义,管理栏目、标签和媒体素材",
url: "",
icon: "Document",
theme: "warning",
ssoReady: false,
category: "business",
},
{
name: "youlai-ai",
description: "基于 Spring AI Alibaba,覆盖 RAG、工作流与多智能体",
url: "",
icon: "MagicStick",
theme: "info",
ssoReady: false,
category: "smart",
},
{
name: "youlai-app",
description: "uni-app 跨平台移动端,出差在外也能处理待办",
url: "",
icon: "Cellphone",
theme: "success",
ssoReady: false,
category: "smart",
},
{
name: "youlai-electron",
description: "Electron 桌面客户端,离线也能打开常用功能",
url: "",
icon: "Platform",
theme: "info",
ssoReady: false,
category: "smart",
},
{
name: "认证身份",
description: "绑定统一认证身份,单点登录自动匹配",
url: "/profile?tab=identity",
icon: "Link",
theme: "primary",
ssoReady: false,
category: "account",
},
];
const router = useRouter();
const appGroups = categories
.map((category) => ({ category, apps: apps.filter((app) => app.category === category.key) }))
// 未启用统一认证时不展示账号分组
.filter(
(group) => group.apps.length > 0 && (appConfig.oidcEnabled || group.category.key !== "account")
);
// 站内路径走路由跳转,外部地址新标签打开
function handleAppClick(app: AppCard): void {
// 未接入统一认证且没有入口的应用只作展示
if (!app.ssoReady && !app.url) {
ElMessage.info(`${app.name} 尚未接入统一认证`);
return;
}
if (/^https?:\/\//.test(app.url)) {
window.open(app.url, "_blank", "noopener");
return;
}
// 统一认证授权地址需要整页跳转,由后端重定向到认证中心
if (app.url.startsWith("/oauth2/")) {
window.location.href = app.url;
return;
}
router.push(app.url);
}
</script>
<style scoped lang="scss">
/* 分组按内容高度排列,不占用页面剩余高度 */
.page-content :deep(.el-card__body) {
grid-template-rows: none;
grid-auto-rows: min-content;
align-content: start;
}
.app-section + .app-section {
padding-top: 24px;
margin-top: 24px;
border-top: 1px solid var(--el-border-color-extra-light);
}
.app-section__header {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: baseline;
margin-bottom: 16px;
}
.app-section__name {
position: relative;
padding-left: 10px;
font-size: 15px;
font-weight: 600;
color: var(--el-text-color-primary);
&::before {
position: absolute;
top: 50%;
left: 0;
width: 3px;
height: 14px;
content: "";
background: var(--el-color-primary);
border-radius: 2px;
transform: translateY(-50%);
}
}
.app-section__count {
padding: 0 6px;
font-size: 12px;
line-height: 16px;
color: var(--el-text-color-secondary);
background: var(--el-fill-color-light);
border-radius: 8px;
}
.app-section__desc {
font-size: 12px;
color: var(--el-text-color-secondary);
}
.app-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 14px;
}
.app-card {
display: flex;
gap: 14px;
align-items: flex-start;
padding: 16px;
cursor: pointer;
background: var(--el-bg-color);
border: 1px solid var(--el-border-color-lighter);
border-radius: 10px;
transition:
border-color 0.2s,
box-shadow 0.2s,
transform 0.2s;
&:hover {
border-color: var(--app-border);
box-shadow: 0 6px 16px rgb(0 0 0 / 6%);
transform: translateY(-2px);
.app-card__arrow {
color: var(--app-color);
transform: translateX(2px);
}
}
&.is-disabled {
cursor: not-allowed;
.app-card__icon {
opacity: 0.75;
filter: saturate(0.7);
}
.app-card__name {
color: var(--el-text-color-regular);
}
&:hover {
border-color: var(--el-border-color-lighter);
box-shadow: none;
transform: none;
}
}
&__icon {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 44px;
height: 44px;
color: var(--app-color);
background: var(--app-bg);
border-radius: 10px;
}
&__body {
flex: 1;
min-width: 0;
}
&__name {
font-size: 15px;
font-weight: 600;
line-height: 22px;
color: var(--el-text-color-primary);
}
&__desc {
display: -webkit-box;
margin-top: 6px;
overflow: hidden;
-webkit-line-clamp: 2;
font-size: 12px;
line-height: 1.7;
color: var(--el-text-color-secondary);
-webkit-box-orient: vertical;
}
&__arrow {
flex-shrink: 0;
align-self: center;
font-size: 14px;
color: var(--el-text-color-placeholder);
transition:
color 0.2s,
transform 0.2s;
}
}
.app-card--primary {
--app-color: var(--el-color-primary);
--app-bg: var(--el-color-primary-light-9);
--app-border: var(--el-color-primary-light-5);
}
.app-card--success {
--app-color: var(--el-color-success);
--app-bg: var(--el-color-success-light-9);
--app-border: var(--el-color-success-light-5);
}
.app-card--warning {
--app-color: var(--el-color-warning);
--app-bg: var(--el-color-warning-light-9);
--app-border: var(--el-color-warning-light-5);
}
.app-card--info {
--app-color: var(--el-color-info);
--app-bg: var(--el-color-info-light-9);
--app-border: var(--el-color-info-light-5);
}
</style>