feat(@vben/web-antd): 顶栏项目切换器 + 物联运维平台 SSO 无感跳转
- 顶栏 TenantDropdown 替换为 ProjectDropdown(新建 widget)
- 进入时拉 /system/project/simple-list;仅当本地 projectId 不在列表时
才回退到首项,避免静默改写用户选择
- 空列表不渲染,避免出现永远空下拉
- 新增"物联运维"按钮,走 OAuth2 authorization code 流程跳 IoT 前端
- state 使用 crypto.randomUUID() / getRandomValues() 生成(CSRF 防护)
- VITE_IOT_BASE_URL 未配置时按钮隐藏,不再硬编码兜底 URL
- 使用原生 <button disabled> 替代 <a role="button">,修复可访问性
- 新增 /sso-callback 回调页 + /system/sso/callback API
- 挂载后立即 history.replaceState 清 code/state,避免二次 exchange
- API 层做 snake_case → camelCase 映射,统一前端风格
- 文档化 redirectUri 必须与 OAuth2 客户端 redirectUris 白名单一致
- authStore 新增 ssoLogin,与 authLogin 抽取共用 postAuthSuccess
- token 为空直接抛错,fetchUserInfo 失败回滚 token 避免 401 循环
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1 @@
|
||||
export { default as ProjectDropdown } from './project-dropdown.vue';
|
||||
@@ -0,0 +1,91 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
import { IconifyIcon } from '@vben/icons';
|
||||
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@vben-core/shadcn-ui';
|
||||
|
||||
/**
|
||||
* 窄接口:widget 不依赖 apps/* 的 API 类型,只取下拉展示所需字段。
|
||||
* 消费端若传入更大的 Project 对象会被结构化子类型接收,无兼容问题。
|
||||
*/
|
||||
interface Project {
|
||||
id?: number;
|
||||
name: string;
|
||||
code?: string;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
defineOptions({
|
||||
name: 'ProjectDropdown',
|
||||
});
|
||||
|
||||
const props = defineProps<{
|
||||
projectList?: Project[];
|
||||
visitProjectId?: null | number;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits(['success']);
|
||||
|
||||
const projects = computed(() => props.projectList ?? []);
|
||||
|
||||
async function handleChange(id: number | undefined) {
|
||||
if (!id) {
|
||||
return;
|
||||
}
|
||||
const project = projects.value.find((item) => item.id === id);
|
||||
if (!project) {
|
||||
return;
|
||||
}
|
||||
emit('success', project);
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger>
|
||||
<Button
|
||||
variant="outline"
|
||||
class="hover:bg-accent ml-1 mr-2 h-8 w-32 cursor-pointer rounded-full p-1.5"
|
||||
>
|
||||
<IconifyIcon icon="lucide:folder-kanban" class="mr-2" />
|
||||
{{
|
||||
projects.find((item) => item.id === visitProjectId)?.name ||
|
||||
'请选择项目'
|
||||
}}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent class="w-40 p-0 pb-1">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
v-for="project in projects"
|
||||
:key="project.id"
|
||||
:disabled="project.id === visitProjectId"
|
||||
class="mx-1 flex cursor-pointer items-center rounded-sm py-1 leading-8"
|
||||
@click="handleChange(project.id)"
|
||||
>
|
||||
<template v-if="project.id === visitProjectId">
|
||||
<IconifyIcon icon="lucide:check" class="mr-2" />
|
||||
{{ project.name }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ project.name }}
|
||||
</template>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
v-if="projects.length === 0"
|
||||
disabled
|
||||
class="mx-1 flex cursor-default items-center rounded-sm py-1 text-xs leading-8 opacity-60"
|
||||
>
|
||||
暂无项目
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</template>
|
||||
Reference in New Issue
Block a user