Files
aiot-platform-ui/packages/effects/layouts/src/widgets/project-dropdown/project-dropdown.vue
lzh 37acdcf394 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>
2026-04-22 23:43:44 +08:00

92 lines
2.3 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.

<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>