Skip to content

Commit e9e11cc

Browse files
committed
v14.0-G: 插件账号UI重构 + 官网API代理+注册路由修复
插件(sidebarProvider.ts): - 账户登录 -> 账号,行右侧加登录状态圆点(绿=已登录/灰=未登录) - 内置登录/注册Tab切换,不再跳外部官网 - 注册表单: 邮箱+用户名+密码+确认密码,直接调/auth/register - 全部密码输入框加眼睛按钮切换明文/密文 - 注册/登录错误提示独立显示 官网(web/): - 修复/register空白页: 添加重定向到/login?tab=register - 修复Cloudflare Pages API调用报JSON错误: 新增functions/api/[[path]].js边缘代理 根本原因: _redirects的/* /index.html 200把所有/api/*请求当SPA处理 - api.js request()改用text()+try-parse,防止空body崩溃
1 parent 1cebfb9 commit e9e11cc

5 files changed

Lines changed: 208 additions & 44 deletions

File tree

extension/src/sidebarProvider.ts

Lines changed: 135 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1634,6 +1634,19 @@ ${commonRules}`;
16341634
display: none; padding: 8px 12px;
16351635
border-top: 1px solid var(--input-border);
16361636
}
1637+
.sdrop-auth-tab-bar {
1638+
display: flex; margin-bottom: 8px; gap: 0;
1639+
border: 1px solid var(--input-border); border-radius: 4px; overflow: hidden;
1640+
}
1641+
.sdrop-tab-btn {
1642+
flex: 1; padding: 4px 0; font-size: 11px; font-weight: 600;
1643+
background: none; color: var(--muted);
1644+
border: none; cursor: pointer; transition: all 0.15s;
1645+
}
1646+
.sdrop-tab-btn.active {
1647+
background: var(--btn-bg); color: var(--btn-fg);
1648+
}
1649+
.sdrop-tab-btn:not(.active):hover { background: var(--input-border); }
16371650
.sdrop-auth-panel input {
16381651
width: 100%; box-sizing: border-box;
16391652
padding: 5px 7px; margin-bottom: 5px;
@@ -1642,18 +1655,25 @@ ${commonRules}`;
16421655
font-size: 11px; outline: none;
16431656
}
16441657
.sdrop-auth-panel input:focus { border-color: var(--btn-bg); }
1658+
.sdrop-pass-wrap {
1659+
position: relative; margin-bottom: 5px;
1660+
}
1661+
.sdrop-pass-wrap input {
1662+
width: 100%; box-sizing: border-box; padding-right: 28px; margin-bottom: 0;
1663+
}
1664+
.sdrop-eye-btn {
1665+
position: absolute; right: 4px; top: 50%; transform: translateY(-50%);
1666+
background: none; border: none; cursor: pointer;
1667+
font-size: 13px; line-height: 1; padding: 2px; color: var(--muted);
1668+
opacity: 0.6; transition: opacity 0.1s;
1669+
}
1670+
.sdrop-eye-btn:hover { opacity: 1; }
16451671
.sdrop-login-btn {
1646-
flex: 1; padding: 5px; font-size: 11px; font-weight: 600;
1672+
width: 100%; padding: 6px; font-size: 11px; font-weight: 600;
16471673
background: var(--btn-bg); color: var(--btn-fg);
1648-
border: none; border-radius: 3px; cursor: pointer;
1674+
border: none; border-radius: 3px; cursor: pointer; margin-top: 4px;
16491675
}
16501676
.sdrop-login-btn:hover { background: var(--btn-hover); }
1651-
.sdrop-register-btn {
1652-
padding: 5px 8px; font-size: 11px;
1653-
background: none; color: var(--btn-bg);
1654-
border: 1px solid var(--btn-bg); border-radius: 3px; cursor: pointer;
1655-
}
1656-
.sdrop-register-btn:hover { background: var(--btn-bg); color: var(--btn-fg); }
16571677
.auth-error {
16581678
color: var(--error); font-size: 11px; margin: 0 0 5px; display: none;
16591679
}
@@ -1696,23 +1716,49 @@ ${commonRules}`;
16961716
</div>
16971717
<div class="sdrop-divider"></div>
16981718
<div class="sdrop-item" id="settingsAuthRow">
1699-
<span id="settingsAuthLabel">🔑 账户登录</span>
1700-
<span id="settingsAuthArrow" style="font-size:10px;color:var(--muted)">▾</span>
1719+
<span id="settingsAuthLabel">🔑 账号</span>
1720+
<span style="display:flex;align-items:center;gap:4px">
1721+
<span id="authStatusDot" style="display:inline-block;width:7px;height:7px;border-radius:50%;background:var(--muted);flex-shrink:0"></span>
1722+
<span id="settingsAuthArrow" style="font-size:10px;color:var(--muted)">▾</span>
1723+
</span>
17011724
</div>
17021725
<div id="settingsAuthPanel" class="sdrop-auth-panel">
1703-
<div id="authError" class="auth-error"></div>
1704-
<input id="authUser" type="text" placeholder="用户名或邮箱" autocomplete="username" />
1705-
<input id="authPass" type="password" placeholder="密码" autocomplete="current-password" />
1706-
<div style="display:flex;gap:6px;margin-top:4px">
1726+
<!-- 登录/注册切换 Tab -->
1727+
<div class="sdrop-auth-tab-bar">
1728+
<button class="sdrop-tab-btn active" id="tabAuthLogin">登录</button>
1729+
<button class="sdrop-tab-btn" id="tabAuthRegister">注册</button>
1730+
</div>
1731+
<!-- 登录表单 -->
1732+
<div id="loginForm">
1733+
<div id="authError" class="auth-error"></div>
1734+
<input id="authUser" type="text" placeholder="用户名或邮箱" autocomplete="username" />
1735+
<div class="sdrop-pass-wrap">
1736+
<input id="authPass" type="password" placeholder="密码" autocomplete="current-password" />
1737+
<button class="sdrop-eye-btn" id="authPassEye" title="显示/隐藏密码">👁</button>
1738+
</div>
17071739
<button class="sdrop-login-btn" id="authLoginBtn">登录</button>
1708-
<button class="sdrop-register-btn" id="authRegisterBtn">前往注册 ↗</button>
1740+
</div>
1741+
<!-- 注册表单 -->
1742+
<div id="registerForm" style="display:none">
1743+
<div id="regError" class="auth-error"></div>
1744+
<input id="regEmail" type="email" placeholder="邮箱地址" autocomplete="email" />
1745+
<input id="regUser" type="text" placeholder="用户名" autocomplete="username" />
1746+
<div class="sdrop-pass-wrap">
1747+
<input id="regPass" type="password" placeholder="密码" autocomplete="new-password" />
1748+
<button class="sdrop-eye-btn" id="regPassEye" title="显示/隐藏密码">👁</button>
1749+
</div>
1750+
<div class="sdrop-pass-wrap">
1751+
<input id="regConfirm" type="password" placeholder="确认密码" autocomplete="new-password" />
1752+
<button class="sdrop-eye-btn" id="regConfirmEye" title="显示/隐藏密码">👁</button>
1753+
</div>
1754+
<button class="sdrop-login-btn" id="authRegisterSubmit">注册账号</button>
17091755
</div>
17101756
</div>
17111757
<div id="settingsUserRow" class="sdrop-user-row" style="display:none">
17121758
<div style="display:flex;align-items:center;justify-content:space-between">
17131759
<span style="color:var(--success);font-weight:600">👤 <span id="authUsername"></span>
17141760
<span style="color:var(--muted)" id="authPlan"></span></span>
1715-
<button id="authLogoutBtn" style="background:none;border:none;color:var(--muted);cursor:pointer;font-size:11px;text-decoration:underline">退出</button>
1761+
<button id="authLogoutBtn" style="background:none;border:none;color:var(--muted);cursor:pointer;font-size:11px;text-decoration:underline">退出登录</button>
17161762
</div>
17171763
<div id="authCredits" style="font-size:11px;color:var(--muted);margin-top:2px"></div>
17181764
</div>
@@ -1920,7 +1966,7 @@ ${commonRules}`;
19201966
<script nonce="${nonce}">
19211967
const vscode = acquireVsCodeApi();
19221968
1923-
// ── 认证逻辑(登录UI位于设置菜单内) ──────────────────────────────
1969+
// ── 认证逻辑(登录/注册UI位于设置菜单内) ──────────────────────────────
19241970
const authUser = document.getElementById('authUser');
19251971
const authPass = document.getElementById('authPass');
19261972
const authError = document.getElementById('authError');
@@ -1930,6 +1976,51 @@ ${commonRules}`;
19301976
function showAuthError(msg) {
19311977
if (authError) { authError.textContent = msg; authError.style.display = msg ? 'block' : 'none'; }
19321978
}
1979+
function showRegError(msg) {
1980+
const el = document.getElementById('regError');
1981+
if (el) { el.textContent = msg; el.style.display = msg ? 'block' : 'none'; }
1982+
}
1983+
1984+
// 眼睛切换
1985+
function bindEye(eyeId, inputId) {
1986+
const eye = document.getElementById(eyeId);
1987+
const inp = document.getElementById(inputId);
1988+
if (!eye || !inp) return;
1989+
eye.addEventListener('click', (e) => {
1990+
e.stopPropagation();
1991+
const show = inp.type === 'password';
1992+
inp.type = show ? 'text' : 'password';
1993+
eye.textContent = show ? '🙈' : '👁';
1994+
});
1995+
}
1996+
bindEye('authPassEye', 'authPass');
1997+
bindEye('regPassEye', 'regPass');
1998+
bindEye('regConfirmEye', 'regConfirm');
1999+
2000+
// 登录/注册 Tab 切换
2001+
function switchAuthTab(tab) {
2002+
const loginForm = document.getElementById('loginForm');
2003+
const regForm = document.getElementById('registerForm');
2004+
const tabL = document.getElementById('tabAuthLogin');
2005+
const tabR = document.getElementById('tabAuthRegister');
2006+
if (tab === 'login') {
2007+
if (loginForm) loginForm.style.display = '';
2008+
if (regForm) regForm.style.display = 'none';
2009+
if (tabL) { tabL.classList.add('active'); }
2010+
if (tabR) { tabR.classList.remove('active'); }
2011+
setTimeout(() => authUser && authUser.focus(), 50);
2012+
} else {
2013+
if (loginForm) loginForm.style.display = 'none';
2014+
if (regForm) regForm.style.display = '';
2015+
if (tabL) { tabL.classList.remove('active'); }
2016+
if (tabR) { tabR.classList.add('active'); }
2017+
setTimeout(() => { const el = document.getElementById('regEmail'); if (el) el.focus(); }, 50);
2018+
}
2019+
}
2020+
const tabAuthLogin = document.getElementById('tabAuthLogin');
2021+
const tabAuthRegister = document.getElementById('tabAuthRegister');
2022+
if (tabAuthLogin) tabAuthLogin.addEventListener('click', (e) => { e.stopPropagation(); switchAuthTab('login'); });
2023+
if (tabAuthRegister) tabAuthRegister.addEventListener('click', (e) => { e.stopPropagation(); switchAuthTab('register'); });
19332024
19342025
function toggleSettingsAuth() {
19352026
const panel = document.getElementById('settingsAuthPanel');
@@ -1940,18 +2031,26 @@ ${commonRules}`;
19402031
if (!open) setTimeout(() => authUser && authUser.focus(), 50);
19412032
}
19422033
1943-
function openRegister() {
1944-
vscode.postMessage({ command: 'openExternal', url: 'https://testpilotai.pages.dev/register' });
1945-
}
1946-
19472034
function doAuth() {
19482035
showAuthError('');
1949-
const u = authUser.value.trim();
1950-
const p = authPass.value;
2036+
const u = authUser ? authUser.value.trim() : '';
2037+
const p = authPass ? authPass.value : '';
19512038
if (!u || !p) { showAuthError('请填写用户名和密码'); return; }
19522039
vscode.postMessage({ command: 'login', emailOrUsername: u, password: p });
19532040
}
19542041
2042+
function doRegister() {
2043+
showRegError('');
2044+
const email = (document.getElementById('regEmail')).value.trim();
2045+
const user = (document.getElementById('regUser')).value.trim();
2046+
const pass = (document.getElementById('regPass')).value;
2047+
const confirm = (document.getElementById('regConfirm')).value;
2048+
if (!email || !user || !pass) { showRegError('请填写所有字段'); return; }
2049+
if (pass !== confirm) { showRegError('两次密码不一致'); return; }
2050+
if (pass.length < 6) { showRegError('密码至少6位'); return; }
2051+
vscode.postMessage({ command: 'register', email, username: user, password: pass });
2052+
}
2053+
19552054
function doLogout() {
19562055
vscode.postMessage({ command: 'logout' });
19572056
}
@@ -1962,8 +2061,8 @@ ${commonRules}`;
19622061
if (authPass) authPass.addEventListener('keydown', (e) => { if (e.key === 'Enter') doAuth(); });
19632062
const authLoginBtn = document.getElementById('authLoginBtn');
19642063
if (authLoginBtn) authLoginBtn.addEventListener('click', doAuth);
1965-
const authRegisterBtn = document.getElementById('authRegisterBtn');
1966-
if (authRegisterBtn) authRegisterBtn.addEventListener('click', openRegister);
2064+
const authRegisterSubmit = document.getElementById('authRegisterSubmit');
2065+
if (authRegisterSubmit) authRegisterSubmit.addEventListener('click', doRegister);
19672066
const authLogoutBtn = document.getElementById('authLogoutBtn');
19682067
if (authLogoutBtn) authLogoutBtn.addEventListener('click', doLogout);
19692068
const themeToggleLabel = document.getElementById('themeToggleLabel');
@@ -1974,11 +2073,13 @@ ${commonRules}`;
19742073
const userRow = document.getElementById('settingsUserRow');
19752074
const authLabel = document.getElementById('settingsAuthLabel');
19762075
const authArrow = document.getElementById('settingsAuthArrow');
2076+
const statusDot = document.getElementById('authStatusDot');
19772077
if (loggedIn && user) {
19782078
if (authPanel) authPanel.style.display = 'none';
19792079
if (userRow) userRow.style.display = 'block';
19802080
if (authLabel) authLabel.textContent = '👤 ' + (user.username || '');
19812081
if (authArrow) authArrow.style.display = 'none';
2082+
if (statusDot) { statusDot.style.background = 'var(--success)'; statusDot.title = '已登录'; }
19822083
if (authUsername) authUsername.textContent = user.username || '';
19832084
if (authPlan) authPlan.textContent = user.plan ? '(' + user.plan + ')' : '';
19842085
const creditsEl = document.getElementById('authCredits');
@@ -1990,7 +2091,8 @@ ${commonRules}`;
19902091
} else {
19912092
if (authPanel) authPanel.style.display = 'none';
19922093
if (userRow) userRow.style.display = 'none';
1993-
if (authLabel) authLabel.textContent = '🔑 账户登录';
2094+
if (authLabel) authLabel.textContent = '🔑 账号';
2095+
if (statusDot) { statusDot.style.background = 'var(--muted)'; statusDot.title = '未登录'; }
19942096
if (authArrow) { authArrow.textContent = '▾'; authArrow.style.display = ''; }
19952097
}
19962098
}
@@ -2377,9 +2479,15 @@ ${commonRules}`;
23772479
if (msg.success) {
23782480
setAuthState(true, msg.user);
23792481
showAuthError('');
2482+
showRegError('');
23802483
} else {
23812484
setAuthState(false, null);
2382-
if (msg.error) { showAuthError(msg.error); }
2485+
const regFormEl = document.getElementById('registerForm');
2486+
const isRegTab = regFormEl && regFormEl.style.display !== 'none';
2487+
if (msg.error) {
2488+
if (isRegTab) showRegError(msg.error);
2489+
else showAuthError(msg.error);
2490+
}
23832491
}
23842492
break;
23852493
case "shareResult":

web/functions/api/[[path]].js

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* Cloudflare Pages Function — API 反向代理
3+
*
4+
* 将所有 /api/* 请求透明代理到后端服务器,
5+
* 解决 Cloudflare Pages 静态托管无法访问后端 API 的问题。
6+
*/
7+
8+
const BACKEND = 'https://testpilot.xinzaoai.com';
9+
10+
export async function onRequest(context) {
11+
const url = new URL(context.request.url);
12+
const target = BACKEND + url.pathname + url.search;
13+
14+
// 透传原始请求(方法、Headers、Body)
15+
const req = new Request(target, {
16+
method: context.request.method,
17+
headers: context.request.headers,
18+
body: context.request.body,
19+
redirect: 'follow',
20+
});
21+
22+
const res = await fetch(req);
23+
24+
// 透传响应,追加 CORS 头
25+
const headers = new Headers(res.headers);
26+
headers.set('Access-Control-Allow-Origin', '*');
27+
headers.set('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');
28+
headers.set('Access-Control-Allow-Headers', 'Content-Type,Authorization');
29+
30+
if (context.request.method === 'OPTIONS') {
31+
return new Response(null, { status: 204, headers });
32+
}
33+
34+
return new Response(res.body, {
35+
status: res.status,
36+
statusText: res.statusText,
37+
headers,
38+
});
39+
}

web/src/App.jsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { BrowserRouter, Routes, Route } from 'react-router-dom';
1+
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
22
import { AuthProvider } from './context/AuthContext';
33
import { LocaleProvider } from './context/LocaleContext';
44
import Layout from './components/Layout';
@@ -31,6 +31,7 @@ export default function App() {
3131
<Route path="/leaderboard" element={<LeaderboardPage />} />
3232
<Route path="/pricing" element={<PricingPage />} />
3333
<Route path="/login" element={<LoginPage />} />
34+
<Route path="/register" element={<Navigate to="/login?tab=register" replace />} />
3435
<Route path="/share" element={<SharePage />} />
3536
<Route path="/download" element={<DownloadPage />} />
3637
<Route path="/terms" element={<TermsPage />} />

web/src/lib/api.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,15 @@ async function request(method, path, body) {
1717
window.location.href = '/login';
1818
return null;
1919
}
20-
const data = await res.json();
21-
if (!res.ok) throw new Error(data.detail || '请求失败');
20+
// 安全解析:空 body 或非 JSON 时不崩溃
21+
const text = await res.text();
22+
let data = null;
23+
if (text) {
24+
try { data = JSON.parse(text); } catch {
25+
if (!res.ok) throw new Error(`服务器返回非JSON响应 (HTTP ${res.status})`);
26+
}
27+
}
28+
if (!res.ok) throw new Error((data && data.detail) || `请求失败 (HTTP ${res.status})`);
2229
return data;
2330
}
2431

0 commit comments

Comments
 (0)