804812dd7e
- 新增 GET /api/wecom/oauth-url 和 /api/wecom/oauth-callback - OAuth回调返回HTML页面直接写localStorage(避坑企微webview 307重定向不可靠) - 菜单URL改为OAuth静默授权链接(snsapi_base + agentid) - redirect目标编码到state参数中(避坑redirect_uri不允许自定义query) - wecom_userid大小写不敏感匹配 - 前端路由守卫处理 ?token= 自动登录(fallback) - Login.vue企微环境检测自动跳OAuth - setup-menu同步更新为OAuth链接 Co-Authored-By: Claude <noreply@anthropic.com>
225 lines
6.0 KiB
Vue
225 lines
6.0 KiB
Vue
<script setup lang="ts">
|
|
import { ref, onMounted } from 'vue'
|
|
import { useRouter, useRoute } from 'vue-router'
|
|
import { useAuthStore } from '@/stores/auth'
|
|
import { ElMessage } from 'element-plus'
|
|
|
|
const router = useRouter()
|
|
const route = useRoute()
|
|
const auth = useAuthStore()
|
|
const loading = ref(false)
|
|
|
|
function isWecom(): boolean {
|
|
const ua = navigator.userAgent || ''
|
|
return /wxwork/i.test(ua)
|
|
}
|
|
|
|
function isMobileDevice(): boolean {
|
|
const ua = navigator.userAgent || ''
|
|
return /Android|iPhone|iPad|iPod|webOS/i.test(ua) || window.innerWidth < 768
|
|
}
|
|
|
|
function getHomePath(): string {
|
|
// 客户经理永远走移动端;支局长/分管领导根据设备自动适配
|
|
if (auth.isManager) return '/m'
|
|
return isMobileDevice() ? '/m' : '/'
|
|
}
|
|
|
|
function goCasdoorLogin() {
|
|
const returnUrl = (route.query.redirect as string) || ''
|
|
if (returnUrl) {
|
|
sessionStorage.setItem('login_return_url', returnUrl)
|
|
}
|
|
const endpoint = import.meta.env.VITE_CASDOOR_ENDPOINT
|
|
const clientId = import.meta.env.VITE_CASDOOR_CLIENT_ID
|
|
const redirectUri = encodeURIComponent(window.location.origin + '/login')
|
|
const casdoorUrl = `${endpoint}/login/oauth/authorize?client_id=${clientId}&response_type=code&redirect_uri=${redirectUri}&scope=openid+profile&state=login`
|
|
window.location.href = casdoorUrl
|
|
}
|
|
|
|
onMounted(async () => {
|
|
// ── Casdoor callback ──
|
|
const code = route.query.code as string
|
|
const state = route.query.state as string
|
|
if (code) {
|
|
loading.value = true
|
|
try {
|
|
await auth.casdoorLogin(code, state)
|
|
const returnUrl = sessionStorage.getItem('login_return_url')
|
|
if (returnUrl) { sessionStorage.removeItem('login_return_url'); router.push(returnUrl); return }
|
|
ElMessage.success('登录成功')
|
|
router.push(getHomePath())
|
|
} catch (e: any) {
|
|
ElMessage.error('登录失败: ' + (e.response?.data?.detail || e.message))
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
return
|
|
}
|
|
|
|
// ── Wecom callback ──
|
|
const wecomCode = route.query.wecom_code as string
|
|
if (wecomCode) {
|
|
loading.value = true
|
|
try {
|
|
const res = await auth.wecomLogin(wecomCode)
|
|
if (res.data.need_bind) {
|
|
window.location.href = res.data.casdoor_url
|
|
return
|
|
}
|
|
auth.saveLogin({
|
|
access_token: res.data.access_token,
|
|
user_id: res.data.user_id,
|
|
name: res.data.name,
|
|
role: res.data.role,
|
|
})
|
|
const returnUrl = sessionStorage.getItem('login_return_url')
|
|
if (returnUrl) { sessionStorage.removeItem('login_return_url'); router.push(returnUrl); return }
|
|
ElMessage.success('登录成功')
|
|
router.push(getHomePath())
|
|
} catch (e: any) {
|
|
ElMessage.error('企微登录失败')
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
return
|
|
}
|
|
|
|
// ── WeChat Work silent login: redirect to OAuth (no user interaction needed) ──
|
|
if (isWecom()) {
|
|
const targetPath = (route.query.redirect as string) || '/m'
|
|
const endpoint = import.meta.env.VITE_CASDOOR_ENDPOINT || ''
|
|
// In production, the backend builds the OAuth URL; use the API
|
|
const apiBase = window.location.origin
|
|
window.location.href = `${apiBase}/api/wecom/oauth-url?redirect=${encodeURIComponent(targetPath)}`
|
|
return
|
|
}
|
|
|
|
// ── Auto-redirect to Casdoor (no button needed) ──
|
|
loading.value = true
|
|
goCasdoorLogin()
|
|
})
|
|
</script>
|
|
|
|
<template>
|
|
<div class="login-page">
|
|
<!-- Decorative ink-wash circles -->
|
|
<div class="bg-orb bg-orb--1"></div>
|
|
<div class="bg-orb bg-orb--2"></div>
|
|
<div class="bg-orb bg-orb--3"></div>
|
|
|
|
<div class="splash-card">
|
|
<div class="brand">
|
|
<h1>企迹</h1>
|
|
<div class="brand-rule"></div>
|
|
<p>政企周报管理系统</p>
|
|
</div>
|
|
<div class="redirect-hint">
|
|
<span class="dot-pulse"></span>
|
|
正在跳转至统一认证...
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
/* ═══ Background — editorial ink wash ═══ */
|
|
.login-page {
|
|
display: flex;
|
|
justify-content: center;
|
|
align-items: center;
|
|
height: 100vh;
|
|
background: var(--ink);
|
|
position: relative;
|
|
overflow: hidden;
|
|
font-family: var(--font-body);
|
|
}
|
|
|
|
/* Decorative orbs — subtle ink-wash circles */
|
|
.bg-orb {
|
|
position: absolute;
|
|
border-radius: 50%;
|
|
pointer-events: none;
|
|
}
|
|
.bg-orb--1 {
|
|
width: 600px; height: 600px;
|
|
background: radial-gradient(circle, rgba(196,147,74,0.06) 0%, transparent 70%);
|
|
top: -180px; right: -180px;
|
|
}
|
|
.bg-orb--2 {
|
|
width: 400px; height: 400px;
|
|
background: radial-gradient(circle, rgba(255,255,255,0.03) 0%, transparent 70%);
|
|
bottom: -100px; left: -80px;
|
|
}
|
|
.bg-orb--3 {
|
|
width: 200px; height: 200px;
|
|
background: radial-gradient(circle, rgba(196,147,74,0.08) 0%, transparent 60%);
|
|
top: 50%; left: 50%;
|
|
transform: translate(-50%, -50%);
|
|
}
|
|
|
|
/* ═══ Splash Card ═══ */
|
|
.splash-card {
|
|
position: relative;
|
|
z-index: 1;
|
|
text-align: center;
|
|
animation: fade-in 0.6s ease-out;
|
|
}
|
|
|
|
@keyframes fade-in {
|
|
from { opacity: 0; transform: translateY(12px); }
|
|
to { opacity: 1; transform: translateY(0); }
|
|
}
|
|
|
|
/* ═══ Brand ═══ */
|
|
.brand {
|
|
margin-bottom: 40px;
|
|
}
|
|
.brand h1 {
|
|
margin: 0;
|
|
font-family: var(--font-heading);
|
|
font-size: 48px;
|
|
font-weight: 400;
|
|
letter-spacing: 0.15em;
|
|
color: #fff;
|
|
line-height: 1.2;
|
|
}
|
|
.brand-rule {
|
|
width: 36px; height: 3px;
|
|
background: var(--gold);
|
|
margin: 16px auto;
|
|
}
|
|
.brand p {
|
|
margin: 0;
|
|
font-family: var(--font-body);
|
|
font-size: 13px;
|
|
color: rgba(255,255,255,0.35);
|
|
letter-spacing: 0.15em;
|
|
}
|
|
|
|
/* ═══ Redirect Hint ═══ */
|
|
.redirect-hint {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 10px;
|
|
font-family: var(--font-body);
|
|
font-size: 13px;
|
|
color: rgba(255,255,255,0.45);
|
|
letter-spacing: 0.06em;
|
|
}
|
|
|
|
/* Pulsing dot animation */
|
|
.dot-pulse {
|
|
display: inline-block;
|
|
width: 6px; height: 6px;
|
|
background: var(--gold);
|
|
border-radius: 50%;
|
|
animation: pulse 1.2s ease-in-out infinite;
|
|
}
|
|
@keyframes pulse {
|
|
0%, 100% { opacity: 0.3; transform: scale(0.8); }
|
|
50% { opacity: 1; transform: scale(1.2); }
|
|
}
|
|
</style>
|