Files
wecom_it_smart_desk/frontend-portal/src/views/PortalSelect.vue
T

476 lines
11 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="portal-select">
<!-- 加载中状态 -->
<div v-if="loading" class="loading-container">
<el-icon class="loading-icon" :size="48">
<Loading />
</el-icon>
<p class="loading-text">正在加载用户信息...</p>
</div>
<!-- 错误状态 -->
<div v-else-if="error" class="error-container">
<el-icon class="error-icon" :size="48" color="#ef4444">
<CircleCloseFilled />
</el-icon>
<p class="error-text">{{ error }}</p>
<el-button type="primary" @click="handleRetry">重试</el-button>
</div>
<!-- 角色选择页面 -->
<div v-else class="select-container">
<!-- 标题区域 -->
<div class="header">
<h1 class="title">IT智能服务台</h1>
<p class="subtitle">选择您要进入的工作台</p>
</div>
<!-- 用户信息卡片 -->
<div v-if="userInfo" class="user-info">
<el-avatar :size="48" :src="userInfo.avatar || undefined">
{{ userInfo.name?.charAt(0) || '?' }}
</el-avatar>
<div class="user-details">
<span class="user-name">{{ userInfo.name }}</span>
<span v-if="userInfo.department" class="user-department">{{ userInfo.department }}</span>
</div>
</div>
<!-- 角色卡片列表 -->
<div class="role-cards">
<!-- 用户端卡片 -->
<div
class="role-card"
:class="{ 'role-card--active': selectedRole === 'user' }"
@click="selectRole('user')"
>
<div class="role-card__icon">
<el-icon :size="48" color="#3b82f6">
<User />
</el-icon>
</div>
<div class="role-card__content">
<h3 class="role-card__title">用户端</h3>
<p class="role-card__desc">提交工单查看进度浏览知识库</p>
</div>
<div class="role-card__action">
<el-button type="primary" size="large" @click.stop="enterRole('user')">
进入
</el-button>
</div>
</div>
<!-- 坐席端卡片 -->
<div
v-if="hasAgentRole"
class="role-card"
:class="{ 'role-card--active': selectedRole === 'agent' }"
@click="selectRole('agent')"
>
<div class="role-card__icon">
<el-icon :size="48" color="#f59e0b">
<Headset />
</el-icon>
</div>
<div class="role-card__content">
<h3 class="role-card__title">坐席端</h3>
<p class="role-card__desc">处理会话AI辅助管理工单</p>
</div>
<div class="role-card__action">
<el-button type="warning" size="large" @click.stop="enterRole('agent')">
进入
</el-button>
</div>
</div>
<!-- 管理端卡片 -->
<div
v-if="hasAdminRole"
class="role-card"
:class="{ 'role-card--active': selectedRole === 'admin' }"
@click="selectRole('admin')"
>
<div class="role-card__icon">
<el-icon :size="48" color="#ef4444">
<Setting />
</el-icon>
</div>
<div class="role-card__content">
<h3 class="role-card__title">管理端</h3>
<p class="role-card__desc">系统配置数据分析权限管理</p>
</div>
<div class="role-card__action">
<el-button type="danger" size="large" @click.stop="enterRole('admin')">
进入
</el-button>
</div>
</div>
</div>
<!-- 底部提示 -->
<div class="footer">
<p class="footer-text">
当前账号{{ userInfo?.name }} ({{ userInfo?.employee_id }})
</p>
<p class="footer-hint">
<el-icon><InfoFilled /></el-icon>
每次进入需要重新选择工作台
</p>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { usePortalStore } from '@/stores/portal'
import { storeToRefs } from 'pinia'
import { Loading, CircleCloseFilled, User, Headset, Setting, InfoFilled } from '@element-plus/icons-vue'
import apiClient from '@/api/index'
// 获取 Portal Store
const portalStore = usePortalStore()
// 解构状态
const { userInfo, loading, error, hasAgentRole, hasAdminRole } = storeToRefs(portalStore)
// 选中的角色
const selectedRole = ref<string | null>(null)
// ==================== 生命周期 ====================
onMounted(async () => {
const urlParams = new URLSearchParams(window.location.search)
const token = urlParams.get('token')
const code = urlParams.get('code')
// 1. 企微 OAuth2 回调:URL 中有 code 参数
if (code && !token) {
loading.value = true
try {
const response = await apiClient.post('/h5/oauth/callback', { code })
const data = response.data.data
if (data?.token) {
portalStore.setToken(data.token)
// 清除 URL 中的 code/state 参数
window.history.replaceState({}, '', window.location.pathname)
} else {
error.value = 'OAuth2授权失败:未获取到Token'
return
}
} catch (err: any) {
console.error('OAuth2 回调失败:', err)
error.value = err.response?.data?.detail || 'OAuth2授权失败'
return
} finally {
loading.value = false
}
}
// 2. 从 Portal 跳转回来:URL 中有 token 参数
if (token) {
portalStore.setToken(token)
window.history.replaceState({}, '', window.location.pathname)
}
// 3. 检查是否已登录(localStorage 缓存)
if (!portalStore.isAuthenticated) {
portalStore.restoreFromCache()
if (!portalStore.isAuthenticated) {
// 未登录,尝试触发 OAuth2 流程
const corpId = import.meta.env.VITE_WECOM_CORP_ID || ''
if (corpId) {
// 生产环境:调用后端获取 OAuth2 授权 URL
try {
loading.value = true
const response = await apiClient.get('/h5/oauth/authorize')
const authorizeUrl = response.data.data?.authorize_url
if (authorizeUrl) {
window.location.href = authorizeUrl
return
}
} catch (err) {
console.error('获取 OAuth2 URL 失败:', err)
} finally {
loading.value = false
}
}
// 无 corpId 或获取失败:显示错误(开发环境可 Mock 登录)
error.value = '未登录,请通过企业微信工作台访问'
return
}
}
// 4. 加载用户信息
await portalStore.fetchUserInfo()
// 5. 如果用户只有一个有效角色,直接跳转(避免多角色用户被自动跳走)
// 注意:user角色是默认的,但如果有agent或admin角色,应该让用户选择
const validRoles = portalStore.roles.filter(
(r: any) => r.name === 'agent' || r.name === 'admin'
)
if (validRoles.length === 0 && portalStore.roleCount === 1) {
// 只有默认的user角色,直接跳转
const singleRole = portalStore.roles[0]
if (singleRole) {
enterRole(singleRole.name)
}
}
// 否则:显示角色选择页面(让用户选择)
})
// ==================== 方法 ====================
/**
* 选择角色
*/
function selectRole(role: string) {
selectedRole.value = role
}
/**
* 进入角色对应的工作台
*/
function enterRole(role: string) {
// 保存当前选择的角色到 localStorage(用于记住选择)
localStorage.setItem('portal_selected_role', role)
// 跳转到对应的工作台
const roleUrls: Record<string, string> = {
user: '/itdesk/',
agent: '/itagent/',
admin: '/itadmin/',
}
const url = roleUrls[role]
if (url) {
// 将 Token 传递给目标页面
const token = portalStore.token
if (token) {
window.location.href = `${url}?token=${encodeURIComponent(token)}`
} else {
window.location.href = url
}
}
}
/**
* 重试加载
*/
function handleRetry() {
portalStore.fetchUserInfo()
}
</script>
<style scoped>
/* 页面容器 */
.portal-select {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
}
/* 加载中状态 */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.loading-icon {
animation: spin 1s linear infinite;
color: #3b82f6;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.loading-text {
color: #94a3b8;
font-size: 16px;
}
/* 错误状态 */
.error-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.error-text {
color: #ef4444;
font-size: 16px;
}
/* 选择页面 */
.select-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 32px;
max-width: 800px;
width: 100%;
}
/* 标题区域 */
.header {
text-align: center;
}
.title {
font-size: 32px;
font-weight: 600;
color: #f1f5f9;
margin-bottom: 8px;
}
.subtitle {
font-size: 16px;
color: #94a3b8;
}
/* 用户信息卡片 */
.user-info {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 20px;
background: rgba(30, 41, 59, 0.8);
border-radius: 12px;
border: 1px solid rgba(71, 85, 105, 0.5);
}
.user-details {
display: flex;
flex-direction: column;
}
.user-name {
font-size: 16px;
font-weight: 500;
color: #f1f5f9;
}
.user-department {
font-size: 14px;
color: #94a3b8;
}
/* 角色卡片列表 */
.role-cards {
display: flex;
flex-direction: column;
gap: 16px;
width: 100%;
}
/* 角色卡片 */
.role-card {
display: flex;
align-items: center;
gap: 20px;
padding: 24px;
background: rgba(30, 41, 59, 0.8);
border-radius: 16px;
border: 2px solid transparent;
cursor: pointer;
transition: all 0.3s ease;
}
.role-card:hover {
background: rgba(30, 41, 59, 0.95);
border-color: rgba(59, 130, 246, 0.5);
transform: translateY(-2px);
}
.role-card--active {
border-color: #3b82f6;
background: rgba(59, 130, 246, 0.1);
}
.role-card__icon {
flex-shrink: 0;
width: 80px;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
background: rgba(59, 130, 246, 0.1);
border-radius: 16px;
}
.role-card__content {
flex: 1;
}
.role-card__title {
font-size: 20px;
font-weight: 600;
color: #f1f5f9;
margin-bottom: 4px;
}
.role-card__desc {
font-size: 14px;
color: #94a3b8;
}
.role-card__action {
flex-shrink: 0;
}
/* 底部提示 */
.footer {
text-align: center;
margin-top: 16px;
}
.footer-text {
font-size: 14px;
color: #64748b;
margin-bottom: 8px;
}
.footer-hint {
font-size: 12px;
color: #475569;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
/* 响应式布局 */
@media (max-width: 640px) {
.role-card {
flex-direction: column;
text-align: center;
}
.role-card__icon {
width: 64px;
height: 64px;
}
.role-card__action {
width: 100%;
}
.role-card__action .el-button {
width: 100%;
}
}
</style>