feat: 2026-07-11 全量更新 - 代办集成+会议室预定+知识迭代修复+UI统一+Bug修复
== 已部署上线 (9项) == - 代办事项真实数据源集成 (企微审批API 8bug修复链) - H5/坐席端 Logo样式统一+绿色背景 - 视频引导页修复 (localStorage key v2) - 坐席端 v9 Vue版本修复 (ElMessage._context) - 截图按钮 v10 修复 (getDisplayMedia user gesture) - 扫码样式恢复+H5扫码登录跳转修复 - H5截图快捷键提示 == 代码完成待部署 (3项) == - 知识迭代3Bug修复 (#8 POST端点/#7 MERGE幂等/#6 过期检查) - 会议室预定-小鱼易联终端 (40文件, 40/40测试通过) - IT资产升级审批推送 (asset_service.py) == 需求文档 (2项) == - 坐席端AI辅助消息框-PRD (4项新功能确认) - 坐席端布局优化建议 v2.0 (7天计划) == 新增文档 == - 日报-2026-07-11.md - 知识迭代Bug修复报告-20260711.md - 会议室预定-部署指南.md - CHANGELOG.md 更新 == 测试 == - test_todo_integration.py: 40/40 - test_meetingroom.py: 40/40 - test_bugfix_ki_suggestions.py: 21/21
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
// =============================================================================
|
||||
// 企微IT智能服务台 — 统一认证 API 模块
|
||||
// =============================================================================
|
||||
// 说明:三端(H5/坐席/管理)共用同一套认证API
|
||||
// API 设计文档:docs/03-技术架构/02-技术方案/技术方案-认证API设计.md
|
||||
// =============================================================================
|
||||
|
||||
import apiClient from './index'
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 类型定义
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
/** 用户信息 */
|
||||
export interface UserInfo {
|
||||
userid: string
|
||||
name: string
|
||||
role: string
|
||||
avatar?: string
|
||||
department?: string
|
||||
phone?: string
|
||||
email?: string
|
||||
corp_name?: string
|
||||
}
|
||||
|
||||
/** 二维码响应 */
|
||||
export interface QrcodeResponse {
|
||||
qrcode_url: string
|
||||
qrcode_png_base64: string
|
||||
ticket: string
|
||||
expires_in: number
|
||||
expires_at?: string
|
||||
}
|
||||
|
||||
/** 扫码状态响应 */
|
||||
export interface ScanStatusResponse {
|
||||
status: 'waiting' | 'scanned' | 'confirmed' | 'expired' | 'cancelled'
|
||||
user_info?: UserInfo
|
||||
token?: string
|
||||
is_new_user?: boolean
|
||||
}
|
||||
|
||||
/** OAuth2 回调响应 */
|
||||
export interface OAuthCallbackResponse {
|
||||
token: string
|
||||
user_info: UserInfo
|
||||
is_new_user: boolean
|
||||
}
|
||||
|
||||
/** 绑定请求 */
|
||||
export interface BindRequest {
|
||||
employee_id: string
|
||||
corp_id?: string
|
||||
bind_type?: 'existing' | 'new'
|
||||
employee_no?: string
|
||||
real_name?: string
|
||||
department?: string
|
||||
phone?: string
|
||||
}
|
||||
|
||||
/** 绑定响应 */
|
||||
export interface BindResponse {
|
||||
token: string
|
||||
user_info: UserInfo
|
||||
}
|
||||
|
||||
/** 验证 Token 响应 */
|
||||
export interface VerifyResponse {
|
||||
valid: boolean
|
||||
user_info: UserInfo
|
||||
expire_at: string
|
||||
}
|
||||
|
||||
/** 当前用户响应 */
|
||||
export interface MeResponse extends UserInfo {}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// API 函数
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 获取扫码登录二维码
|
||||
* 生成登录二维码,供企微外用户扫描
|
||||
*/
|
||||
export async function getQrcode(): Promise<QrcodeResponse> {
|
||||
return apiClient.get('/auth/qrcode')
|
||||
}
|
||||
|
||||
/**
|
||||
* 轮询查询扫码状态
|
||||
* 客户端轮询此接口查询用户扫码结果
|
||||
* @param ticket - 扫码登录票据
|
||||
*/
|
||||
export async function getScanStatus(ticket: string): Promise<ScanStatusResponse> {
|
||||
return apiClient.get('/auth/scan/status', { params: { ticket } })
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 OAuth2 回调
|
||||
* 企微内打开页面时,通过OAuth2获取userid
|
||||
* @param code - 企微授权码
|
||||
* @param state - 随机state,防止CSRF
|
||||
*/
|
||||
export async function handleOAuthCallback(
|
||||
code: string,
|
||||
state?: string
|
||||
): Promise<OAuthCallbackResponse> {
|
||||
return apiClient.get('/auth/oauth2/callback', {
|
||||
params: { code, state },
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 账号绑定
|
||||
* 互联企业用户扫码后无本地记录,绑定已有账号或申请新账号
|
||||
*/
|
||||
export async function bindAccount(data: BindRequest): Promise<BindResponse> {
|
||||
return apiClient.post('/auth/bind', data)
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 Token
|
||||
* 验证Token有效性,续期
|
||||
* @param token - 要验证的 Token 字符串
|
||||
*/
|
||||
export async function verifyToken(token: string): Promise<VerifyResponse> {
|
||||
return apiClient.post('/auth/verify', { token })
|
||||
}
|
||||
|
||||
/**
|
||||
* 登出
|
||||
* 清除Token
|
||||
*/
|
||||
export async function logout(): Promise<void> {
|
||||
return apiClient.post('/auth/logout')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
* 获取已登录用户详情
|
||||
*/
|
||||
export async function getCurrentUser(): Promise<MeResponse> {
|
||||
return apiClient.get('/auth/me')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 OAuth2 授权 URL
|
||||
* 用于企微内自动跳转授权
|
||||
* @param redirect_uri - 授权成功后的回调地址
|
||||
*/
|
||||
export async function getOAuthAuthorizeUrl(redirect_uri: string): Promise<{ authorize_url: string }> {
|
||||
return apiClient.get('/auth/oauth2/url', { params: { redirect_uri } })
|
||||
}
|
||||
@@ -17,7 +17,7 @@
|
||||
<el-icon :size="16"><Headset /></el-icon>
|
||||
</div>
|
||||
<div>
|
||||
<div class="sidebar-logo-text">IT智能服务台</div>
|
||||
<div class="sidebar-logo-text">智能IT服务平台</div>
|
||||
<div class="sidebar-logo-sub">管理后台 v1.0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,13 @@ const routes = [
|
||||
component: () => import('@/views/Login.vue'),
|
||||
meta: { title: '管理员登录', requiresAuth: false },
|
||||
},
|
||||
// 账号绑定页(互联企业用户)
|
||||
{
|
||||
path: '/bind',
|
||||
name: 'Bind',
|
||||
component: () => import('@/views/Bind.vue'),
|
||||
meta: { title: '账号绑定', requiresAuth: false },
|
||||
},
|
||||
{
|
||||
// 管理后台主布局(需要认证)
|
||||
path: '/',
|
||||
@@ -235,7 +242,7 @@ const router = createRouter({
|
||||
router.beforeEach((to, _from, next) => {
|
||||
// 设置页面标题
|
||||
if (to.meta.title) {
|
||||
document.title = `${to.meta.title} - IT智能服务台管理后台`
|
||||
document.title = `${to.meta.title} - 智能IT服务平台`
|
||||
}
|
||||
|
||||
// ===== 新增:处理 URL 中的 token 参数(从 Portal 跳转过来时) =====
|
||||
@@ -253,6 +260,12 @@ router.beforeEach((to, _from, next) => {
|
||||
const requiresAuth = to.meta.requiresAuth !== false
|
||||
const token = localStorage.getItem('admin_token')
|
||||
|
||||
// 账号绑定页不需要认证
|
||||
if (to.name === 'Bind') {
|
||||
next()
|
||||
return
|
||||
}
|
||||
|
||||
if (requiresAuth && !token) {
|
||||
// 需要认证但没有 token,跳转到登录页
|
||||
next({ path: '/login', query: { redirect: to.fullPath } })
|
||||
|
||||
@@ -0,0 +1,405 @@
|
||||
<!-- =============================================================================
|
||||
// 企微IT智能服务台 — 管理端账号绑定页
|
||||
// =============================================================================
|
||||
// 说明:互联企业用户扫码后无本地记录时,跳转绑定页面
|
||||
// - 绑定已有账号
|
||||
// - 申请新账号
|
||||
// ============================================================================= -->
|
||||
|
||||
<template>
|
||||
<div class="bind-page">
|
||||
<!-- 背景装饰 -->
|
||||
<div class="bind-bg">
|
||||
<div class="bind-bg-gradient"></div>
|
||||
</div>
|
||||
|
||||
<!-- 绑定卡片 -->
|
||||
<div class="bind-card">
|
||||
<!-- 标题区 -->
|
||||
<div class="bind-header">
|
||||
<div class="bind-logo-icon">
|
||||
<el-icon :size="28"><Link /></el-icon>
|
||||
</div>
|
||||
<h1 class="bind-title">账号绑定</h1>
|
||||
<p class="bind-subtitle">请绑定您的企业账号</p>
|
||||
</div>
|
||||
|
||||
<!-- 用户信息 -->
|
||||
<div v-if="wecomUserInfo" class="wecom-user-info">
|
||||
<el-descriptions :column="1" border>
|
||||
<el-descriptions-item label="企业微信">{{ wecomUserInfo.name }}</el-descriptions-item>
|
||||
<el-descriptions-item v-if="wecomUserInfo.corp_name" label="企业">{{ wecomUserInfo.corp_name }}</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</div>
|
||||
|
||||
<!-- 绑定方式选择 -->
|
||||
<div v-if="!showBindForm" class="bind-options">
|
||||
<el-radio-group v-model="bindType">
|
||||
<el-radio value="existing" border>绑定已有账号</el-radio>
|
||||
<el-radio value="new" border>申请新账号</el-radio>
|
||||
</el-radio-group>
|
||||
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
class="next-btn"
|
||||
@click="handleNext"
|
||||
>
|
||||
下一步
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 绑定表单 -->
|
||||
<div v-else class="bind-form">
|
||||
<!-- 绑定已有账号 -->
|
||||
<el-form v-if="bindType === 'existing'" :model="bindForm" label-position="top" @submit.prevent="handleBindExisting">
|
||||
<el-form-item label="员工号" required>
|
||||
<el-input
|
||||
v-model="bindForm.employee_no"
|
||||
placeholder="请输入您的员工号"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<div class="form-actions">
|
||||
<el-button size="large" @click="handleBack">返回</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
native-type="submit"
|
||||
:loading="binding"
|
||||
>
|
||||
绑定
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
|
||||
<!-- 申请新账号 -->
|
||||
<el-form v-if="bindType === 'new'" :model="bindForm" label-position="top" @submit.prevent="handleBindNew">
|
||||
<el-form-item label="姓名" required>
|
||||
<el-input
|
||||
v-model="bindForm.real_name"
|
||||
placeholder="请输入您的姓名"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="部门" required>
|
||||
<el-input
|
||||
v-model="bindForm.department"
|
||||
placeholder="请输入部门"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="手机" required>
|
||||
<el-input
|
||||
v-model="bindForm.phone"
|
||||
placeholder="请输入手机号"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<div class="form-actions">
|
||||
<el-button size="large" @click="handleBack">返回</el-button>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
native-type="submit"
|
||||
:loading="binding"
|
||||
>
|
||||
申请绑定
|
||||
</el-button>
|
||||
</div>
|
||||
</el-form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, reactive, onMounted } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Link } from '@element-plus/icons-vue'
|
||||
import { bindAccount } from '@/api/auth'
|
||||
import { useAdminStore } from '@/stores/admin'
|
||||
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const adminStore = useAdminStore()
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 状态
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
/** 企微用户信息 */
|
||||
const wecomUserInfo = ref<{
|
||||
userid: string
|
||||
name: string
|
||||
corp_name?: string
|
||||
} | null>(null)
|
||||
|
||||
/** 绑定方式 */
|
||||
const bindType = ref<'existing' | 'new'>('existing')
|
||||
|
||||
/** 是否显示绑定表单 */
|
||||
const showBindForm = ref(false)
|
||||
|
||||
/** 绑定中状态 */
|
||||
const binding = ref(false)
|
||||
|
||||
/** 绑定表单 */
|
||||
const bindForm = reactive({
|
||||
employee_no: '',
|
||||
real_name: '',
|
||||
department: '',
|
||||
phone: '',
|
||||
})
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 方法
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 下一步
|
||||
*/
|
||||
function handleNext(): void {
|
||||
showBindForm.value = true
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回
|
||||
*/
|
||||
function handleBack(): void {
|
||||
showBindForm.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 绑定已有账号
|
||||
*/
|
||||
async function handleBindExisting(): Promise<void> {
|
||||
if (!wecomUserInfo.value) {
|
||||
ElMessage.error('用户信息无效')
|
||||
return
|
||||
}
|
||||
|
||||
if (!bindForm.employee_no.trim()) {
|
||||
ElMessage.error('请输入员工号')
|
||||
return
|
||||
}
|
||||
|
||||
binding.value = true
|
||||
|
||||
try {
|
||||
const result = await bindAccount({
|
||||
employee_id: wecomUserInfo.value.userid,
|
||||
bind_type: 'existing',
|
||||
employee_no: bindForm.employee_no,
|
||||
})
|
||||
|
||||
// 绑定成功,登录
|
||||
localStorage.setItem('admin_token', result.token)
|
||||
adminStore.token = result.token
|
||||
localStorage.setItem('admin_user_id', result.user_info.userid)
|
||||
adminStore.adminUserId = result.user_info.userid
|
||||
adminStore.adminInfo = {
|
||||
user_id: result.user_info.userid,
|
||||
name: result.user_info.name,
|
||||
status: 'online',
|
||||
}
|
||||
|
||||
ElMessage.success('绑定成功')
|
||||
router.replace('/')
|
||||
} catch (error: any) {
|
||||
console.error('绑定失败:', error)
|
||||
const msg = error?.message || '绑定失败,请重试'
|
||||
ElMessage.error(msg)
|
||||
} finally {
|
||||
binding.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 申请新账号
|
||||
*/
|
||||
async function handleBindNew(): Promise<void> {
|
||||
if (!wecomUserInfo.value) {
|
||||
ElMessage.error('用户信息无效')
|
||||
return
|
||||
}
|
||||
|
||||
if (!bindForm.real_name.trim() || !bindForm.department.trim() || !bindForm.phone.trim()) {
|
||||
ElMessage.error('请填写完整信息')
|
||||
return
|
||||
}
|
||||
|
||||
binding.value = true
|
||||
|
||||
try {
|
||||
const result = await bindAccount({
|
||||
employee_id: wecomUserInfo.value.userid,
|
||||
bind_type: 'new',
|
||||
real_name: bindForm.real_name,
|
||||
department: bindForm.department,
|
||||
phone: bindForm.phone,
|
||||
})
|
||||
|
||||
// 绑定成功,登录
|
||||
localStorage.setItem('admin_token', result.token)
|
||||
adminStore.token = result.token
|
||||
localStorage.setItem('admin_user_id', result.user_info.userid)
|
||||
adminStore.adminUserId = result.user_info.userid
|
||||
adminStore.adminInfo = {
|
||||
user_id: result.user_info.userid,
|
||||
name: result.user_info.name,
|
||||
status: 'online',
|
||||
}
|
||||
|
||||
ElMessage.success('申请成功')
|
||||
router.replace('/')
|
||||
} catch (error: any) {
|
||||
console.error('申请绑定失败:', error)
|
||||
const msg = error?.message || '申请失败,请重试'
|
||||
ElMessage.error(msg)
|
||||
} finally {
|
||||
binding.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 生命周期
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
onMounted(() => {
|
||||
// 从路由参数获取企微用户信息
|
||||
const userid = route.query.userid as string
|
||||
const name = route.query.name as string
|
||||
const corpName = route.query.corp_name as string
|
||||
|
||||
if (userid && name) {
|
||||
wecomUserInfo.value = {
|
||||
userid,
|
||||
name,
|
||||
corp_name: corpName,
|
||||
}
|
||||
} else {
|
||||
// 缺少必要参数,返回登录页
|
||||
ElMessage.error('缺少用户信息,请重新登录')
|
||||
router.replace('/login')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
/* 绑定页面容器 */
|
||||
.bind-page {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
position: relative;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
/* 背景装饰 */
|
||||
.bind-bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
.bind-bg-gradient {
|
||||
position: absolute;
|
||||
top: -50%;
|
||||
left: -50%;
|
||||
width: 200%;
|
||||
height: 200%;
|
||||
background: radial-gradient(ellipse at center, rgba(59, 130, 246, 0.08) 0%, transparent 60%);
|
||||
animation: rotate 30s linear infinite;
|
||||
}
|
||||
@keyframes rotate {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 绑定卡片 */
|
||||
.bind-card {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
width: 400px;
|
||||
padding: 40px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
/* 标题区 */
|
||||
.bind-header {
|
||||
text-align: center;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.bind-logo-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: white;
|
||||
margin: 0 auto 16px;
|
||||
}
|
||||
|
||||
.bind-title {
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
|
||||
.bind-subtitle {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 企微用户信息 */
|
||||
.wecom-user-info {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
/* 绑定选项 */
|
||||
.bind-options {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.bind-options :deep(.el-radio-group) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.bind-options :deep(.el-radio) {
|
||||
padding: 12px;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.next-btn {
|
||||
width: 100%;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
/* 绑定表单 */
|
||||
.bind-form {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.form-actions :deep(.el-button) {
|
||||
flex: 1;
|
||||
}
|
||||
</style>
|
||||
+252
-455
@@ -1,12 +1,12 @@
|
||||
<!--
|
||||
=============================================================================
|
||||
IT智能服务台 — 管理员登录页 (v1.2, 2026-07-06)
|
||||
=============================================================================
|
||||
说明:智能检测企微登录状态,提供三种登录方式
|
||||
- PRD v1.5 §4.5: 企微免密登录/企微扫码登录/账号密码+OTP
|
||||
- 复用坐席端登录 API(POST /api/agents/login)
|
||||
- 企微免密登录:检测到企微登录账号且有管理员角色,免密直接进入
|
||||
-->
|
||||
<!-- =============================================================================
|
||||
// 企微IT智能服务台 — 管理端统一登录页
|
||||
// =============================================================================
|
||||
// 说明:统一认证登录页面
|
||||
// - 企微内:自动跳转 OAuth2 授权
|
||||
// - 企微外:展示扫码登录页面,轮询扫码状态
|
||||
// - 支持账号绑定(互联企业用户)
|
||||
// ============================================================================= -->
|
||||
|
||||
<template>
|
||||
<div class="login-page">
|
||||
<!-- 背景装饰 -->
|
||||
@@ -21,34 +21,19 @@ IT智能服务台 — 管理员登录页 (v1.2, 2026-07-06)
|
||||
<div class="login-logo-icon">
|
||||
<el-icon :size="28"><Headset /></el-icon>
|
||||
</div>
|
||||
<h1 class="login-title">IT智能服务台</h1>
|
||||
<h1 class="login-title">智能IT服务平台</h1>
|
||||
<p class="login-subtitle">管理后台</p>
|
||||
</div>
|
||||
|
||||
<!-- 企微免密登录(仅企微内管理员可见) -->
|
||||
<div v-if="wecomUserId" style="text-align: center; margin-bottom: 12px;">
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
:loading="wecomQuickLoading"
|
||||
@click="handleWecomQuickLogin"
|
||||
class="option-btn"
|
||||
style="width: 100%;"
|
||||
>
|
||||
<el-icon><Key /></el-icon>
|
||||
企微免密登录({{ wecomUserId }})
|
||||
</el-button>
|
||||
</div>
|
||||
|
||||
<!-- 企微扫码登录面板(始终可见,与坐席端一致) -->
|
||||
<div class="qr-login">
|
||||
<!-- 扫码登录面板 -->
|
||||
<div v-if="showQrPanel" class="qr-login">
|
||||
<div class="qr-container">
|
||||
<div v-if="qrLoading" class="qr-loading">
|
||||
<el-icon class="is-loading"><Loading /></el-icon>
|
||||
<p>加载中...</p>
|
||||
</div>
|
||||
<div v-else-if="qrCode" class="qr-code">
|
||||
<img :src="qrCode" alt="企微扫码登录" />
|
||||
<div v-else-if="qrCodeUrl" class="qr-code">
|
||||
<img :src="qrCodeUrl" alt="企微扫码登录" />
|
||||
</div>
|
||||
<div v-else class="qr-error">
|
||||
<p>获取二维码失败</p>
|
||||
@@ -56,315 +41,69 @@ IT智能服务台 — 管理员登录页 (v1.2, 2026-07-06)
|
||||
</div>
|
||||
</div>
|
||||
<p class="qr-hint">请使用企业微信扫码登录</p>
|
||||
|
||||
<!-- 分隔线 -->
|
||||
<div class="divider">
|
||||
<span>其他登录方式</span>
|
||||
</div>
|
||||
|
||||
<el-button
|
||||
size="large"
|
||||
class="password-login-btn"
|
||||
@click="showPasswordPanel = true"
|
||||
>
|
||||
<span>🔐</span>
|
||||
账号密码登录
|
||||
</el-button>
|
||||
<p v-if="scanStatus === 'scanned'" class="scan-tip">
|
||||
<el-icon><CircleCheckFilled /></el-icon> 已扫码,请在手机确认
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- 账号密码登录表单(与二维码同时展示,无需返回按钮) -->
|
||||
<div v-if="showPasswordPanel" class="password-login">
|
||||
<el-form
|
||||
ref="formRef"
|
||||
:model="loginForm"
|
||||
:rules="rules"
|
||||
label-position="top"
|
||||
@submit.prevent="handleLogin"
|
||||
>
|
||||
<el-form-item label="账号" prop="userId">
|
||||
<el-input
|
||||
v-model="loginForm.userId"
|
||||
placeholder="请输入账号(如 sxn)"
|
||||
size="large"
|
||||
:prefix-icon="User"
|
||||
@keydown.enter="handleLogin"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item label="密码" prop="password">
|
||||
<el-input
|
||||
v-model="loginForm.password"
|
||||
type="password"
|
||||
placeholder="请输入密码"
|
||||
size="large"
|
||||
:prefix-icon="Lock"
|
||||
show-password
|
||||
@keydown.enter="handleLogin"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<!-- OTP 输入区: 仅在 requireOtp 时显示 -->
|
||||
<el-form-item v-if="requireOtp" label="OTP 验证码" prop="otpCode">
|
||||
<el-input
|
||||
v-model="loginForm.otpCode"
|
||||
placeholder="请输入 Google Authenticator 验证码"
|
||||
size="large"
|
||||
:prefix-icon="Key"
|
||||
maxlength="6"
|
||||
@keydown.enter="handleLogin"
|
||||
/>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item>
|
||||
<el-button
|
||||
type="primary"
|
||||
size="large"
|
||||
:loading="adminStore.logging"
|
||||
:disabled="adminStore.logging"
|
||||
@click="handleLogin"
|
||||
class="login-btn"
|
||||
>
|
||||
{{ adminStore.logging ? '登录中...' : (requireOtp ? '验证 OTP' : '登录管理后台') }}
|
||||
</el-button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
</div>
|
||||
|
||||
<!-- 错误提示 -->
|
||||
<el-alert
|
||||
v-if="errorMsg"
|
||||
:title="errorMsg"
|
||||
type="error"
|
||||
show-icon
|
||||
:closable="true"
|
||||
@close="errorMsg = ''"
|
||||
style="margin-top: 16px"
|
||||
/>
|
||||
|
||||
<!-- 提示信息 -->
|
||||
<div class="login-tips">
|
||||
<el-icon :size="14"><InfoFilled /></el-icon>
|
||||
<span>仅限组长(admin角色)登录管理后台</span>
|
||||
<!-- 加载状态 -->
|
||||
<div v-if="loading" class="loading-container">
|
||||
<el-icon class="is-loading" size="32"><Loading /></el-icon>
|
||||
<p>{{ loadingText }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
// ==========================================================================
|
||||
// 依赖导入
|
||||
// ==========================================================================
|
||||
import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { Loading, CircleCheckFilled, Headset } from '@element-plus/icons-vue'
|
||||
import { getQrcode, getScanStatus, handleOAuthCallback, type QrcodeResponse, type ScanStatusResponse } from '@/api/auth'
|
||||
import { useAdminStore } from '@/stores/admin'
|
||||
import type { FormInstance, FormRules } from 'element-plus'
|
||||
import { Loading, User, Lock, Key, InfoFilled, Headset } from '@element-plus/icons-vue'
|
||||
|
||||
// ==========================================================================
|
||||
// Store
|
||||
// ==========================================================================
|
||||
const router = useRouter()
|
||||
const adminStore = useAdminStore()
|
||||
|
||||
// ==========================================================================
|
||||
// 表单状态
|
||||
// ==========================================================================
|
||||
// --------------------------------------------------------------------------
|
||||
// 状态
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
/** 表单引用 */
|
||||
const formRef = ref<FormInstance>()
|
||||
/** 是否显示扫码面板 */
|
||||
const showQrPanel = ref(true)
|
||||
|
||||
/** 登录表单数据 */
|
||||
const loginForm = reactive({
|
||||
userId: '',
|
||||
password: '',
|
||||
otpCode: '',
|
||||
})
|
||||
|
||||
/** 是否需要 OTP 验证 */
|
||||
const requireOtp = ref(false)
|
||||
|
||||
/** 错误信息 */
|
||||
const errorMsg = ref<string>('')
|
||||
|
||||
// ==========================================================================
|
||||
// 企微智能检测相关状态(PRD v1.5 §4.5)
|
||||
// ==========================================================================
|
||||
|
||||
/** 是否显示账号密码登录 */
|
||||
const showPasswordPanel = ref(false)
|
||||
|
||||
/** 企微用户ID(用于免密登录) */
|
||||
const wecomUserId = ref('')
|
||||
|
||||
/** 企微快捷登录loading */
|
||||
const wecomQuickLoading = ref(false)
|
||||
|
||||
/** 企微二维码 */
|
||||
const qrCode = ref('')
|
||||
/** 二维码 URL */
|
||||
const qrCodeUrl = ref('')
|
||||
|
||||
/** 二维码加载中 */
|
||||
const qrLoading = ref(false)
|
||||
|
||||
/** 扫码状态 */
|
||||
const scanStatus = ref<'waiting' | 'scanned' | ''>('')
|
||||
|
||||
/** 全局加载状态 */
|
||||
const loading = ref(false)
|
||||
|
||||
/** 加载提示文字 */
|
||||
const loadingText = ref('请稍候...')
|
||||
|
||||
/** 轮询定时器 */
|
||||
let pollTimer: ReturnType<typeof setInterval> | null = null
|
||||
/** 当前二维码票据 */
|
||||
|
||||
/** 当前扫码登录票据 */
|
||||
let currentTicket = ''
|
||||
|
||||
/** 企微检测中 */
|
||||
const wecomChecking = ref(false)
|
||||
|
||||
/** 表单校验规则 */
|
||||
const rules: FormRules = {
|
||||
userId: [
|
||||
{ required: true, message: '请输入账号', trigger: 'blur' },
|
||||
],
|
||||
password: [
|
||||
{ required: true, message: '请输入密码', trigger: 'blur' },
|
||||
],
|
||||
otpCode: [
|
||||
{ required: true, message: '请输入 OTP 验证码', trigger: 'blur' },
|
||||
{ len: 6, message: '验证码为 6 位数字', trigger: 'blur' },
|
||||
],
|
||||
}
|
||||
|
||||
// ==========================================================================
|
||||
// --------------------------------------------------------------------------
|
||||
// 方法
|
||||
// ==========================================================================
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 检测企微客户端登录状态(PRD v1.5 §4.5)
|
||||
* 页面加载时自动检测
|
||||
* 检测是否在企微环境
|
||||
*/
|
||||
async function checkWecomClient(): Promise<void> {
|
||||
wecomChecking.value = true
|
||||
|
||||
try {
|
||||
// 首先尝试使用 UA 检测
|
||||
const isWxWork = /wxwork/i.test(navigator.userAgent)
|
||||
|
||||
// 不在企微环境
|
||||
if (!isWxWork) {
|
||||
wecomChecking.value = false
|
||||
return
|
||||
}
|
||||
|
||||
// 在企微客户端中,尝试使用企微 JS-SDK
|
||||
if (typeof (window as any).wx !== 'undefined') {
|
||||
try {
|
||||
// 获取当前页面 URL
|
||||
const currentUrl = window.location.href.split('#')[0]
|
||||
|
||||
// 调用后端接口获取 JS-SDK 签名配置
|
||||
const apiClient = (await import('@/api')).default
|
||||
const configResponse = await apiClient.get('/wecom/jsapi-config', {
|
||||
params: { url: currentUrl }
|
||||
})
|
||||
|
||||
const jsConfig: any = configResponse
|
||||
console.log('[Admin Login] 获取到企微 JS-SDK 配置:', jsConfig)
|
||||
|
||||
// 使用 wx.config 配置企微 JS-SDK
|
||||
await new Promise<void>((resolve) => {
|
||||
(window as any).wx.config({
|
||||
beta: true,
|
||||
debug: false,
|
||||
appId: jsConfig.corp_id,
|
||||
timestamp: jsConfig.timestamp,
|
||||
nonceStr: jsConfig.nonce_str,
|
||||
signature: jsConfig.signature,
|
||||
jsApiList: ['agentConfig']
|
||||
})
|
||||
resolve()
|
||||
})
|
||||
|
||||
// 使用 wx.agentConfig 获取当前用户信息
|
||||
const userInfo = await new Promise<any>((resolve, reject) => {
|
||||
(window as any).wx.agentConfig({
|
||||
corpid: jsConfig.corp_id,
|
||||
agentid: jsConfig.agent_id,
|
||||
timestamp: jsConfig.timestamp,
|
||||
nonceStr: jsConfig.nonce_str,
|
||||
signature: jsConfig.signature,
|
||||
success: (res: any) => resolve(res),
|
||||
fail: (res: any) => reject(new Error(res.errMsg || 'agentConfig 失败'))
|
||||
})
|
||||
})
|
||||
|
||||
// 获取用户ID
|
||||
const userId = userInfo.userId
|
||||
if (userId) {
|
||||
console.log('[Admin Login] 获取到企微用户 ID:', userId)
|
||||
wecomUserId.value = userId
|
||||
|
||||
// 检测角色
|
||||
const roleResponse = await apiClient.get('/wecom/check-role', {
|
||||
params: { userid: userId }
|
||||
})
|
||||
const roleData: any = roleResponse
|
||||
console.log('[Admin Login] 用户角色:', roleData)
|
||||
|
||||
// 如果是管理员,显示免密登录选项
|
||||
if (roleData.role === 'admin') {
|
||||
// 企微内管理员可免密登录(按钮将显示在顶部)
|
||||
} else {
|
||||
// 非管理员,使用二维码和密码登录
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// 检测失败,显示普通登录
|
||||
console.warn('[Admin Login] 企微 JS-SDK 检测失败:', e)
|
||||
}
|
||||
} else {
|
||||
// 没有企微 JS-SDK
|
||||
console.log('[Admin Login] 无企微 JS-SDK,使用默认登录')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('企微客户端检测失败:', error)
|
||||
} finally {
|
||||
wecomChecking.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 企微免密登录
|
||||
*/
|
||||
async function handleWecomQuickLogin(): Promise<void> {
|
||||
wecomQuickLoading.value = true
|
||||
|
||||
try {
|
||||
const apiClient = (await import('@/api')).default
|
||||
|
||||
// 拦截器已统一返回 inner data(CTRT-03),response 即 {token, employee_id, name, ...}
|
||||
const result: any = await apiClient.post('/auth_wecom/jsdk-login', {
|
||||
userid: wecomUserId.value,
|
||||
login_source: 'wecom_jsdk_admin'
|
||||
})
|
||||
|
||||
if (result?.token) {
|
||||
const { token, employee_id, name } = result
|
||||
|
||||
// 存储 token
|
||||
localStorage.setItem('admin_token', token)
|
||||
if (employee_id) {
|
||||
localStorage.setItem('admin_user_id', employee_id)
|
||||
}
|
||||
|
||||
// 存储到 store
|
||||
adminStore.token = token
|
||||
adminStore.adminUserId = employee_id
|
||||
|
||||
ElMessage.success('登录成功')
|
||||
router.push('/')
|
||||
} else {
|
||||
throw new Error('免密登录失败')
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('企微免密登录失败:', error)
|
||||
ElMessage.error('免密登录失败,请使用其他方式')
|
||||
} finally {
|
||||
wecomQuickLoading.value = false
|
||||
}
|
||||
function isWecomEnv(): boolean {
|
||||
if (typeof navigator === 'undefined') return false
|
||||
return /wxwork/i.test(navigator.userAgent)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -372,21 +111,16 @@ async function handleWecomQuickLogin(): Promise<void> {
|
||||
*/
|
||||
async function fetchQrCode(): Promise<void> {
|
||||
qrLoading.value = true
|
||||
scanStatus.value = 'waiting'
|
||||
|
||||
try {
|
||||
const apiClient = (await import('@/api')).default
|
||||
const result: any = await apiClient.post('/auth_qrcode/create')
|
||||
if (result.qrcode_png_base64) {
|
||||
qrCode.value = `data:image/png;base64,${result.qrcode_png_base64}`
|
||||
} else if (result.qrcode_url) {
|
||||
qrCode.value = result.qrcode_url
|
||||
}
|
||||
// 保存票据并启动轮询
|
||||
if (result.ticket) {
|
||||
currentTicket = result.ticket
|
||||
startPolling()
|
||||
}
|
||||
const data: QrcodeResponse = await getQrcode()
|
||||
qrCodeUrl.value = `data:image/png;base64,${data.qrcode_png_base64}`
|
||||
currentTicket = data.ticket
|
||||
startPolling()
|
||||
} catch (error) {
|
||||
console.error('获取二维码失败:', error)
|
||||
ElMessage.error('获取二维码失败,请重试')
|
||||
} finally {
|
||||
qrLoading.value = false
|
||||
}
|
||||
@@ -395,40 +129,43 @@ async function fetchQrCode(): Promise<void> {
|
||||
/**
|
||||
* 轮询扫码状态
|
||||
*/
|
||||
async function pollQrCode(): Promise<void> {
|
||||
async function pollScanStatus(): Promise<void> {
|
||||
if (!currentTicket) return
|
||||
|
||||
try {
|
||||
const apiClient = (await import('@/api')).default
|
||||
const result: any = await apiClient.get(`/auth_qrcode/poll/${currentTicket}`)
|
||||
if (!result) return
|
||||
const result: ScanStatusResponse = await getScanStatus(currentTicket)
|
||||
|
||||
const { status, token, employee_id, name } = result
|
||||
|
||||
if (status === 'confirmed' && token) {
|
||||
if (result.status === 'scanned') {
|
||||
scanStatus.value = 'scanned'
|
||||
} else if (result.status === 'confirmed' && result.token) {
|
||||
stopPolling()
|
||||
localStorage.setItem('admin_token', token)
|
||||
if (employee_id) localStorage.setItem('admin_user_id', employee_id)
|
||||
adminStore.token = token
|
||||
adminStore.adminUserId = employee_id
|
||||
ElMessage.success('扫码登录成功')
|
||||
router.push('/')
|
||||
} else if (status === 'expired') {
|
||||
await handleLoginSuccess(result.token, result.user_info)
|
||||
} else if (result.status === 'expired') {
|
||||
stopPolling()
|
||||
qrCode.value = ''
|
||||
ElMessage.warning('二维码已过期,请刷新')
|
||||
ElMessage.warning('二维码已过期,请重新扫码')
|
||||
fetchQrCode()
|
||||
} else if (result.status === 'cancelled') {
|
||||
stopPolling()
|
||||
ElMessage.info('扫码已取消')
|
||||
scanStatus.value = 'waiting'
|
||||
fetchQrCode()
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('轮询扫码状态失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/** 启动轮询(每 2 秒) */
|
||||
/**
|
||||
* 启动轮询
|
||||
*/
|
||||
function startPolling(): void {
|
||||
stopPolling()
|
||||
pollTimer = setInterval(pollQrCode, 2000)
|
||||
pollTimer = setInterval(pollScanStatus, 2000)
|
||||
}
|
||||
|
||||
/** 停止轮询 */
|
||||
/**
|
||||
* 停止轮询
|
||||
*/
|
||||
function stopPolling(): void {
|
||||
if (pollTimer) {
|
||||
clearInterval(pollTimer)
|
||||
@@ -437,42 +174,160 @@ function stopPolling(): void {
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理登录
|
||||
* 处理登录成功
|
||||
*/
|
||||
async function handleLogin(): Promise<void> {
|
||||
// 表单校验
|
||||
const valid = await formRef.value?.validate().catch(() => false)
|
||||
if (!valid) return
|
||||
|
||||
errorMsg.value = ''
|
||||
async function handleLoginSuccess(token: string, userInfo?: any): Promise<void> {
|
||||
loading.value = true
|
||||
loadingText.value = '登录中...'
|
||||
|
||||
try {
|
||||
await adminStore.login(
|
||||
loginForm.userId.trim(),
|
||||
loginForm.password,
|
||||
loginForm.otpCode.trim() || undefined
|
||||
)
|
||||
} catch (error: unknown) {
|
||||
// 检查是否需要 OTP 验证
|
||||
if (error instanceof Error && error.message === 'require_otp') {
|
||||
requireOtp.value = true
|
||||
loginForm.otpCode = ''
|
||||
ElMessage.info('请输入 OTP 验证码')
|
||||
// 检查是否需要账号绑定
|
||||
if (userInfo?.is_new_user) {
|
||||
// 跳转绑定页面,传递用户信息
|
||||
router.push({
|
||||
name: 'Bind',
|
||||
query: {
|
||||
userid: userInfo.userid,
|
||||
name: userInfo.name,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
const errMsg = error instanceof Error ? error.message : '登录失败,请重试'
|
||||
errorMsg.value = errMsg
|
||||
// 保存 token
|
||||
localStorage.setItem('admin_token', token)
|
||||
adminStore.token = token
|
||||
if (userInfo) {
|
||||
localStorage.setItem('admin_user_id', userInfo.userid)
|
||||
adminStore.adminUserId = userInfo.userid
|
||||
adminStore.adminInfo = {
|
||||
user_id: userInfo.userid,
|
||||
name: userInfo.name,
|
||||
status: 'online',
|
||||
}
|
||||
}
|
||||
|
||||
ElMessage.success('登录成功')
|
||||
router.push('/')
|
||||
} catch (error) {
|
||||
console.error('登录失败:', error)
|
||||
ElMessage.error('登录失败,请重试')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时检测企微客户端状态 + 获取二维码
|
||||
onMounted(() => {
|
||||
checkWecomClient()
|
||||
fetchQrCode()
|
||||
/**
|
||||
* 处理 OAuth2 回调
|
||||
*/
|
||||
async function processOAuthCallback(code: string, state?: string): Promise<void> {
|
||||
loading.value = true
|
||||
loadingText.value = '授权中...'
|
||||
|
||||
try {
|
||||
const result = await handleOAuthCallback(code, state)
|
||||
|
||||
if (result.is_new_user) {
|
||||
// 需要账号绑定
|
||||
router.push({
|
||||
name: 'Bind',
|
||||
query: {
|
||||
userid: result.user_info.userid,
|
||||
name: result.user_info.name,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 登录成功
|
||||
localStorage.setItem('admin_token', result.token)
|
||||
adminStore.token = result.token
|
||||
localStorage.setItem('admin_user_id', result.user_info.userid)
|
||||
adminStore.adminUserId = result.user_info.userid
|
||||
adminStore.adminInfo = {
|
||||
user_id: result.user_info.userid,
|
||||
name: result.user_info.name,
|
||||
status: 'online',
|
||||
}
|
||||
|
||||
ElMessage.success('登录成功')
|
||||
router.push('/')
|
||||
} catch (error) {
|
||||
console.error('OAuth回调处理失败:', error)
|
||||
ElMessage.error('授权失败,请重试')
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳转到 OAuth2 授权页面
|
||||
*/
|
||||
async function redirectToOAuth(): Promise<void> {
|
||||
const currentRedirectUri = window.location.origin + '/itadmin/'
|
||||
|
||||
try {
|
||||
const { getOAuthAuthorizeUrl } = await import('@/api/auth')
|
||||
const data = await getOAuthAuthorizeUrl(currentRedirectUri)
|
||||
if (data.authorize_url) {
|
||||
window.location.href = data.authorize_url
|
||||
return
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('从后端获取授权URL失败,尝试本地构造:', error)
|
||||
}
|
||||
|
||||
// 降级:本地构造授权URL
|
||||
const corpId = import.meta.env.VITE_WECOM_CORP_ID || ''
|
||||
if (!corpId) {
|
||||
console.warn('未配置 VITE_WECOM_CORP_ID,无法跳转授权')
|
||||
return
|
||||
}
|
||||
|
||||
const redirectUri = encodeURIComponent(currentRedirectUri)
|
||||
const oauthUrl = `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${corpId}&redirect_uri=${redirectUri}&response_type=code&scope=snsapi_base&state=STATE#wechat_redirect`
|
||||
window.location.href = oauthUrl
|
||||
}
|
||||
|
||||
// --------------------------------------------------------------------------
|
||||
// 生命周期
|
||||
// --------------------------------------------------------------------------
|
||||
|
||||
onMounted(async () => {
|
||||
// 检查 URL 中的 code 参数(OAuth2 回调)
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const code = urlParams.get('code')
|
||||
const state = urlParams.get('state')
|
||||
|
||||
if (code) {
|
||||
// 有 OAuth2 code,处理回调
|
||||
await processOAuthCallback(code, state || undefined)
|
||||
return
|
||||
}
|
||||
|
||||
// 检测企微环境
|
||||
if (isWecomEnv()) {
|
||||
loading.value = true
|
||||
loadingText.value = '正在跳转企微授权...'
|
||||
|
||||
try {
|
||||
// 企微内,跳转 OAuth2 授权
|
||||
await redirectToOAuth()
|
||||
} catch (error) {
|
||||
console.error('跳转授权失败:', error)
|
||||
// 降级显示扫码登录
|
||||
showQrPanel.value = true
|
||||
fetchQrCode()
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
} else {
|
||||
// 企微外,显示扫码登录
|
||||
showQrPanel.value = true
|
||||
fetchQrCode()
|
||||
}
|
||||
})
|
||||
|
||||
// 页面卸载时停止轮询
|
||||
onUnmounted(() => {
|
||||
stopPolling()
|
||||
})
|
||||
@@ -549,121 +404,63 @@ onUnmounted(() => {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* 登录按钮 */
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* 提示信息 */
|
||||
.login-tips {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
margin-top: 20px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
/* 登录方式选择 */
|
||||
.login-options {
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.login-options-title {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.login-options-btns {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.option-btn {
|
||||
width: 100%;
|
||||
height: 44px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
/* 扫码登录 */
|
||||
.qr-login {
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.qr-title {
|
||||
font-size: 16px;
|
||||
color: var(--text-primary);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.qr-code {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
.qr-container {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
margin: 0 auto 16px;
|
||||
background: var(--bg-secondary);
|
||||
border-radius: 8px;
|
||||
background: var(--bg-tertiary);
|
||||
border-radius: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.qr-code img {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
}
|
||||
|
||||
.qr-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.qr-hint {
|
||||
font-size: 13px;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
/* 二维码容器 */
|
||||
.qr-container {
|
||||
margin-bottom: 12px;
|
||||
.qr-code img {
|
||||
width: 180px;
|
||||
height: 180px;
|
||||
}
|
||||
|
||||
.qr-error {
|
||||
color: var(--text-muted);
|
||||
text-align: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
/* 分隔线 */
|
||||
.divider {
|
||||
.qr-hint {
|
||||
font-size: 14px;
|
||||
color: var(--text-muted);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.scan-tip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 16px 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
.divider::before,
|
||||
.divider::after {
|
||||
content: '';
|
||||
flex: 1;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.divider span {
|
||||
padding: 0 12px;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
color: var(--success-color, #07c160);
|
||||
font-size: 14px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
/* 账号密码登录按钮 */
|
||||
.password-login-btn {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/* 账号密码登录面板 */
|
||||
.password-login {
|
||||
margin-top: 8px;
|
||||
/* 加载状态 */
|
||||
.loading-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
padding: 40px 0;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user