feat(platform): Implement legacy system integration with Wrapper Bridge architecture
Complete integration of the old clinical research platform (www.xunzhengyixue.com) into the new AI platform via Token injection + iframe embedding: Backend: - Add legacy-bridge module (MySQL pool, auth service, routes) - POST /api/v1/legacy/auth: JWT -> phone lookup -> Token injection into old MySQL - Auto-create user in old system if not found (matched by phone number) Frontend: - LegacySystemPage: iframe container with Bridge URL construction - ResearchManagement + StatisticalTools entry components - Module registry updated from external links to iframe embed mode ECS (token-bridge.html deployed to www.xunzhengyixue.com): - Wrapper Bridge: sets cookies within same-origin context - Storage Access API for cross-site dev environments - CSS injection: hide old system nav/footer, remove padding gaps - Inner iframe loads target page with full DOM access (same-origin) Key technical decisions: - Token injection (direct MySQL write) instead of calling login API - Wrapper Bridge instead of parent-page cookie setting (cross-origin fix) - Storage Access API + SameSite=None;Secure for third-party cookie handling - User isolation guaranteed by phone number matching Documentation: - Integration plan v4.0 with full implementation record - Implementation summary with 6 pitfalls documented - System status guide updated (ST module now integrated) Tested: Local E2E verified - auto login, research management, 126 statistical tools, report generation, download, UI layout all working correctly Made-with: Cursor
This commit is contained in:
104
frontend-v2/src/modules/legacy/LegacySystemPage.tsx
Normal file
104
frontend-v2/src/modules/legacy/LegacySystemPage.tsx
Normal file
@@ -0,0 +1,104 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { Spin, message } from 'antd'
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import apiClient from '@/common/api/axios'
|
||||
|
||||
const BRIDGE_URL = 'https://www.xunzhengyixue.com/token-bridge.html'
|
||||
|
||||
interface LegacyAuthData {
|
||||
token: string
|
||||
nickname: string
|
||||
id: number
|
||||
userRole: string
|
||||
}
|
||||
|
||||
interface LegacySystemPageProps {
|
||||
targetUrl: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Embeds the old system in an iframe via a same-origin bridge page.
|
||||
*
|
||||
* Flow: call backend to inject token into old MySQL → build bridge URL with
|
||||
* auth params → iframe loads bridge → bridge sets cookies + loads target page
|
||||
* in a nested iframe → bridge injects custom CSS (same-origin DOM access).
|
||||
*/
|
||||
const LegacySystemPage: React.FC<LegacySystemPageProps> = ({ targetUrl }) => {
|
||||
const [status, setStatus] = useState<'authenticating' | 'ready' | 'error'>('authenticating')
|
||||
const [bridgeUrl, setBridgeUrl] = useState('')
|
||||
const [errorMsg, setErrorMsg] = useState('')
|
||||
const iframeRef = useRef<HTMLIFrameElement>(null)
|
||||
const authDoneRef = useRef(false)
|
||||
|
||||
const authenticate = useCallback(async () => {
|
||||
if (authDoneRef.current) return
|
||||
|
||||
try {
|
||||
setStatus('authenticating')
|
||||
const resp = await apiClient.post<{ success: boolean; data: LegacyAuthData }>('/api/v1/legacy/auth')
|
||||
const { token, nickname, id, userRole } = resp.data.data
|
||||
|
||||
const redirectPath = new URL(targetUrl).pathname
|
||||
const params = new URLSearchParams({
|
||||
token,
|
||||
nickname,
|
||||
id: String(id),
|
||||
userRole,
|
||||
redirect: redirectPath,
|
||||
})
|
||||
setBridgeUrl(`${BRIDGE_URL}?${params.toString()}`)
|
||||
|
||||
authDoneRef.current = true
|
||||
setStatus('ready')
|
||||
} catch (err: any) {
|
||||
const msg = err?.response?.data?.message || err?.message || '旧系统认证失败'
|
||||
setErrorMsg(msg)
|
||||
setStatus('error')
|
||||
message.error(msg)
|
||||
}
|
||||
}, [targetUrl])
|
||||
|
||||
useEffect(() => {
|
||||
authenticate()
|
||||
}, [authenticate])
|
||||
|
||||
if (status === 'authenticating') {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', gap: 16 }}>
|
||||
<Spin indicator={<LoadingOutlined style={{ fontSize: 36 }} spin />} />
|
||||
<span style={{ color: '#666' }}>正在连接旧系统...</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (status === 'error') {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '100%', gap: 16 }}>
|
||||
<span style={{ fontSize: 48 }}>⚠️</span>
|
||||
<span style={{ color: '#999' }}>{errorMsg}</span>
|
||||
<button
|
||||
onClick={() => { authDoneRef.current = false; authenticate() }}
|
||||
style={{ padding: '8px 24px', cursor: 'pointer', borderRadius: 6, border: '1px solid #d9d9d9', background: '#fff' }}
|
||||
>
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<iframe
|
||||
ref={iframeRef}
|
||||
src={bridgeUrl}
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
border: 'none',
|
||||
display: 'block',
|
||||
}}
|
||||
title="旧系统"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default LegacySystemPage
|
||||
13
frontend-v2/src/modules/legacy/ResearchManagement.tsx
Normal file
13
frontend-v2/src/modules/legacy/ResearchManagement.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import LegacySystemPage from './LegacySystemPage'
|
||||
|
||||
const RESEARCH_MANAGEMENT_URL = 'https://www.xunzhengyixue.com/index.html'
|
||||
|
||||
const ResearchManagement: React.FC = () => {
|
||||
return (
|
||||
<div style={{ flex: 1, height: '100%', overflow: 'hidden' }}>
|
||||
<LegacySystemPage targetUrl={RESEARCH_MANAGEMENT_URL} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ResearchManagement
|
||||
13
frontend-v2/src/modules/legacy/StatisticalTools.tsx
Normal file
13
frontend-v2/src/modules/legacy/StatisticalTools.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import LegacySystemPage from './LegacySystemPage'
|
||||
|
||||
const STATISTICAL_TOOLS_URL = 'https://www.xunzhengyixue.com/tool.html'
|
||||
|
||||
const StatisticalTools: React.FC = () => {
|
||||
return (
|
||||
<div style={{ flex: 1, height: '100%', overflow: 'hidden' }}>
|
||||
<LegacySystemPage targetUrl={STATISTICAL_TOOLS_URL} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default StatisticalTools
|
||||
Reference in New Issue
Block a user