feat(oidc): 接入统一认证登录与身份自助绑定

- 登录页接入统一认证入口并处理认证回调
- 个人中心新增认证身份区块,支持自助绑定与解绑
- 头像菜单与应用中心新增认证身份入口
- 应用中心删除经营管理分组,业务系统卡片指向统一认证授权地址
- 用户管理页移除认证身份代绑入口与弹窗
- 身份关联接口从用户域移到认证域
This commit is contained in:
Ray.Hao
2026-10-28 08:21:27 +08:00
parent 9cddb95fb7
commit b5bacd2cbf
205 changed files with 6705 additions and 7079 deletions

View File

@@ -4,11 +4,9 @@
<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">
<div class="app-section__title">
<span class="app-section__name">{{ group.category.name }}</span>
<span class="app-section__count">{{ group.apps.length }}</span>
</div>
<p class="app-section__desc">{{ group.category.description }}</p>
<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">
@@ -16,19 +14,21 @@
v-for="app in group.apps"
:key="app.name"
class="app-card"
:class="[`app-card--${app.theme}`, { 'is-disabled': !app.ssoReady }]"
:class="[`app-card--${app.theme}`, { 'is-disabled': !app.ssoReady && !app.url }]"
@click="handleAppClick(app)"
>
<div class="app-card__top">
<div class="app-card__icon">
<el-icon :size="26">
<component :is="app.icon" />
</el-icon>
</div>
<el-tag v-if="!app.ssoReady" type="info" size="small" effect="plain">即将接入</el-tag>
<div class="app-card__icon">
<el-icon :size="22">
<component :is="app.icon" />
</el-icon>
</div>
<div class="app-card__name">{{ app.name }}</div>
<div class="app-card__desc">{{ app.description }}</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>
@@ -37,22 +37,31 @@
</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" | "operation" | "smart";
/**
* 应用分类标识
*/
type CategoryKey = "business" | "smart" | "account";
/** 应用分类 */
/**
* 应用分类
*/
interface AppCategory {
key: CategoryKey;
name: string;
description: string;
}
/** 应用卡片 */
/**
* 应用卡片
*/
interface AppCard {
/** 应用名称 */
name: string;
@@ -64,7 +73,7 @@ interface AppCard {
icon: string;
/** 卡片主题色,对应 Element Plus 的状态色 */
theme: "primary" | "success" | "warning" | "info";
/** 是否已接入统一认证:未接入的卡片置灰不可跳转 */
/** 是否已接入统一认证;未接入且没有入口的卡片置灰不可跳转 */
ssoReady: boolean;
/** 所属分类 */
category: CategoryKey;
@@ -77,24 +86,24 @@ const categories: AppCategory[] = [
name: "业务系统",
description: "已上线的系统,账号与权限由认证中心下发",
},
{
key: "operation",
name: "经营管理",
description: "设备、资产、进销存等经营场景的系统",
},
{
key: "smart",
name: "智能与多端",
description: "智能助手与移动端、桌面端客户端",
},
{
key: "account",
name: "我的账号",
description: "账号与统一认证身份的关联",
},
];
/** 应用清单:接入统一认证后把 ssoReady 改为 true 并补上 url */
/** 应用清单:接入统一认证的应用补上 url 并置 ssoReady 为 true */
const apps: AppCard[] = [
{
name: "youlai-admin",
description: "Vue3 + Element Plus + 多语言中后台管理系统",
url: "/",
url: OIDC_AUTHORIZATION_URL,
icon: "Monitor",
theme: "primary",
ssoReady: true,
@@ -118,60 +127,6 @@ const apps: AppCard[] = [
ssoReady: false,
category: "business",
},
{
name: "设备管理",
description: "设备台账、点检保养与维修记录,备件出入库联动",
url: "",
icon: "Setting",
theme: "primary",
ssoReady: false,
category: "operation",
},
{
name: "资产管理",
description: "资产入库、领用调拨、折旧计提与定期盘点",
url: "",
icon: "Box",
theme: "success",
ssoReady: false,
category: "operation",
},
{
name: "进销存",
description: "采购、销售与库存管理,支持调拨和库存预警",
url: "",
icon: "Goods",
theme: "warning",
ssoReady: false,
category: "operation",
},
{
name: "客户关系",
description: "客户档案、商机跟进与合同回款",
url: "",
icon: "User",
theme: "info",
ssoReady: false,
category: "operation",
},
{
name: "工单中心",
description: "报修派单、处理时效与满意度回访",
url: "",
icon: "Tickets",
theme: "primary",
ssoReady: false,
category: "operation",
},
{
name: "项目管理",
description: "立项、任务分解、进度跟踪与工时统计",
url: "",
icon: "Files",
theme: "success",
ssoReady: false,
category: "operation",
},
{
name: "youlai-ai",
description: "基于 Spring AI Alibaba,覆盖 RAG、工作流与多智能体",
@@ -199,17 +154,30 @@ const apps: AppCard[] = [
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);
// 未启用统一认证时不展示账号分组
.filter(
(group) => group.apps.length > 0 && (appConfig.oidcEnabled || group.category.key !== "account")
);
// 站内路径走路由跳转,外部地址新标签打开
function handleAppClick(app: AppCard): void {
if (!app.ssoReady) {
// 未接入统一认证且没有入口的应用只作展示
if (!app.ssoReady && !app.url) {
ElMessage.info(`${app.name} 尚未接入统一认证`);
return;
}
@@ -217,25 +185,28 @@ function handleAppClick(app: AppCard): void {
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">
.app-section + .app-section {
padding-top: 26px;
margin-top: 26px;
border-top: 1px dashed var(--el-border-color-lighter);
padding-top: 24px;
margin-top: 24px;
border-top: 1px solid var(--el-border-color-extra-light);
}
.app-section__header {
margin-bottom: 16px;
}
.app-section__title {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
align-items: baseline;
margin-bottom: 16px;
}
.app-section__name {
@@ -259,61 +230,60 @@ function handleAppClick(app: AppCard): void {
}
.app-section__count {
padding: 0 7px;
padding: 0 6px;
font-size: 12px;
line-height: 18px;
line-height: 16px;
color: var(--el-text-color-secondary);
background: var(--el-fill-color-light);
border-radius: 9px;
border-radius: 8px;
}
.app-section__desc {
margin: 6px 0 0 10px;
font-size: 12px;
color: var(--el-text-color-secondary);
}
.app-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
gap: 16px;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 14px;
}
.app-card {
position: relative;
display: flex;
flex-direction: column;
padding: 20px;
overflow: hidden;
gap: 14px;
align-items: flex-start;
padding: 16px;
cursor: pointer;
background: linear-gradient(180deg, var(--app-bg) 0%, var(--el-bg-color) 72%);
background: var(--el-bg-color);
border: 1px solid var(--el-border-color-lighter);
border-radius: 12px;
border-radius: 10px;
transition:
border-color 0.2s,
box-shadow 0.2s,
transform 0.2s;
&::before {
position: absolute;
inset: 0 0 auto;
height: 3px;
content: "";
background: linear-gradient(90deg, var(--app-color), transparent);
}
&:hover {
border-color: var(--app-color);
box-shadow: 0 10px 24px rgb(0 0 0 / 8%);
transform: translateY(-3px);
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.85;
filter: grayscale(0.35);
opacity: 0.75;
filter: saturate(0.7);
}
.app-card__name {
color: var(--el-text-color-regular);
}
&:hover {
@@ -323,68 +293,73 @@ function handleAppClick(app: AppCard): void {
}
}
&__top {
display: flex;
align-items: flex-start;
justify-content: space-between;
}
&__icon {
display: flex;
flex-shrink: 0;
align-items: center;
justify-content: center;
width: 52px;
height: 52px;
color: #fff;
background: linear-gradient(135deg, var(--app-color), var(--app-color-light));
border-radius: 14px;
box-shadow: 0 6px 16px var(--app-shadow);
width: 44px;
height: 44px;
color: var(--app-color);
background: var(--app-bg);
border-radius: 10px;
}
&__body {
flex: 1;
min-width: 0;
}
&__name {
margin-top: 16px;
font-size: 16px;
font-size: 15px;
font-weight: 600;
line-height: 22px;
color: var(--el-text-color-primary);
}
&__desc {
display: -webkit-box;
margin-top: 8px;
margin-top: 6px;
overflow: hidden;
-webkit-line-clamp: 2;
font-size: 13px;
line-height: 1.6;
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-color-light: var(--el-color-primary-light-3);
--app-bg: var(--el-color-primary-light-9);
--app-shadow: rgb(64 158 255 / 30%);
--app-border: var(--el-color-primary-light-5);
}
.app-card--success {
--app-color: var(--el-color-success);
--app-color-light: var(--el-color-success-light-3);
--app-bg: var(--el-color-success-light-9);
--app-shadow: rgb(103 194 58 / 30%);
--app-border: var(--el-color-success-light-5);
}
.app-card--warning {
--app-color: var(--el-color-warning);
--app-color-light: var(--el-color-warning-light-3);
--app-bg: var(--el-color-warning-light-9);
--app-shadow: rgb(230 162 60 / 30%);
--app-border: var(--el-color-warning-light-5);
}
.app-card--info {
--app-color: var(--el-color-info);
--app-color-light: var(--el-color-info-light-3);
--app-bg: var(--el-color-info-light-9);
--app-shadow: rgb(144 147 153 / 30%);
--app-border: var(--el-color-info-light-5);
}
</style>