Merge feature/mzl-dev: 修复飞书免登功能
This commit is contained in:
@@ -560,4 +560,47 @@ HTML;
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取飞书配置(用于前端免登)
|
||||
* 根据 company 参数返回对应的 appId
|
||||
*/
|
||||
public function feishuConfig(Request $request): Response
|
||||
{
|
||||
$companyParam = $request->get('company');
|
||||
|
||||
if (!$companyParam) {
|
||||
return $this->error('缺少 company 参数');
|
||||
}
|
||||
|
||||
try {
|
||||
// company 参数是 company_id
|
||||
$company = \app\model\Company::find($companyParam);
|
||||
if (!$company) {
|
||||
return $this->error('公司不存在');
|
||||
}
|
||||
|
||||
$companyId = $company->id;
|
||||
|
||||
// 获取该公司的默认飞书应用
|
||||
$feishuApp = \app\model\CompanyFeishuApp::where('company_id', $companyId)
|
||||
->where('system_id', 1)
|
||||
->where('status', 1)
|
||||
->first();
|
||||
|
||||
if (!$feishuApp) {
|
||||
return $this->error('该公司未配置飞书应用');
|
||||
}
|
||||
|
||||
error_log("✅ feishuConfig - company_id={$companyId}, app_id={$feishuApp->app_id}");
|
||||
|
||||
return $this->success([
|
||||
'app_id' => $feishuApp->app_id,
|
||||
'company_id' => $companyId,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
error_log("❌ feishuConfig 失败: " . $e->getMessage());
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ Route::group('/api/v1', function () {
|
||||
Route::post('/auth/feishu-login', [app\controller\AuthController::class, 'feishuLogin']);
|
||||
Route::post('/auth/qrcode-login', [app\controller\AuthController::class, 'qrcodeLogin']);
|
||||
Route::get('/auth/qrcode-login/status', [app\controller\AuthController::class, 'qrcodeLoginStatus']);
|
||||
Route::get('/auth/feishu-config', [app\controller\AuthController::class, 'feishuConfig']);
|
||||
|
||||
// 测试接口
|
||||
Route::get('/test/notification', [app\controller\TestController::class, 'notification']);
|
||||
|
||||
+4
-43
@@ -4,50 +4,11 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>frontend</title>
|
||||
<!-- 飞书扫码登录 SDK -->
|
||||
<title>企业工具台</title>
|
||||
<!-- 飞书扫码登录 SDK(飞书外扫码登录用) -->
|
||||
<script src="https://lf-package-cn.feishucdn.com/obj/feishu-static/lark/passport/qrcode/LarkSSOSDKWebQRCode-1.0.3.js"></script>
|
||||
<!-- 飞书 H5 JSBridge SDK(多 CDN 备用) -->
|
||||
<script>
|
||||
// 尝试加载飞书 JSBridge,如果失败则尝试其他 CDN
|
||||
var scripts = [
|
||||
'https://lf1-cdn-tos.bytegoofy.com/goofy/lark/passport/h5-jsbridge-1.0.3.js',
|
||||
'https://lf9-cn.feishucdn.com/obj/lark/passport/h5-jsbridge-1.0.3.js'
|
||||
];
|
||||
|
||||
function loadScript(url) {
|
||||
return new Promise(function(resolve, reject) {
|
||||
var script = document.createElement('script');
|
||||
script.src = url;
|
||||
script.onload = function() { console.log('JSBridge loaded from:', url); resolve(); };
|
||||
script.onerror = function() { reject(url); };
|
||||
document.head.appendChild(script);
|
||||
});
|
||||
}
|
||||
|
||||
// 按顺序尝试加载 JSBridge
|
||||
function loadJSBridge() {
|
||||
var index = 0;
|
||||
function tryNext() {
|
||||
if (index < scripts.length) {
|
||||
loadScript(scripts[index])
|
||||
.catch(function() {
|
||||
console.warn('JSBridge load failed:', scripts[index]);
|
||||
index++;
|
||||
tryNext();
|
||||
});
|
||||
} else {
|
||||
console.error('All JSBridge CDNs failed');
|
||||
}
|
||||
}
|
||||
tryNext();
|
||||
}
|
||||
|
||||
// 页面加载后尝试加载 JSBridge
|
||||
if (navigator.userAgent.includes('Lark') || navigator.userAgent.includes('feishu')) {
|
||||
loadJSBridge();
|
||||
}
|
||||
</script>
|
||||
<!-- 飞书 H5 JSSDK(网页应用免登用) -->
|
||||
<script src="https://lf1-cdn-tos.bytegoofy.com/goofy/lark/op/h5-js-sdk-1.5.19.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
@@ -0,0 +1,270 @@
|
||||
.page {
|
||||
min-height: 100vh;
|
||||
background: #f5f5f5;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 5px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
text-align: center;
|
||||
color: #666;
|
||||
margin-bottom: 20px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-bottom: 16px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.btn {
|
||||
flex: 1;
|
||||
min-width: 100px;
|
||||
padding: 10px 16px;
|
||||
border: 1px solid #d9d9d9;
|
||||
border-radius: 6px;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
border-color: #1677ff;
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.btnPrimary {
|
||||
flex: 1;
|
||||
min-width: 100px;
|
||||
padding: 10px 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
background: #1677ff;
|
||||
color: white;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.btnPrimary:hover {
|
||||
background: #4096ff;
|
||||
}
|
||||
|
||||
.checkInfo {
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
.warning {
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
.detail {
|
||||
margin-top: 8px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.tip {
|
||||
margin-top: 8px;
|
||||
padding: 8px 12px;
|
||||
background: #fffbe6;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
color: #856404;
|
||||
}
|
||||
|
||||
.infoRow {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
.infoRow:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
flex-shrink: 0;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #666;
|
||||
word-break: break-all;
|
||||
text-align: right;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.userAgent {
|
||||
padding: 10px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
word-break: break-all;
|
||||
font-family: monospace;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.objectsList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.objectItem {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 12px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.objectName {
|
||||
font-family: monospace;
|
||||
color: #333;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.objectType {
|
||||
color: #1677ff;
|
||||
}
|
||||
|
||||
.authCode {
|
||||
padding: 12px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
word-break: break-all;
|
||||
color: #333;
|
||||
border: 1px solid #d9d9d9;
|
||||
}
|
||||
|
||||
.suggestions {
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.suggestionItem {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.suggestionItem:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.suggestionItem strong {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.suggestionItem ul {
|
||||
margin: 0;
|
||||
padding-left: 20px;
|
||||
}
|
||||
|
||||
.suggestionItem li {
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
/* 日志容器 */
|
||||
.logContainer {
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
background: #1e1e1e;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.noLogs {
|
||||
color: #666;
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.logEntry {
|
||||
display: flex;
|
||||
padding: 4px 0;
|
||||
line-height: 1.5;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.logEntry.info {
|
||||
color: #d4d4d4;
|
||||
}
|
||||
|
||||
.logEntry.success {
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
.logEntry.error {
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.logEntry.warning {
|
||||
color: #fbbf24;
|
||||
}
|
||||
|
||||
.logTime {
|
||||
color: #6b7280;
|
||||
margin-right: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.logMessage {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.logContainer::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
|
||||
.logContainer::-webkit-scrollbar-track {
|
||||
background: #2d2d2d;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.logContainer::-webkit-scrollbar-thumb {
|
||||
background: #555;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.logContainer::-webkit-scrollbar-thumb:hover {
|
||||
background: #777;
|
||||
}
|
||||
@@ -0,0 +1,487 @@
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import { Card } from 'antd-mobile'
|
||||
import styles from './feishu-env-test.module.css'
|
||||
|
||||
// 声明飞书 JSAPI 类型
|
||||
declare global {
|
||||
interface Window {
|
||||
tt?: any
|
||||
ttjsBridge?: any
|
||||
h5sdk?: {
|
||||
ready: (callback: () => void) => void
|
||||
config: (config: any) => void
|
||||
error?: (handler: (error: any) => void) => void
|
||||
}
|
||||
bridge?: any
|
||||
_tt?: any
|
||||
Lark?: any
|
||||
Feishu?: any
|
||||
}
|
||||
}
|
||||
|
||||
interface EnvInfo {
|
||||
currentUrl: string
|
||||
origin: string
|
||||
pathname: string
|
||||
searchParams: Record<string, string>
|
||||
userAgent: string
|
||||
isInFeishu: boolean
|
||||
ttExists: boolean
|
||||
ttType: string
|
||||
ttMethods: string[]
|
||||
globalObjects: Record<string, string>
|
||||
}
|
||||
|
||||
interface LogEntry {
|
||||
time: string
|
||||
message: string
|
||||
type: 'info' | 'success' | 'error' | 'warning'
|
||||
}
|
||||
|
||||
export default function FeishuEnvTest() {
|
||||
const [envInfo, setEnvInfo] = useState<EnvInfo | null>(null)
|
||||
const [ttReady, setTtReady] = useState(false)
|
||||
const [authCode, setAuthCode] = useState<string>('')
|
||||
const [testResult, setTestResult] = useState<string>('')
|
||||
const [checkingCount, setCheckingCount] = useState(0)
|
||||
const [logs, setLogs] = useState<LogEntry[]>([])
|
||||
const [appId, setAppId] = useState<string>('')
|
||||
const pollingRef = useRef<NodeJS.Timeout | null>(null)
|
||||
|
||||
// 添加日志
|
||||
const addLog = (message: string, type: 'info' | 'success' | 'error' | 'warning' = 'info') => {
|
||||
const now = new Date()
|
||||
const time = `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}:${now.getSeconds().toString().padStart(2, '0')}.${now.getMilliseconds().toString().padStart(3, '0')}`
|
||||
setLogs(prev => [...prev, { time, message, type }])
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
addLog('=== 页面加载完成 ===', 'info')
|
||||
checkEnvironment()
|
||||
// 移除自动轮询,避免不停地检查
|
||||
}, [])
|
||||
|
||||
const checkEnvironment = () => {
|
||||
addLog('🔍 开始检查环境...', 'info')
|
||||
|
||||
const ua = navigator.userAgent
|
||||
const url = new URL(window.location.href)
|
||||
const searchParams: Record<string, string> = {}
|
||||
url.searchParams.forEach((value, key) => {
|
||||
searchParams[key] = value
|
||||
})
|
||||
|
||||
// 检查 UA
|
||||
const isInFeishu = ua.includes('Lark') || ua.includes('feishu') || ua.includes('lark')
|
||||
addLog(`📱 User Agent 检测: ${isInFeishu ? '✅ 飞书环境' : '❌ 非飞书环境'}`, isInFeishu ? 'success' : 'warning')
|
||||
|
||||
// 检查 URL
|
||||
addLog(`🔗 当前 URL: ${window.location.href}`, 'info')
|
||||
addLog(`🔗 域名: ${window.location.origin}`, 'info')
|
||||
addLog(`🔗 路径: ${window.location.pathname}`, 'info')
|
||||
|
||||
// 检查参数
|
||||
if (Object.keys(searchParams).length > 0) {
|
||||
addLog(`📋 URL 参数: ${JSON.stringify(searchParams)}`, 'info')
|
||||
}
|
||||
|
||||
// 检查所有可能的飞书相关全局对象
|
||||
addLog('🔍 检查全局对象...', 'info')
|
||||
const globalObjects: Record<string, string> = {}
|
||||
const keysToCheck = ['tt', 'ttjsBridge', 'h5sdk', 'bridge', '_tt', 'Lark', 'Feishu']
|
||||
keysToCheck.forEach(key => {
|
||||
if ((window as any)[key] !== undefined) {
|
||||
globalObjects[key] = typeof (window as any)[key]
|
||||
addLog(` ✓ window.${key} = ${typeof (window as any)[key]}`, 'success')
|
||||
} else {
|
||||
addLog(` ✗ window.${key} = undefined`, 'info')
|
||||
}
|
||||
})
|
||||
|
||||
// 检查 tt 对象
|
||||
let ttType = 'undefined'
|
||||
let ttMethods: string[] = []
|
||||
if (window.tt) {
|
||||
ttType = typeof window.tt
|
||||
addLog(`✅ tt 对象存在!类型: ${ttType}`, 'success')
|
||||
try {
|
||||
ttMethods = Object.keys(window.tt).filter(k => typeof window.tt[k] === 'function')
|
||||
addLog(` 可用方法 (${ttMethods.length} 个): ${ttMethods.slice(0, 5).join(', ')}${ttMethods.length > 5 ? '...' : ''}`, 'info')
|
||||
} catch (e) {
|
||||
addLog(` 无法获取方法列表: ${(e as Error).message}`, 'warning')
|
||||
}
|
||||
} else {
|
||||
addLog('❌ tt 对象不存在', 'error')
|
||||
}
|
||||
|
||||
// 检查 h5sdk 对象
|
||||
if (window.h5sdk) {
|
||||
addLog(`✅ h5sdk 对象存在!类型: ${typeof window.h5sdk}`, 'success')
|
||||
try {
|
||||
const h5sdkMethods = Object.keys(window.h5sdk).filter(k => typeof window.h5sdk[k] === 'function')
|
||||
addLog(` h5sdk 可用方法 (${h5sdkMethods.length} 个): ${h5sdkMethods.join(', ')}`, 'info')
|
||||
addLog(` has ready(): ${typeof window.h5sdk.ready === 'function' ? '✅ 是' : '❌ 否'}`, 'info')
|
||||
addLog(` has config(): ${typeof window.h5sdk.config === 'function' ? '✅ 是' : '❌ 否'}`, 'info')
|
||||
addLog(` has error(): ${typeof window.h5sdk.error === 'function' ? '✅ 是' : '❌ 否'}`, 'info')
|
||||
} catch (e) {
|
||||
addLog(` 无法获取 h5sdk 方法列表: ${(e as Error).message}`, 'warning')
|
||||
}
|
||||
} else {
|
||||
addLog('❌ h5sdk 对象不存在', 'warning')
|
||||
}
|
||||
|
||||
setEnvInfo({
|
||||
currentUrl: window.location.href,
|
||||
origin: window.location.origin,
|
||||
pathname: window.location.pathname,
|
||||
searchParams,
|
||||
userAgent: ua,
|
||||
isInFeishu,
|
||||
ttExists: !!window.tt,
|
||||
ttType,
|
||||
ttMethods,
|
||||
globalObjects,
|
||||
})
|
||||
|
||||
setCheckingCount(prev => prev + 1)
|
||||
addLog(`✅ 环境检查完成 (第 ${checkingCount + 1} 次)`, 'info')
|
||||
}
|
||||
|
||||
const testAuthCode = () => {
|
||||
addLog('🚀 开始测试免登...', 'info')
|
||||
|
||||
if (!window.tt) {
|
||||
addLog('❌ tt 对象不存在,无法测试免登', 'error')
|
||||
setTestResult('❌ tt 对象不存在,无法测试')
|
||||
return
|
||||
}
|
||||
|
||||
if (!appId) {
|
||||
addLog('❌ 请先输入 appId', 'error')
|
||||
setTestResult('❌ 请先输入 appId')
|
||||
return
|
||||
}
|
||||
|
||||
addLog(`📞 调用 tt.requestAuthCode (appId: ${appId.substring(0, 10)}...)`, 'info')
|
||||
setTestResult('正在调用 tt.requestAuthCode...')
|
||||
|
||||
try {
|
||||
window.tt.requestAuthCode({
|
||||
appId: appId,
|
||||
success: (res: any) => {
|
||||
addLog(`✅ requestAuthCode 成功!`, 'success')
|
||||
addLog(` 授权码: ${res.code?.substring(0, 30)}...`, 'info')
|
||||
addLog(` 完整响应: ${JSON.stringify(res)}`, 'info')
|
||||
setAuthCode(res.code || '无授权码')
|
||||
setTestResult('✅ 成功获取授权码: ' + (res.code?.substring(0, 20) || '无') + '...')
|
||||
},
|
||||
fail: (err: any) => {
|
||||
addLog(`❌ requestAuthCode 失败: ${JSON.stringify(err)}`, 'error')
|
||||
setTestResult('❌ 获取授权码失败: ' + JSON.stringify(err))
|
||||
},
|
||||
})
|
||||
} catch (e: any) {
|
||||
addLog(`❌ 调用异常: ${e.message}`, 'error')
|
||||
setTestResult('❌ 调用异常: ' + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 测试 h5sdk.ready()
|
||||
const testH5sdkReady = () => {
|
||||
addLog('🚀 开始测试 h5sdk.ready()...', 'info')
|
||||
|
||||
if (!window.h5sdk) {
|
||||
addLog('❌ h5sdk 对象不存在,无法测试', 'error')
|
||||
setTestResult('❌ h5sdk 对象不存在')
|
||||
return
|
||||
}
|
||||
|
||||
if (typeof window.h5sdk.ready !== 'function') {
|
||||
addLog('❌ h5sdk.ready 不是函数', 'error')
|
||||
setTestResult('❌ h5sdk.ready 不可用')
|
||||
return
|
||||
}
|
||||
|
||||
addLog('📞 调用 h5sdk.ready()...', 'info')
|
||||
setTestResult('正在调用 h5sdk.ready()...')
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
addLog('⚠️ h5sdk.ready() 5秒内未触发回调', 'warning')
|
||||
setTestResult('⚠️ h5sdk.ready() 超时')
|
||||
}, 5000)
|
||||
|
||||
try {
|
||||
window.h5sdk.ready(() => {
|
||||
clearTimeout(timeoutId)
|
||||
addLog('✅ h5sdk.ready() 回调已触发!', 'success')
|
||||
addLog(` tt 对象状态: ${window.tt ? '✅ 存在' : '❌ 不存在'}`, window.tt ? 'success' : 'error')
|
||||
if (window.tt) {
|
||||
const ttMethods = Object.keys(window.tt).filter(k => typeof window.tt[k] === 'function')
|
||||
addLog(` tt 可用方法 (${ttMethods.length} 个): ${ttMethods.slice(0, 5).join(', ')}${ttMethods.length > 5 ? '...' : ''}`, 'info')
|
||||
}
|
||||
setTestResult('✅ h5sdk.ready() 成功,tt 对象' + (window.tt ? '已注入' : '仍未注入'))
|
||||
setTtReady(!!window.tt)
|
||||
checkEnvironment()
|
||||
})
|
||||
|
||||
// 设置错误处理
|
||||
if (window.h5sdk.error) {
|
||||
window.h5sdk.error((error: any) => {
|
||||
clearTimeout(timeoutId)
|
||||
addLog(`❌ h5sdk error: ${JSON.stringify(error)}`, 'error')
|
||||
setTestResult('❌ h5sdk 报错: ' + JSON.stringify(error))
|
||||
})
|
||||
}
|
||||
} catch (e: any) {
|
||||
clearTimeout(timeoutId)
|
||||
addLog(`❌ 调用异常: ${e.message}`, 'error')
|
||||
setTestResult('❌ 调用异常: ' + e.message)
|
||||
}
|
||||
}
|
||||
|
||||
// 开始轮询检查 tt 对象
|
||||
const startPolling = () => {
|
||||
if (pollingRef.current) {
|
||||
addLog('⚠️ 轮询已在运行中', 'warning')
|
||||
return
|
||||
}
|
||||
|
||||
addLog('⏱️ 开始轮询检查 tt 对象(最多 10 秒)...', 'info')
|
||||
setTestResult('开始轮询检查 tt 对象(最多 10 秒)...')
|
||||
|
||||
let attempts = 0
|
||||
const maxAttempts = 100
|
||||
|
||||
pollingRef.current = setInterval(() => {
|
||||
attempts++
|
||||
addLog(` 🔍 第 ${attempts} 次检查 tt 对象...`, 'info')
|
||||
|
||||
if (window.tt) {
|
||||
addLog(`✅ tt 对象已注入!(第 ${attempts} 次检查)`, 'success')
|
||||
if (pollingRef.current) {
|
||||
clearInterval(pollingRef.current)
|
||||
pollingRef.current = null
|
||||
}
|
||||
checkEnvironment()
|
||||
setTestResult(`✅ tt 对象已注入(第 ${attempts} 次检查)`)
|
||||
setTtReady(true)
|
||||
} else if (attempts >= maxAttempts) {
|
||||
addLog(`❌ 超时:tt 对象仍未注入(已检查 ${attempts} 次)`, 'error')
|
||||
if (pollingRef.current) {
|
||||
clearInterval(pollingRef.current)
|
||||
pollingRef.current = null
|
||||
}
|
||||
setTestResult('❌ 超时:tt 对象仍未注入')
|
||||
}
|
||||
}, 100)
|
||||
}
|
||||
|
||||
// 停止轮询
|
||||
const stopPolling = () => {
|
||||
if (pollingRef.current) {
|
||||
clearInterval(pollingRef.current)
|
||||
pollingRef.current = null
|
||||
addLog('⏹️ 已停止轮询', 'info')
|
||||
}
|
||||
}
|
||||
|
||||
// 清空日志
|
||||
const clearLogs = () => {
|
||||
setLogs([])
|
||||
addLog('📝 日志已清空', 'info')
|
||||
}
|
||||
|
||||
if (!envInfo) {
|
||||
return <div className={styles.page}>加载中...</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.page}>
|
||||
<div className={styles.container}>
|
||||
<h1>🔍 飞书环境检测</h1>
|
||||
<p className={styles.subtitle}>用于诊断飞书免登问题</p>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<Card title="操作" className={styles.card}>
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '6px', fontSize: '14px', fontWeight: '500' }}>
|
||||
飞书 AppId(必填):
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={appId}
|
||||
onChange={(e) => setAppId(e.target.value)}
|
||||
placeholder="例如: cli_xxxxxxxxxxxxx"
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px 12px',
|
||||
border: '1px solid #d9d9d9',
|
||||
borderRadius: '6px',
|
||||
fontSize: '14px',
|
||||
boxSizing: 'border-box'
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.buttons}>
|
||||
<button onClick={checkEnvironment} className={styles.btn}>
|
||||
🔄 刷新检测
|
||||
</button>
|
||||
<button onClick={startPolling} className={styles.btn}>
|
||||
⏱️ 开始轮询
|
||||
</button>
|
||||
<button onClick={stopPolling} className={styles.btn}>
|
||||
⏹️ 停止轮询
|
||||
</button>
|
||||
<button onClick={testAuthCode} className={styles.btnPrimary}>
|
||||
🚀 测试免登
|
||||
</button>
|
||||
<button onClick={testH5sdkReady} className={styles.btnPrimary}>
|
||||
🔧 测试 h5sdk.ready
|
||||
</button>
|
||||
<button onClick={clearLogs} className={styles.btn}>
|
||||
🗑️ 清空日志
|
||||
</button>
|
||||
</div>
|
||||
<div className={styles.checkInfo}>
|
||||
已检测次数: {checkingCount} | 轮询状态: {pollingRef.current ? '运行中' : '已停止'}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 实时日志 */}
|
||||
<Card title="📋 实时日志" className={styles.card}>
|
||||
<div className={styles.logContainer}>
|
||||
{logs.length === 0 ? (
|
||||
<div className={styles.noLogs}>暂无日志</div>
|
||||
) : (
|
||||
logs.map((log, index) => (
|
||||
<div key={index} className={`${styles.logEntry} ${styles[log.type]}`}>
|
||||
<span className={styles.logTime}>[{log.time}]</span>
|
||||
<span className={styles.logMessage}>{log.message}</span>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* tt 对象状态 */}
|
||||
<Card title="tt 对象状态" className={styles.card}>
|
||||
{envInfo.ttExists ? (
|
||||
<div className={styles.success}>
|
||||
✅ tt 对象存在
|
||||
<div className={styles.detail}>类型: {envInfo.ttType}</div>
|
||||
{envInfo.ttMethods.length > 0 && (
|
||||
<div className={styles.detail}>
|
||||
可用方法: {envInfo.ttMethods.slice(0, 10).join(', ')}
|
||||
{envInfo.ttMethods.length > 10 && '...'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.error}>
|
||||
❌ tt 对象不存在
|
||||
<div className={styles.tip}>
|
||||
如果在飞书客户端中打开,请尝试"开始轮询"按钮
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* 环境检测 */}
|
||||
<Card title="环境信息" className={styles.card}>
|
||||
<div className={styles.infoRow}>
|
||||
<span className={styles.label}>当前 URL:</span>
|
||||
<span className={styles.value}>{envInfo.currentUrl}</span>
|
||||
</div>
|
||||
<div className={styles.infoRow}>
|
||||
<span className={styles.label}>域名:</span>
|
||||
<span className={styles.value}>{envInfo.origin}</span>
|
||||
</div>
|
||||
<div className={styles.infoRow}>
|
||||
<span className={styles.label}>路径:</span>
|
||||
<span className={styles.value}>{envInfo.pathname}</span>
|
||||
</div>
|
||||
<div className={styles.infoRow}>
|
||||
<span className={styles.label}>在飞书中:</span>
|
||||
<span className={envInfo.isInFeishu ? styles.success : styles.warning}>
|
||||
{envInfo.isInFeishu ? '✅ 是' : '❌ 否'}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.infoRow}>
|
||||
<span className={styles.label}>URL 参数:</span>
|
||||
<span className={styles.value}>
|
||||
{Object.keys(envInfo.searchParams).length > 0
|
||||
? JSON.stringify(envInfo.searchParams)
|
||||
: '(无)'}
|
||||
</span>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* User Agent */}
|
||||
<Card title="User Agent" className={styles.card}>
|
||||
<div className={styles.userAgent}>{envInfo.userAgent}</div>
|
||||
</Card>
|
||||
|
||||
{/* 全局对象 */}
|
||||
<Card title="飞书相关全局对象" className={styles.card}>
|
||||
{Object.keys(envInfo.globalObjects).length > 0 ? (
|
||||
<div className={styles.objectsList}>
|
||||
{Object.entries(envInfo.globalObjects).map(([key, type]) => (
|
||||
<div key={key} className={styles.objectItem}>
|
||||
<span className={styles.objectName}>window.{key}</span>
|
||||
<span className={styles.objectType}>{type}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.warning}>⚠️ 未检测到飞书相关全局对象</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* 免登测试结果 */}
|
||||
{testResult && (
|
||||
<Card title="测试结果" className={styles.card}>
|
||||
<div className={testResult.includes('✅') ? styles.success : styles.error}>
|
||||
{testResult}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 授权码 */}
|
||||
{authCode && (
|
||||
<Card title="获取的授权码" className={styles.card}>
|
||||
<div className={styles.authCode}>{authCode}</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* 诊断建议 */}
|
||||
<Card title="💡 诊断建议" className={styles.card}>
|
||||
<div className={styles.suggestions}>
|
||||
<div className={styles.suggestionItem}>
|
||||
<strong>如果 tt 对象不存在:</strong>
|
||||
<ul>
|
||||
<li>确认从飞书工作台应用图标进入,而非直接打开链接</li>
|
||||
<li>确认飞书开放平台 H5 应用地址配置正确</li>
|
||||
<li>尝试点击"开始轮询"按钮等待 tt 对象注入</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className={styles.suggestionItem}>
|
||||
<strong>如果 UA 检测不是飞书:</strong>
|
||||
<ul>
|
||||
<li>可能飞书客户端的 UA 已更新</li>
|
||||
<li>检查 User Agent 中是否包含 "Lark" 或 "feishu" 关键词</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className={styles.suggestionItem}>
|
||||
<strong>如果免登测试失败:</strong>
|
||||
<ul>
|
||||
<li>检查飞书应用是否获得必要权限</li>
|
||||
<li>检查后端飞书配置是否正确</li>
|
||||
<li>查看浏览器控制台详细错误日志</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Toast } from 'antd-mobile'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
@@ -12,10 +12,17 @@ declare global {
|
||||
tt?: any
|
||||
// 旧版 ttjsBridge(兼容性)
|
||||
ttjsBridge?: any
|
||||
// H5 JSSDK(网页应用免登)
|
||||
h5sdk?: {
|
||||
ready: (callback: () => void) => void
|
||||
config: (config: any) => void
|
||||
error?: (handler: (error: any) => void) => void
|
||||
}
|
||||
// 其他可能的飞书 API
|
||||
h5sdk?: any
|
||||
bridge?: any
|
||||
_tt?: any
|
||||
Lark?: any
|
||||
Feishu?: any
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,9 +56,9 @@ export default function Login() {
|
||||
return
|
||||
}
|
||||
|
||||
// 没有token,继续登录流程
|
||||
console.log('⚠️ 未检测到 token,显示登录页面')
|
||||
setLoading(false)
|
||||
// 没有 token,但不立即显示登录界面
|
||||
// 等待第二个 useEffect 检测飞书环境并尝试免登
|
||||
// 只有在非飞书环境或免登失败时才显示登录界面
|
||||
|
||||
// 监听飞书登录成功事件
|
||||
const handleFeishuLoginSuccess = ((e: any) => {
|
||||
@@ -72,32 +79,113 @@ export default function Login() {
|
||||
const ua = navigator.userAgent
|
||||
const isInFeishu = ua.includes('Lark') || ua.includes('feishu') || ua.includes('lark')
|
||||
const currentUrl = window.location.href
|
||||
const currentOrigin = window.location.origin
|
||||
|
||||
console.log('=== 环境检测 ===')
|
||||
console.log('当前 URL:', currentUrl)
|
||||
console.log('当前域名:', currentOrigin)
|
||||
console.log('User Agent:', ua)
|
||||
console.log('在飞书中:', isInFeishu)
|
||||
console.log('window.tt:', window.tt)
|
||||
|
||||
// 检查域名是否匹配线上环境
|
||||
const expectedDomain = 'uniservice-mobile.hnxinyouhe.com'
|
||||
const isExpectedDomain = currentOrigin.includes(expectedDomain) || currentOrigin.includes('xsm.dev')
|
||||
console.log('域名检查:', {
|
||||
expected: expectedDomain,
|
||||
current: currentOrigin,
|
||||
match: isExpectedDomain
|
||||
})
|
||||
|
||||
// 检查所有可能的飞书相关全局对象
|
||||
const feishuObjects: Record<string, string> = {}
|
||||
for (const key in window) {
|
||||
if (key.toLowerCase().includes('tt') ||
|
||||
key.toLowerCase().includes('lark') ||
|
||||
key.toLowerCase().includes('feishu') ||
|
||||
key.toLowerCase().includes('bridge') ||
|
||||
key.toLowerCase().includes('h5sdk')) {
|
||||
feishuObjects[key] = typeof (window as any)[key]
|
||||
}
|
||||
}
|
||||
console.log('🔍 飞书相关全局对象:', feishuObjects)
|
||||
console.log('🔍 h5sdk 对象:', window.h5sdk)
|
||||
if (window.h5sdk) {
|
||||
console.log(' h5sdk 属性:', Object.keys(window.h5sdk))
|
||||
}
|
||||
|
||||
// 检查 URL 中是否来自飞书消息通知
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
const authCode = urlParams.get('auth_code')
|
||||
const fromFeishu = urlParams.get('from') === 'feishu'
|
||||
|
||||
// 关键修复:在飞书中但 tt 对象不存在时,刷新页面让飞书注入 tt 对象
|
||||
// 增加多次重试机制
|
||||
// 关键修复:在飞书中但 tt 对象不存在时,检查 h5sdk 并等待环境初始化
|
||||
// 网页应用会先注入 h5sdk,然后通过 h5sdk.ready() 等待 tt 对象
|
||||
if (isInFeishu && !window.tt) {
|
||||
const reloadCount = parseInt(urlParams.get('reload') || '0')
|
||||
if (reloadCount < 3) {
|
||||
console.log(`⚠️ 在飞书中但 tt 对象不存在,第 ${reloadCount + 1} 次刷新页面`)
|
||||
const separator = currentUrl.indexOf('?') > 0 ? '&' : '?'
|
||||
// 清除旧的 reload 参数并添加新的
|
||||
const newUrl = currentUrl.split('?')[0] + '?' + urlParams.toString().replace(/reload=\d+/g, '').replace(/^&+/, '')
|
||||
window.location.href = newUrl + (newUrl.includes('?') ? '&' : '?') + `reload=${reloadCount + 1}`
|
||||
console.log('⚠️ 在飞书中但 tt 对象不存在,检查 h5sdk...')
|
||||
|
||||
// 如果有 h5sdk,使用 h5sdk.ready() 等待
|
||||
if (window.h5sdk && typeof window.h5sdk.ready === 'function') {
|
||||
console.log('✅ 检测到 h5sdk,使用 h5sdk.ready() 等待 tt 对象注入')
|
||||
setEnvironment('feishu')
|
||||
|
||||
let readyCalled = false
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (!readyCalled) {
|
||||
console.error('❌ h5sdk.ready() 超时(5秒)')
|
||||
setLoading(false)
|
||||
}
|
||||
}, 5000)
|
||||
|
||||
window.h5sdk.ready(() => {
|
||||
readyCalled = true
|
||||
clearTimeout(timeoutId)
|
||||
console.log('✅ h5sdk.ready() 回调触发,检查 tt 对象:', !!window.tt)
|
||||
if (window.tt) {
|
||||
console.log('✅ tt 对象已注入,开始免登')
|
||||
handleFeishuAutoLogin()
|
||||
} else {
|
||||
console.error('❌ h5sdk.ready() 后 tt 对象仍不存在')
|
||||
setLoading(false)
|
||||
}
|
||||
})
|
||||
|
||||
// 设置错误处理
|
||||
if (window.h5sdk.error) {
|
||||
window.h5sdk.error((error: any) => {
|
||||
console.error('❌ h5sdk error:', error)
|
||||
setLoading(false)
|
||||
})
|
||||
}
|
||||
return
|
||||
} else {
|
||||
console.log('❌ 多次刷新后 tt 对象仍不存在,放弃自动登录')
|
||||
}
|
||||
|
||||
// 如果没有 h5sdk,轮询等待 tt 对象
|
||||
console.log('⚠️ 未检测到 h5sdk,开始轮询等待 tt 对象...')
|
||||
setEnvironment('feishu')
|
||||
|
||||
let attempts = 0
|
||||
const maxAttempts = 100 // 100 * 100ms = 10秒
|
||||
|
||||
const checkInterval = setInterval(() => {
|
||||
attempts++
|
||||
console.log(`🔍 检查 tt 对象 (${attempts}/${maxAttempts}):`, !!window.tt, typeof window.tt)
|
||||
|
||||
if (window.tt && typeof window.tt === 'object') {
|
||||
clearInterval(checkInterval)
|
||||
console.log('✅ tt 对象已注入,开始免登')
|
||||
handleFeishuAutoLogin()
|
||||
} else if (attempts >= maxAttempts) {
|
||||
clearInterval(checkInterval)
|
||||
console.error('❌ 等待 tt 对象超时')
|
||||
console.error('❌ 请检查飞书开放平台配置:')
|
||||
console.error(' 1. H5 可信域名配置是否正确')
|
||||
console.error(' 2. 确认应用已发布')
|
||||
console.error(' 3. 尝试从飞书工作台重新进入应用')
|
||||
console.error(' 4. 检查是否正确加载了 H5 JSSDK')
|
||||
setLoading(false)
|
||||
}
|
||||
}, 100)
|
||||
return
|
||||
}
|
||||
|
||||
if (authCode) {
|
||||
@@ -110,20 +198,33 @@ export default function Login() {
|
||||
setEnvironment('feishu')
|
||||
handleFeishuAutoLogin()
|
||||
} else if (isInFeishu) {
|
||||
// 飞书内环境,等待 tt 对象加载后尝试免登
|
||||
console.log('⏳ 在飞书内环境,等待 tt 对象加载...')
|
||||
// 飞书内环境,tt 对象由飞书客户端自动注入
|
||||
// 使用轮询等待 tt 对象可用(最多 10 秒)
|
||||
console.log('⏳ 在飞书内环境,等待飞书客户端注入 tt 对象...')
|
||||
setEnvironment('feishu')
|
||||
setLoading(false)
|
||||
// 给 tt 对象一些时间来加载(最多 2 秒)
|
||||
setTimeout(() => {
|
||||
if (window.tt) {
|
||||
console.log('✅ tt 对象已加载,开始免登')
|
||||
|
||||
let attempts = 0
|
||||
const maxAttempts = 100 // 100 * 100ms = 10秒
|
||||
|
||||
const checkInterval = setInterval(() => {
|
||||
attempts++
|
||||
console.log(`🔍 检查 tt 对象 (${attempts}/${maxAttempts}):`, !!window.tt, typeof window.tt)
|
||||
|
||||
if (window.tt && typeof window.tt === 'object') {
|
||||
clearInterval(checkInterval)
|
||||
console.log('✅ tt 对象已由飞书客户端注入,开始免登')
|
||||
handleFeishuAutoLogin()
|
||||
} else {
|
||||
console.log('⚠️ tt 对象仍未加载,显示二维码登录')
|
||||
setShowQRCode(true)
|
||||
} else if (attempts >= maxAttempts) {
|
||||
clearInterval(checkInterval)
|
||||
console.error('❌ 等待 tt 对象超时,最后状态 window.tt:', window.tt, typeof window.tt)
|
||||
console.error('❌ 请检查:1) 飞书开放平台是否正确配置 H5 应用地址 2) 页面是否从飞书应用入口进入 3) 应用是否已发布')
|
||||
|
||||
// 免登失败,显示登录界面(飞书内显示提示)
|
||||
setLoading(false)
|
||||
setEnvironment('feishu')
|
||||
// 不显示二维码,而是显示手动登录选项
|
||||
}
|
||||
}, 2000)
|
||||
}, 100)
|
||||
} else {
|
||||
// 飞书外:显示登录选项
|
||||
console.log('📱 飞书外环境,显示登录选项')
|
||||
@@ -135,8 +236,6 @@ export default function Login() {
|
||||
// 飞书内静默授权
|
||||
const handleFeishuAutoLogin = () => {
|
||||
console.log('🚀 开始飞书免登')
|
||||
console.log('🔍 window.tt:', window.tt)
|
||||
console.log('🔍 window.tt 类型:', typeof window.tt)
|
||||
|
||||
// 从 URL 获取 company 参数(可能来自当前 URL 或 redirect URL)
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
@@ -153,60 +252,134 @@ export default function Login() {
|
||||
}
|
||||
}
|
||||
|
||||
// 详细检查 tt 对象
|
||||
if (window.tt) {
|
||||
console.log('✅ tt 对象存在')
|
||||
console.log('🔍 tt 的所有属性:', Object.keys(window.tt))
|
||||
console.log('🔍 tt.requestAuthCode 类型:', typeof window.tt.requestAuthCode)
|
||||
console.log('🔍 window.tt:', window.tt)
|
||||
console.log('🔍 window.h5sdk:', window.h5sdk)
|
||||
console.log('🔍 tt 的属性:', Object.keys(window.tt || {}))
|
||||
|
||||
// 检查 requestAuthCode 是否存在
|
||||
if ('requestAuthCode' in window.tt) {
|
||||
console.log('✅ requestAuthCode 属性存在')
|
||||
console.log('🔍 类型:', typeof window.tt.requestAuthCode)
|
||||
} else {
|
||||
console.log('❌ requestAuthCode 属性不存在')
|
||||
}
|
||||
} else {
|
||||
console.log('❌ tt 对象不存在')
|
||||
setLoading(false)
|
||||
Toast.show('飞书 API 未加载,请刷新重试')
|
||||
// 如果 tt 对象已存在,直接使用(无需等待 h5sdk.ready)
|
||||
if (window.tt) {
|
||||
console.log('✅ tt 对象已存在,直接调用免登')
|
||||
performAuthCodeRequest()
|
||||
return
|
||||
}
|
||||
|
||||
// 等待一小段时间,确保 tt 完全初始化
|
||||
setTimeout(() => {
|
||||
console.log('⏰ 延迟后再次检查 tt:', !!window.tt)
|
||||
// 如果 tt 不存在但 h5sdk 存在,使用 h5sdk.ready() 等待
|
||||
if (window.h5sdk && typeof window.h5sdk.ready === 'function') {
|
||||
console.log('✅ 检测到 h5sdk,使用 h5sdk.ready() 等待 tt 对象')
|
||||
setEnvironment('feishu')
|
||||
|
||||
if (!window.tt) {
|
||||
let readyCalled = false
|
||||
const timeoutId = setTimeout(() => {
|
||||
if (!readyCalled) {
|
||||
console.error('❌ h5sdk.ready() 超时(5秒)')
|
||||
setLoading(false)
|
||||
Toast.show('飞书环境初始化超时,请刷新重试')
|
||||
}
|
||||
}, 5000)
|
||||
|
||||
window.h5sdk.ready(() => {
|
||||
readyCalled = true
|
||||
clearTimeout(timeoutId)
|
||||
console.log('✅ h5sdk.ready() 回调触发,检查 tt 对象:', !!window.tt)
|
||||
if (window.tt) {
|
||||
console.log('✅ tt 对象已注入,开始免登')
|
||||
performAuthCodeRequest()
|
||||
} else {
|
||||
console.error('❌ h5sdk.ready() 后 tt 对象仍不存在')
|
||||
setLoading(false)
|
||||
Toast.show('飞书环境异常,请刷新重试')
|
||||
}
|
||||
})
|
||||
|
||||
// 设置错误处理
|
||||
if (window.h5sdk.error) {
|
||||
window.h5sdk.error((error: any) => {
|
||||
console.error('❌ h5sdk error:', error)
|
||||
setLoading(false)
|
||||
Toast.show('飞书环境异常:' + JSON.stringify(error))
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 都不存在,报错
|
||||
console.error('❌ tt 对象和 h5sdk 都不存在')
|
||||
setLoading(false)
|
||||
Toast.show('飞书环境异常,请刷新重试')
|
||||
}
|
||||
|
||||
// 执行实际的 requestAuthCode 调用
|
||||
const performAuthCodeRequest = async () => {
|
||||
console.log('🔧 准备调用 tt.requestAuthCode')
|
||||
console.log('🔍 当前 window.tt:', window.tt)
|
||||
console.log('🔍 tt 的属性:', Object.keys(window.tt || {}))
|
||||
|
||||
if (!window.tt) {
|
||||
console.error('❌ tt 对象不存在')
|
||||
setLoading(false)
|
||||
Toast.show('飞书环境异常(tt 对象不存在),请刷新重试')
|
||||
return
|
||||
}
|
||||
|
||||
// 获取 appId:优先从 URL 参数,否则从后端获取
|
||||
const urlParams = new URLSearchParams(window.location.search)
|
||||
let appId = urlParams.get('app_id')
|
||||
let companyParam = urlParams.get('company')
|
||||
|
||||
console.log('🔍 URL 参数 - app_id:', appId, 'company:', companyParam)
|
||||
|
||||
// 如果没有 appId 但有 company,从后端获取
|
||||
if (!appId && companyParam) {
|
||||
try {
|
||||
console.log('📡 从后端获取 appId...')
|
||||
const response = await fetch(`/api/v1/auth/feishu-config?company=${companyParam}`)
|
||||
const result = await response.json()
|
||||
|
||||
if (result.code === 0 && result.data.app_id) {
|
||||
appId = result.data.app_id
|
||||
console.log('✅ 从后端获取到 appId:', appId?.substring(0, 15) + '...')
|
||||
} else {
|
||||
console.error('❌ 获取 appId 失败:', result.message)
|
||||
setLoading(false)
|
||||
Toast.show('获取飞书配置失败:' + (result.message || '未知错误'))
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('❌ 获取 appId 异常:', e)
|
||||
setLoading(false)
|
||||
Toast.show('飞书 API 加载失败')
|
||||
Toast.show('获取飞书配置失败,请重试')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 尝试调用 requestAuthCode
|
||||
try {
|
||||
console.log('🔧 调用 tt.requestAuthCode,company:', companyParam)
|
||||
if (!appId) {
|
||||
console.error('❌ 无法获取 appId')
|
||||
setLoading(false)
|
||||
Toast.show('缺少飞书应用配置')
|
||||
return
|
||||
}
|
||||
|
||||
// 不传 appId,让飞书自动选择(后端会根据 company 参数处理)
|
||||
window.tt.requestAuthCode({
|
||||
success: (res: any) => {
|
||||
console.log('✅ 成功获取授权码:', res)
|
||||
console.log('✅ code:', res.code?.substring(0, 30))
|
||||
handleFeishuLogin(res.code)
|
||||
},
|
||||
fail: (error: any) => {
|
||||
console.error('❌ requestAuthCode 失败:', error)
|
||||
console.error('❌ 错误详情:', JSON.stringify(error))
|
||||
setLoading(false)
|
||||
Toast.show('免登失败:' + (error.errorMessage || error.errMsg || JSON.stringify(error)))
|
||||
},
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('❌ 调用异常:', e)
|
||||
setLoading(false)
|
||||
Toast.show('免登失败,请使用其他方式登录')
|
||||
}
|
||||
}, 500)
|
||||
// 调用 tt.requestAuthCode 获取授权码
|
||||
try {
|
||||
console.log('🔧 调用 tt.requestAuthCode (appId:', appId?.substring(0, 15) + '...)')
|
||||
|
||||
window.tt.requestAuthCode({
|
||||
appId: appId,
|
||||
success: (res: any) => {
|
||||
console.log('✅ 成功获取授权码:', res.code?.substring(0, 30))
|
||||
handleFeishuLogin(res.code)
|
||||
},
|
||||
fail: (error: any) => {
|
||||
console.error('❌ requestAuthCode 失败:', error)
|
||||
setLoading(false)
|
||||
Toast.show('免登失败:' + (error.errorMessage || error.errMsg || JSON.stringify(error)))
|
||||
},
|
||||
})
|
||||
} catch (e) {
|
||||
console.error('❌ 调用异常:', e)
|
||||
setLoading(false)
|
||||
Toast.show('免登失败,请重试')
|
||||
}
|
||||
}
|
||||
|
||||
// 跳转到飞书授权页面(飞书外扫码登录)
|
||||
@@ -385,6 +558,26 @@ export default function Login() {
|
||||
<div className={styles.login}>
|
||||
<div className={styles.container}>
|
||||
<h1>企业工具台</h1>
|
||||
|
||||
{/* 飞书内免登失败的提示 */}
|
||||
{environment === 'feishu' && (
|
||||
<div style={{
|
||||
background: '#fff3cd',
|
||||
border: '1px solid #ffc107',
|
||||
borderRadius: '8px',
|
||||
padding: '12px',
|
||||
marginBottom: '20px',
|
||||
fontSize: '14px',
|
||||
color: '#856404',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
<div style={{ marginBottom: '5px', fontWeight: 'bold' }}>⚠️ 飞书免登失败</div>
|
||||
<div style={{ fontSize: '12px' }}>
|
||||
可能原因:1) 应用配置未生效 2) 需要重新从工作台进入
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className={styles.subtitle}>请选择登录方式</p>
|
||||
|
||||
<div className={styles.loginMethods}>
|
||||
@@ -396,13 +589,15 @@ export default function Login() {
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button className={styles.methodBtn} onClick={redirectToFeishu}>
|
||||
<div className={styles.methodIcon}>📱</div>
|
||||
<div className={styles.methodText}>
|
||||
<div className={styles.methodTitle}>飞书扫码登录</div>
|
||||
<div className={styles.methodDesc}>使用飞书 APP 扫码登录</div>
|
||||
</div>
|
||||
</button>
|
||||
{environment !== 'feishu' && (
|
||||
<button className={styles.methodBtn} onClick={redirectToFeishu}>
|
||||
<div className={styles.methodIcon}>📱</div>
|
||||
<div className={styles.methodText}>
|
||||
<div className={styles.methodTitle}>飞书扫码登录</div>
|
||||
<div className={styles.methodDesc}>使用飞书 APP 扫码登录</div>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{environment === 'external' && (
|
||||
|
||||
@@ -16,6 +16,7 @@ import RejectPage from '@/pages/approval/reject'
|
||||
import Login from '@/pages/login'
|
||||
import SimpleLogin from '@/pages/login-simple'
|
||||
import DebugFeishu from '@/pages/debug-feishu'
|
||||
import FeishuEnvTest from '@/pages/feishu-env-test'
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
@@ -43,6 +44,7 @@ const router = createBrowserRouter([
|
||||
{ path: '/login', element: <Login /> },
|
||||
{ path: '/login-simple', element: <SimpleLogin /> },
|
||||
{ path: '/debug-feishu', element: <DebugFeishu /> },
|
||||
{ path: '/feishu-env-test', element: <FeishuEnvTest /> },
|
||||
{ path: '/approval/quick-action', element: <QuickAction /> },
|
||||
{ path: '/approval/reject', element: <RejectPage /> },
|
||||
{ path: '*', element: <Navigate to='/login' replace /> },
|
||||
|
||||
Reference in New Issue
Block a user