Merge feature/mzl-dev: 修复飞书免登功能
This commit is contained in:
@@ -560,4 +560,47 @@ HTML;
|
|||||||
return $this->error($e->getMessage());
|
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/feishu-login', [app\controller\AuthController::class, 'feishuLogin']);
|
||||||
Route::post('/auth/qrcode-login', [app\controller\AuthController::class, 'qrcodeLogin']);
|
Route::post('/auth/qrcode-login', [app\controller\AuthController::class, 'qrcodeLogin']);
|
||||||
Route::get('/auth/qrcode-login/status', [app\controller\AuthController::class, 'qrcodeLoginStatus']);
|
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']);
|
Route::get('/test/notification', [app\controller\TestController::class, 'notification']);
|
||||||
|
|||||||
+4
-43
@@ -4,50 +4,11 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>frontend</title>
|
<title>企业工具台</title>
|
||||||
<!-- 飞书扫码登录 SDK -->
|
<!-- 飞书扫码登录 SDK(飞书外扫码登录用) -->
|
||||||
<script src="https://lf-package-cn.feishucdn.com/obj/feishu-static/lark/passport/qrcode/LarkSSOSDKWebQRCode-1.0.3.js"></script>
|
<script src="https://lf-package-cn.feishucdn.com/obj/feishu-static/lark/passport/qrcode/LarkSSOSDKWebQRCode-1.0.3.js"></script>
|
||||||
<!-- 飞书 H5 JSBridge SDK(多 CDN 备用) -->
|
<!-- 飞书 H5 JSSDK(网页应用免登用) -->
|
||||||
<script>
|
<script src="https://lf1-cdn-tos.bytegoofy.com/goofy/lark/op/h5-js-sdk-1.5.19.js"></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>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="root"></div>
|
<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 { useNavigate } from 'react-router-dom'
|
||||||
import { Toast } from 'antd-mobile'
|
import { Toast } from 'antd-mobile'
|
||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
@@ -12,10 +12,17 @@ declare global {
|
|||||||
tt?: any
|
tt?: any
|
||||||
// 旧版 ttjsBridge(兼容性)
|
// 旧版 ttjsBridge(兼容性)
|
||||||
ttjsBridge?: any
|
ttjsBridge?: any
|
||||||
|
// H5 JSSDK(网页应用免登)
|
||||||
|
h5sdk?: {
|
||||||
|
ready: (callback: () => void) => void
|
||||||
|
config: (config: any) => void
|
||||||
|
error?: (handler: (error: any) => void) => void
|
||||||
|
}
|
||||||
// 其他可能的飞书 API
|
// 其他可能的飞书 API
|
||||||
h5sdk?: any
|
|
||||||
bridge?: any
|
bridge?: any
|
||||||
_tt?: any
|
_tt?: any
|
||||||
|
Lark?: any
|
||||||
|
Feishu?: any
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -49,9 +56,9 @@ export default function Login() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 没有token,继续登录流程
|
// 没有 token,但不立即显示登录界面
|
||||||
console.log('⚠️ 未检测到 token,显示登录页面')
|
// 等待第二个 useEffect 检测飞书环境并尝试免登
|
||||||
setLoading(false)
|
// 只有在非飞书环境或免登失败时才显示登录界面
|
||||||
|
|
||||||
// 监听飞书登录成功事件
|
// 监听飞书登录成功事件
|
||||||
const handleFeishuLoginSuccess = ((e: any) => {
|
const handleFeishuLoginSuccess = ((e: any) => {
|
||||||
@@ -72,32 +79,113 @@ export default function Login() {
|
|||||||
const ua = navigator.userAgent
|
const ua = navigator.userAgent
|
||||||
const isInFeishu = ua.includes('Lark') || ua.includes('feishu') || ua.includes('lark')
|
const isInFeishu = ua.includes('Lark') || ua.includes('feishu') || ua.includes('lark')
|
||||||
const currentUrl = window.location.href
|
const currentUrl = window.location.href
|
||||||
|
const currentOrigin = window.location.origin
|
||||||
|
|
||||||
console.log('=== 环境检测 ===')
|
console.log('=== 环境检测 ===')
|
||||||
console.log('当前 URL:', currentUrl)
|
console.log('当前 URL:', currentUrl)
|
||||||
|
console.log('当前域名:', currentOrigin)
|
||||||
console.log('User Agent:', ua)
|
console.log('User Agent:', ua)
|
||||||
console.log('在飞书中:', isInFeishu)
|
console.log('在飞书中:', isInFeishu)
|
||||||
console.log('window.tt:', window.tt)
|
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 中是否来自飞书消息通知
|
// 检查 URL 中是否来自飞书消息通知
|
||||||
const urlParams = new URLSearchParams(window.location.search)
|
const urlParams = new URLSearchParams(window.location.search)
|
||||||
const authCode = urlParams.get('auth_code')
|
const authCode = urlParams.get('auth_code')
|
||||||
const fromFeishu = urlParams.get('from') === 'feishu'
|
|
||||||
|
|
||||||
// 关键修复:在飞书中但 tt 对象不存在时,刷新页面让飞书注入 tt 对象
|
// 关键修复:在飞书中但 tt 对象不存在时,检查 h5sdk 并等待环境初始化
|
||||||
// 增加多次重试机制
|
// 网页应用会先注入 h5sdk,然后通过 h5sdk.ready() 等待 tt 对象
|
||||||
if (isInFeishu && !window.tt) {
|
if (isInFeishu && !window.tt) {
|
||||||
const reloadCount = parseInt(urlParams.get('reload') || '0')
|
console.log('⚠️ 在飞书中但 tt 对象不存在,检查 h5sdk...')
|
||||||
if (reloadCount < 3) {
|
|
||||||
console.log(`⚠️ 在飞书中但 tt 对象不存在,第 ${reloadCount + 1} 次刷新页面`)
|
// 如果有 h5sdk,使用 h5sdk.ready() 等待
|
||||||
const separator = currentUrl.indexOf('?') > 0 ? '&' : '?'
|
if (window.h5sdk && typeof window.h5sdk.ready === 'function') {
|
||||||
// 清除旧的 reload 参数并添加新的
|
console.log('✅ 检测到 h5sdk,使用 h5sdk.ready() 等待 tt 对象注入')
|
||||||
const newUrl = currentUrl.split('?')[0] + '?' + urlParams.toString().replace(/reload=\d+/g, '').replace(/^&+/, '')
|
setEnvironment('feishu')
|
||||||
window.location.href = newUrl + (newUrl.includes('?') ? '&' : '?') + `reload=${reloadCount + 1}`
|
|
||||||
return
|
let readyCalled = false
|
||||||
} else {
|
const timeoutId = setTimeout(() => {
|
||||||
console.log('❌ 多次刷新后 tt 对象仍不存在,放弃自动登录')
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有 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) {
|
if (authCode) {
|
||||||
@@ -110,20 +198,33 @@ export default function Login() {
|
|||||||
setEnvironment('feishu')
|
setEnvironment('feishu')
|
||||||
handleFeishuAutoLogin()
|
handleFeishuAutoLogin()
|
||||||
} else if (isInFeishu) {
|
} else if (isInFeishu) {
|
||||||
// 飞书内环境,等待 tt 对象加载后尝试免登
|
// 飞书内环境,tt 对象由飞书客户端自动注入
|
||||||
console.log('⏳ 在飞书内环境,等待 tt 对象加载...')
|
// 使用轮询等待 tt 对象可用(最多 10 秒)
|
||||||
|
console.log('⏳ 在飞书内环境,等待飞书客户端注入 tt 对象...')
|
||||||
setEnvironment('feishu')
|
setEnvironment('feishu')
|
||||||
setLoading(false)
|
|
||||||
// 给 tt 对象一些时间来加载(最多 2 秒)
|
let attempts = 0
|
||||||
setTimeout(() => {
|
const maxAttempts = 100 // 100 * 100ms = 10秒
|
||||||
if (window.tt) {
|
|
||||||
console.log('✅ tt 对象已加载,开始免登')
|
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()
|
handleFeishuAutoLogin()
|
||||||
} else {
|
} else if (attempts >= maxAttempts) {
|
||||||
console.log('⚠️ tt 对象仍未加载,显示二维码登录')
|
clearInterval(checkInterval)
|
||||||
setShowQRCode(true)
|
console.error('❌ 等待 tt 对象超时,最后状态 window.tt:', window.tt, typeof window.tt)
|
||||||
|
console.error('❌ 请检查:1) 飞书开放平台是否正确配置 H5 应用地址 2) 页面是否从飞书应用入口进入 3) 应用是否已发布')
|
||||||
|
|
||||||
|
// 免登失败,显示登录界面(飞书内显示提示)
|
||||||
|
setLoading(false)
|
||||||
|
setEnvironment('feishu')
|
||||||
|
// 不显示二维码,而是显示手动登录选项
|
||||||
}
|
}
|
||||||
}, 2000)
|
}, 100)
|
||||||
} else {
|
} else {
|
||||||
// 飞书外:显示登录选项
|
// 飞书外:显示登录选项
|
||||||
console.log('📱 飞书外环境,显示登录选项')
|
console.log('📱 飞书外环境,显示登录选项')
|
||||||
@@ -135,8 +236,6 @@ export default function Login() {
|
|||||||
// 飞书内静默授权
|
// 飞书内静默授权
|
||||||
const handleFeishuAutoLogin = () => {
|
const handleFeishuAutoLogin = () => {
|
||||||
console.log('🚀 开始飞书免登')
|
console.log('🚀 开始飞书免登')
|
||||||
console.log('🔍 window.tt:', window.tt)
|
|
||||||
console.log('🔍 window.tt 类型:', typeof window.tt)
|
|
||||||
|
|
||||||
// 从 URL 获取 company 参数(可能来自当前 URL 或 redirect URL)
|
// 从 URL 获取 company 参数(可能来自当前 URL 或 redirect URL)
|
||||||
const urlParams = new URLSearchParams(window.location.search)
|
const urlParams = new URLSearchParams(window.location.search)
|
||||||
@@ -153,50 +252,125 @@ export default function Login() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 详细检查 tt 对象
|
console.log('🔍 window.tt:', window.tt)
|
||||||
if (window.tt) {
|
console.log('🔍 window.h5sdk:', window.h5sdk)
|
||||||
console.log('✅ tt 对象存在')
|
console.log('🔍 tt 的属性:', Object.keys(window.tt || {}))
|
||||||
console.log('🔍 tt 的所有属性:', Object.keys(window.tt))
|
|
||||||
console.log('🔍 tt.requestAuthCode 类型:', typeof window.tt.requestAuthCode)
|
|
||||||
|
|
||||||
// 检查 requestAuthCode 是否存在
|
// 如果 tt 对象已存在,直接使用(无需等待 h5sdk.ready)
|
||||||
if ('requestAuthCode' in window.tt) {
|
if (window.tt) {
|
||||||
console.log('✅ requestAuthCode 属性存在')
|
console.log('✅ tt 对象已存在,直接调用免登')
|
||||||
console.log('🔍 类型:', typeof window.tt.requestAuthCode)
|
performAuthCodeRequest()
|
||||||
} else {
|
|
||||||
console.log('❌ requestAuthCode 属性不存在')
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
console.log('❌ tt 对象不存在')
|
|
||||||
setLoading(false)
|
|
||||||
Toast.show('飞书 API 未加载,请刷新重试')
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 等待一小段时间,确保 tt 完全初始化
|
// 如果 tt 不存在但 h5sdk 存在,使用 h5sdk.ready() 等待
|
||||||
setTimeout(() => {
|
if (window.h5sdk && typeof window.h5sdk.ready === 'function') {
|
||||||
console.log('⏰ 延迟后再次检查 tt:', !!window.tt)
|
console.log('✅ 检测到 h5sdk,使用 h5sdk.ready() 等待 tt 对象')
|
||||||
|
setEnvironment('feishu')
|
||||||
|
|
||||||
|
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) {
|
if (!window.tt) {
|
||||||
|
console.error('❌ tt 对象不存在')
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
Toast.show('飞书 API 加载失败')
|
Toast.show('飞书环境异常(tt 对象不存在),请刷新重试')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 尝试调用 requestAuthCode
|
// 获取 appId:优先从 URL 参数,否则从后端获取
|
||||||
try {
|
const urlParams = new URLSearchParams(window.location.search)
|
||||||
console.log('🔧 调用 tt.requestAuthCode,company:', companyParam)
|
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('获取飞书配置失败,请重试')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!appId) {
|
||||||
|
console.error('❌ 无法获取 appId')
|
||||||
|
setLoading(false)
|
||||||
|
Toast.show('缺少飞书应用配置')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用 tt.requestAuthCode 获取授权码
|
||||||
|
try {
|
||||||
|
console.log('🔧 调用 tt.requestAuthCode (appId:', appId?.substring(0, 15) + '...)')
|
||||||
|
|
||||||
// 不传 appId,让飞书自动选择(后端会根据 company 参数处理)
|
|
||||||
window.tt.requestAuthCode({
|
window.tt.requestAuthCode({
|
||||||
|
appId: appId,
|
||||||
success: (res: any) => {
|
success: (res: any) => {
|
||||||
console.log('✅ 成功获取授权码:', res)
|
console.log('✅ 成功获取授权码:', res.code?.substring(0, 30))
|
||||||
console.log('✅ code:', res.code?.substring(0, 30))
|
|
||||||
handleFeishuLogin(res.code)
|
handleFeishuLogin(res.code)
|
||||||
},
|
},
|
||||||
fail: (error: any) => {
|
fail: (error: any) => {
|
||||||
console.error('❌ requestAuthCode 失败:', error)
|
console.error('❌ requestAuthCode 失败:', error)
|
||||||
console.error('❌ 错误详情:', JSON.stringify(error))
|
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
Toast.show('免登失败:' + (error.errorMessage || error.errMsg || JSON.stringify(error)))
|
Toast.show('免登失败:' + (error.errorMessage || error.errMsg || JSON.stringify(error)))
|
||||||
},
|
},
|
||||||
@@ -204,9 +378,8 @@ export default function Login() {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('❌ 调用异常:', e)
|
console.error('❌ 调用异常:', e)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
Toast.show('免登失败,请使用其他方式登录')
|
Toast.show('免登失败,请重试')
|
||||||
}
|
}
|
||||||
}, 500)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 跳转到飞书授权页面(飞书外扫码登录)
|
// 跳转到飞书授权页面(飞书外扫码登录)
|
||||||
@@ -385,6 +558,26 @@ export default function Login() {
|
|||||||
<div className={styles.login}>
|
<div className={styles.login}>
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<h1>企业工具台</h1>
|
<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>
|
<p className={styles.subtitle}>请选择登录方式</p>
|
||||||
|
|
||||||
<div className={styles.loginMethods}>
|
<div className={styles.loginMethods}>
|
||||||
@@ -396,6 +589,7 @@ export default function Login() {
|
|||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
{environment !== 'feishu' && (
|
||||||
<button className={styles.methodBtn} onClick={redirectToFeishu}>
|
<button className={styles.methodBtn} onClick={redirectToFeishu}>
|
||||||
<div className={styles.methodIcon}>📱</div>
|
<div className={styles.methodIcon}>📱</div>
|
||||||
<div className={styles.methodText}>
|
<div className={styles.methodText}>
|
||||||
@@ -403,6 +597,7 @@ export default function Login() {
|
|||||||
<div className={styles.methodDesc}>使用飞书 APP 扫码登录</div>
|
<div className={styles.methodDesc}>使用飞书 APP 扫码登录</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{environment === 'external' && (
|
{environment === 'external' && (
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import RejectPage from '@/pages/approval/reject'
|
|||||||
import Login from '@/pages/login'
|
import Login from '@/pages/login'
|
||||||
import SimpleLogin from '@/pages/login-simple'
|
import SimpleLogin from '@/pages/login-simple'
|
||||||
import DebugFeishu from '@/pages/debug-feishu'
|
import DebugFeishu from '@/pages/debug-feishu'
|
||||||
|
import FeishuEnvTest from '@/pages/feishu-env-test'
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
@@ -43,6 +44,7 @@ const router = createBrowserRouter([
|
|||||||
{ path: '/login', element: <Login /> },
|
{ path: '/login', element: <Login /> },
|
||||||
{ path: '/login-simple', element: <SimpleLogin /> },
|
{ path: '/login-simple', element: <SimpleLogin /> },
|
||||||
{ path: '/debug-feishu', element: <DebugFeishu /> },
|
{ path: '/debug-feishu', element: <DebugFeishu /> },
|
||||||
|
{ path: '/feishu-env-test', element: <FeishuEnvTest /> },
|
||||||
{ path: '/approval/quick-action', element: <QuickAction /> },
|
{ path: '/approval/quick-action', element: <QuickAction /> },
|
||||||
{ path: '/approval/reject', element: <RejectPage /> },
|
{ path: '/approval/reject', element: <RejectPage /> },
|
||||||
{ path: '*', element: <Navigate to='/login' replace /> },
|
{ path: '*', element: <Navigate to='/login' replace /> },
|
||||||
|
|||||||
Reference in New Issue
Block a user