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

476 lines
11 KiB
Vue
Raw Normal View History

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