476 lines
11 KiB
Vue
476 lines
11 KiB
Vue
<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>
|