|
| 1 | +/** |
| 2 | + * 微信小程序测试页(v8.0) |
| 3 | + * |
| 4 | + * 功能:检测开发者工具/创建会话/导航/点击/输入/截图/WXML/页面数据 |
| 5 | + */ |
| 6 | + |
| 7 | +import { useState, useEffect } from 'react'; |
| 8 | +import { |
| 9 | + Smartphone, Play, MousePointer, Type, Camera, |
| 10 | + Code2, Loader2, RefreshCw, Database, X, FolderOpen, CheckCircle, XCircle, |
| 11 | +} from 'lucide-react'; |
| 12 | +import { request } from '../lib/engineClient'; |
| 13 | + |
| 14 | +interface SessionInfo { session_id: string; } |
| 15 | +interface DevtoolsStatus { found: boolean; path: string; message: string; } |
| 16 | + |
| 17 | +export default function MiniProgramTestPage() { |
| 18 | + const [devtools, setDevtools] = useState<DevtoolsStatus | null>(null); |
| 19 | + const [session, setSession] = useState<SessionInfo | null>(null); |
| 20 | + const [loading, setLoading] = useState(false); |
| 21 | + const [projectPath, setProjectPath] = useState(''); |
| 22 | + const [screenshot, setScreenshot] = useState(''); |
| 23 | + const [wxml, setWxml] = useState(''); |
| 24 | + const [pageData, setPageData] = useState(''); |
| 25 | + const [selector, setSelector] = useState(''); |
| 26 | + const [inputText, setInputText] = useState(''); |
| 27 | + const [navUrl, setNavUrl] = useState(''); |
| 28 | + const [log, setLog] = useState<string[]>([]); |
| 29 | + |
| 30 | + const addLog = (msg: string) => setLog(prev => [`[${new Date().toLocaleTimeString()}] ${msg}`, ...prev].slice(0, 50)); |
| 31 | + |
| 32 | + const checkDevtools = async () => { |
| 33 | + try { |
| 34 | + const res = await request<DevtoolsStatus>('GET', '/api/v1/miniprogram/devtools/status'); |
| 35 | + setDevtools(res); |
| 36 | + addLog(res.message); |
| 37 | + } catch (e: unknown) { addLog(`检测失败: ${e}`); } |
| 38 | + }; |
| 39 | + |
| 40 | + useEffect(() => { checkDevtools(); }, []); |
| 41 | + |
| 42 | + const connect = async () => { |
| 43 | + if (!projectPath) { addLog('请输入小程序项目路径'); return; } |
| 44 | + setLoading(true); |
| 45 | + try { |
| 46 | + const res = await request<{ session_id: string }>('POST', '/api/v1/miniprogram/session/create', { |
| 47 | + project_path: projectPath, |
| 48 | + }); |
| 49 | + setSession({ session_id: res.session_id }); |
| 50 | + addLog(`已连接: ${projectPath}`); |
| 51 | + } catch (e: unknown) { addLog(`连接失败: ${e}`); } |
| 52 | + finally { setLoading(false); } |
| 53 | + }; |
| 54 | + |
| 55 | + const disconnect = async () => { |
| 56 | + if (!session) return; |
| 57 | + try { |
| 58 | + await request('DELETE', `/api/v1/miniprogram/session/${session.session_id}`); |
| 59 | + addLog('会话已关闭'); |
| 60 | + } catch { /* ignore */ } |
| 61 | + setSession(null); setScreenshot(''); setWxml(''); setPageData(''); |
| 62 | + }; |
| 63 | + |
| 64 | + const doNavigate = async () => { |
| 65 | + if (!session || !navUrl) return; |
| 66 | + try { |
| 67 | + await request('POST', `/api/v1/miniprogram/session/${session.session_id}/navigate`, { url: navUrl }); |
| 68 | + addLog(`导航: ${navUrl}`); |
| 69 | + } catch (e: unknown) { addLog(`导航失败: ${e}`); } |
| 70 | + }; |
| 71 | + |
| 72 | + const doTap = async () => { |
| 73 | + if (!session || !selector) return; |
| 74 | + try { |
| 75 | + await request('POST', `/api/v1/miniprogram/session/${session.session_id}/tap`, { selector }); |
| 76 | + addLog(`点击: ${selector}`); |
| 77 | + } catch (e: unknown) { addLog(`点击失败: ${e}`); } |
| 78 | + }; |
| 79 | + |
| 80 | + const doInput = async () => { |
| 81 | + if (!session || !selector) return; |
| 82 | + try { |
| 83 | + await request('POST', `/api/v1/miniprogram/session/${session.session_id}/input`, { selector, text: inputText }); |
| 84 | + addLog(`输入: ${selector} -> "${inputText}"`); |
| 85 | + } catch (e: unknown) { addLog(`输入失败: ${e}`); } |
| 86 | + }; |
| 87 | + |
| 88 | + const doScreenshot = async () => { |
| 89 | + if (!session) return; |
| 90 | + try { |
| 91 | + const res = await request<{ base64: string; path: string }>('GET', `/api/v1/miniprogram/session/${session.session_id}/screenshot?name=manual`); |
| 92 | + setScreenshot(res.base64); |
| 93 | + addLog(`截图完成: ${res.path}`); |
| 94 | + } catch (e: unknown) { addLog(`截图失败: ${e}`); } |
| 95 | + }; |
| 96 | + |
| 97 | + const doSource = async () => { |
| 98 | + if (!session) return; |
| 99 | + try { |
| 100 | + const res = await request<{ source: string }>('GET', `/api/v1/miniprogram/session/${session.session_id}/source`); |
| 101 | + setWxml(res.source); |
| 102 | + addLog('WXML 已获取'); |
| 103 | + } catch (e: unknown) { addLog(`获取WXML失败: ${e}`); } |
| 104 | + }; |
| 105 | + |
| 106 | + const doPageData = async () => { |
| 107 | + if (!session) return; |
| 108 | + try { |
| 109 | + const res = await request<{ data: unknown }>('GET', `/api/v1/miniprogram/session/${session.session_id}/page-data`); |
| 110 | + setPageData(JSON.stringify(res.data, null, 2)); |
| 111 | + addLog('页面数据已获取'); |
| 112 | + } catch (e: unknown) { addLog(`获取数据失败: ${e}`); } |
| 113 | + }; |
| 114 | + |
| 115 | + return ( |
| 116 | + <div className="p-6 space-y-4 h-full overflow-y-auto"> |
| 117 | + <div className="flex items-center justify-between"> |
| 118 | + <h1 className="text-xl font-bold text-white flex items-center gap-2"> |
| 119 | + <Smartphone className="w-5 h-5 text-green-400" /> 小程序测试 |
| 120 | + </h1> |
| 121 | + {session && ( |
| 122 | + <span className="text-xs text-emerald-400 bg-emerald-400/10 px-2 py-1 rounded"> |
| 123 | + 已连接 |
| 124 | + </span> |
| 125 | + )} |
| 126 | + </div> |
| 127 | + |
| 128 | + {/* 开发者工具状态 */} |
| 129 | + <div className="bg-gray-900 border border-gray-800 rounded-xl p-4"> |
| 130 | + <div className="flex items-center gap-2 text-sm"> |
| 131 | + {devtools?.found |
| 132 | + ? <><CheckCircle className="w-4 h-4 text-green-400" /><span className="text-green-400">微信开发者工具已找到</span><span className="text-gray-500 text-xs ml-2">{devtools.path}</span></> |
| 133 | + : <><XCircle className="w-4 h-4 text-red-400" /><span className="text-red-400">未找到微信开发者工具</span><span className="text-gray-500 text-xs ml-2">请安装后开启"服务端口"</span></> |
| 134 | + } |
| 135 | + <button onClick={checkDevtools} className="ml-auto text-gray-500 hover:text-blue-400 p-1"><RefreshCw className="w-3.5 h-3.5" /></button> |
| 136 | + </div> |
| 137 | + </div> |
| 138 | + |
| 139 | + {!session ? ( |
| 140 | + /* ── 连接面板 ── */ |
| 141 | + <div className="bg-gray-900 border border-gray-800 rounded-xl p-5 space-y-3"> |
| 142 | + <div className="text-sm text-gray-400 font-medium flex items-center gap-2"> |
| 143 | + <FolderOpen className="w-4 h-4" /> 小程序项目路径 |
| 144 | + </div> |
| 145 | + <input value={projectPath} onChange={e => setProjectPath(e.target.value)} |
| 146 | + placeholder="如: D:\Projects\my-miniprogram" |
| 147 | + className="w-full bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 outline-none focus:border-green-500" /> |
| 148 | + <button onClick={connect} disabled={loading || !projectPath} |
| 149 | + className="w-full flex items-center justify-center gap-2 py-2.5 bg-green-600 hover:bg-green-500 text-white rounded-lg text-sm disabled:opacity-40 transition-colors"> |
| 150 | + {loading ? <Loader2 className="w-4 h-4 animate-spin" /> : <Play className="w-4 h-4" />} |
| 151 | + 连接小程序 |
| 152 | + </button> |
| 153 | + </div> |
| 154 | + ) : ( |
| 155 | + /* ── 操作面板 ── */ |
| 156 | + <> |
| 157 | + <div className="flex items-center gap-2"> |
| 158 | + <input value={navUrl} onChange={e => setNavUrl(e.target.value)} placeholder="页面路径 (如 /pages/index/index)" |
| 159 | + className="flex-1 bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 outline-none focus:border-green-500" /> |
| 160 | + <button onClick={doNavigate} className="px-3 py-2 bg-green-600 hover:bg-green-500 text-white text-xs rounded-lg">导航</button> |
| 161 | + </div> |
| 162 | + <div className="flex items-center gap-2"> |
| 163 | + <input value={selector} onChange={e => setSelector(e.target.value)} placeholder="选择器 (.class / #id / view)" |
| 164 | + className="flex-1 bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 outline-none focus:border-green-500" /> |
| 165 | + <input value={inputText} onChange={e => setInputText(e.target.value)} placeholder="输入文本" |
| 166 | + className="w-40 bg-gray-800 border border-gray-700 rounded-lg px-3 py-2 text-sm text-white placeholder-gray-500 outline-none focus:border-green-500" /> |
| 167 | + </div> |
| 168 | + <div className="flex gap-2 flex-wrap"> |
| 169 | + <Btn icon={<MousePointer className="w-3.5 h-3.5" />} label="点击" onClick={doTap} color="bg-blue-600 hover:bg-blue-500" /> |
| 170 | + <Btn icon={<Type className="w-3.5 h-3.5" />} label="输入" onClick={doInput} color="bg-violet-600 hover:bg-violet-500" /> |
| 171 | + <Btn icon={<Camera className="w-3.5 h-3.5" />} label="截图" onClick={doScreenshot} color="bg-amber-600 hover:bg-amber-500" /> |
| 172 | + <Btn icon={<Code2 className="w-3.5 h-3.5" />} label="WXML" onClick={doSource} color="bg-cyan-600 hover:bg-cyan-500" /> |
| 173 | + <Btn icon={<Database className="w-3.5 h-3.5" />} label="页面数据" onClick={doPageData} color="bg-pink-600 hover:bg-pink-500" /> |
| 174 | + <Btn icon={<X className="w-3.5 h-3.5" />} label="断开" onClick={disconnect} color="bg-red-600/80 hover:bg-red-500" /> |
| 175 | + </div> |
| 176 | + |
| 177 | + <div className="grid grid-cols-2 gap-4"> |
| 178 | + <div className="bg-gray-900 border border-gray-800 rounded-xl overflow-hidden"> |
| 179 | + <div className="px-4 py-2 border-b border-gray-800 text-xs text-gray-500 font-medium">截图预览</div> |
| 180 | + <div className="p-2 min-h-[200px] flex items-center justify-center"> |
| 181 | + {screenshot ? ( |
| 182 | + <img src={`data:image/png;base64,${screenshot}`} alt="截图" className="max-w-full max-h-[300px] rounded" /> |
| 183 | + ) : ( |
| 184 | + <span className="text-gray-600 text-xs">点击「截图」按钮</span> |
| 185 | + )} |
| 186 | + </div> |
| 187 | + </div> |
| 188 | + <div className="bg-gray-900 border border-gray-800 rounded-xl overflow-hidden"> |
| 189 | + <div className="px-4 py-2 border-b border-gray-800 text-xs text-gray-500 font-medium"> |
| 190 | + {pageData ? '页面数据' : 'WXML 结构'} |
| 191 | + </div> |
| 192 | + <pre className="p-3 text-xs text-gray-400 max-h-[320px] overflow-auto font-mono whitespace-pre-wrap"> |
| 193 | + {pageData || wxml || '点击「WXML」或「页面数据」按钮'} |
| 194 | + </pre> |
| 195 | + </div> |
| 196 | + </div> |
| 197 | + </> |
| 198 | + )} |
| 199 | + |
| 200 | + {/* 日志 */} |
| 201 | + <div className="bg-gray-900 border border-gray-800 rounded-xl overflow-hidden"> |
| 202 | + <div className="px-4 py-2 border-b border-gray-800 text-xs text-gray-500 font-medium">操作日志</div> |
| 203 | + <div className="p-3 max-h-40 overflow-y-auto"> |
| 204 | + {log.length === 0 ? ( |
| 205 | + <span className="text-gray-600 text-xs">暂无日志</span> |
| 206 | + ) : log.map((l, i) => ( |
| 207 | + <div key={i} className="text-xs text-gray-400 font-mono py-0.5">{l}</div> |
| 208 | + ))} |
| 209 | + </div> |
| 210 | + </div> |
| 211 | + </div> |
| 212 | + ); |
| 213 | +} |
| 214 | + |
| 215 | +function Btn({ icon, label, onClick, color }: { |
| 216 | + icon: React.ReactNode; label: string; onClick: () => void; color: string; |
| 217 | +}) { |
| 218 | + return ( |
| 219 | + <button onClick={onClick} |
| 220 | + className={`flex items-center gap-1.5 px-3 py-1.5 ${color} text-white text-xs rounded-lg transition-colors`}> |
| 221 | + {icon} {label} |
| 222 | + </button> |
| 223 | + ); |
| 224 | +} |
0 commit comments