P8 平台轻量化改造 + SSG修复 + 编程导师 + 文档完善
- Prisma: Tool 模型加 affiliateLink;免费用户沙盒 10→5 次/日 - 后端: Tools API + /admin/tools CRUD 5 端点;Practices 完整模块 - 导航: 主菜单隐藏企业版/社区(URL 可访问) - 首页: 重定位为 AI 工具指南;新增精选工具区块;Feature 重写 - 工具页: affiliateLink 绿色推荐 Badge - SSG 修复: config.ts 构建时直连 localhost:4000,页面 108→127 - 沙盒: 新增编程导师场景(苏格拉底教学法) - 练习系统: Practices 多场景练习(含结构化评分) - 技能广场: 6 个付费 Skill(标题大师/回款助手等) - 管理后台: Models/Posts/Practices CRUD 页面 - 文档: README + progress.md 全面更新;AGENTS.md 同步定位 - 清理: .env.example 移除;tsbuildinfo gitignore
This commit is contained in:
@@ -59,8 +59,8 @@ export function getAssistantContext(pathname: string): AssistantContext {
|
||||
const sorted = Object.keys(PAGE_CONTEXTS).sort((a, b) => b.length - a.length)
|
||||
for (const key of sorted) {
|
||||
if (pathname.startsWith(key)) {
|
||||
return PAGE_CONTEXTS[key]
|
||||
return { ...PAGE_CONTEXTS[key], page: key }
|
||||
}
|
||||
}
|
||||
return PAGE_CONTEXTS['/']
|
||||
return { ...PAGE_CONTEXTS['/'], page: '/' }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react';
|
||||
import { setTokens, clearTokens, initAuth, isLoggedIn, apiLogout, getToken } from './auth';
|
||||
|
||||
interface AuthContextType {
|
||||
isLoggedIn: boolean;
|
||||
@@ -17,28 +18,22 @@ const AuthContext = createContext<AuthContextType>({
|
||||
});
|
||||
|
||||
export function AuthProvider({ children }: { children: ReactNode }) {
|
||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||
const [initialized, setInitialized] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoggedIn(!!localStorage.getItem('token'));
|
||||
setInitialized(true);
|
||||
initAuth().then(() => setInitialized(true));
|
||||
}, []);
|
||||
|
||||
const login = useCallback((token: string, refreshTk?: string) => {
|
||||
localStorage.setItem('token', token);
|
||||
if (refreshTk) localStorage.setItem('refreshToken', refreshTk);
|
||||
setIsLoggedIn(true);
|
||||
setTokens(token, refreshTk);
|
||||
}, []);
|
||||
|
||||
const logout = useCallback(() => {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('refreshToken');
|
||||
setIsLoggedIn(false);
|
||||
apiLogout();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<AuthContext.Provider value={{ isLoggedIn, initialized, login, logout }}>
|
||||
<AuthContext.Provider value={{ isLoggedIn: isLoggedIn() || !!getToken(), initialized, login, logout }}>
|
||||
{children}
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
|
||||
+52
-15
@@ -1,40 +1,69 @@
|
||||
import { API_BASE } from '@/lib/config';
|
||||
|
||||
let memoryToken: string | null = null;
|
||||
let memoryRefreshToken: string | null = null;
|
||||
let memoryAdminToken: string | null = null;
|
||||
let initPromise: Promise<void> | null = null;
|
||||
|
||||
export function getToken(): string | null {
|
||||
if (typeof window === 'undefined') return null;
|
||||
return localStorage.getItem('token');
|
||||
return memoryToken;
|
||||
}
|
||||
|
||||
export function getAdminToken(): string | null {
|
||||
if (typeof window === 'undefined') return null;
|
||||
return localStorage.getItem('adminToken');
|
||||
return memoryAdminToken;
|
||||
}
|
||||
|
||||
export function getRefreshToken(): string | null {
|
||||
if (typeof window === 'undefined') return null;
|
||||
return localStorage.getItem('refreshToken');
|
||||
return memoryRefreshToken;
|
||||
}
|
||||
|
||||
export function setTokens(accessToken: string, refreshToken?: string) {
|
||||
localStorage.setItem('token', accessToken);
|
||||
if (refreshToken) localStorage.setItem('refreshToken', refreshToken);
|
||||
memoryToken = accessToken;
|
||||
if (refreshToken) memoryRefreshToken = refreshToken;
|
||||
}
|
||||
|
||||
export function clearTokens() {
|
||||
localStorage.removeItem('token');
|
||||
localStorage.removeItem('refreshToken');
|
||||
memoryToken = null;
|
||||
memoryRefreshToken = null;
|
||||
}
|
||||
|
||||
export function setAdminToken(token: string) {
|
||||
memoryAdminToken = token;
|
||||
}
|
||||
|
||||
export function clearAdminToken() {
|
||||
localStorage.removeItem('adminToken');
|
||||
memoryAdminToken = null;
|
||||
}
|
||||
|
||||
export function isLoggedIn(): boolean {
|
||||
return !!getToken();
|
||||
return !!memoryToken;
|
||||
}
|
||||
|
||||
export function isAdminLoggedIn(): boolean {
|
||||
return !!getAdminToken();
|
||||
return !!memoryAdminToken;
|
||||
}
|
||||
|
||||
export async function initAuth(): Promise<void> {
|
||||
if (initPromise) return initPromise;
|
||||
if (typeof window === 'undefined') return;
|
||||
if (memoryToken) return;
|
||||
|
||||
initPromise = (async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/auth/verify`, {
|
||||
credentials: 'include',
|
||||
});
|
||||
if (res.ok) {
|
||||
const data = await res.json();
|
||||
if (data.accessToken) memoryToken = data.accessToken;
|
||||
return;
|
||||
}
|
||||
} catch {
|
||||
}
|
||||
})();
|
||||
|
||||
await initPromise;
|
||||
initPromise = null;
|
||||
}
|
||||
|
||||
export async function adminApiFetch(url: string, opts: RequestInit = {}): Promise<Response> {
|
||||
@@ -57,6 +86,7 @@ export async function refreshToken(): Promise<string | null> {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ accessToken: token }),
|
||||
credentials: 'include',
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.message);
|
||||
@@ -76,15 +106,22 @@ export async function apiFetch(url: string, opts: RequestInit = {}): Promise<Res
|
||||
};
|
||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||
|
||||
let res = await fetch(`${API_BASE}${url}`, { ...opts, headers });
|
||||
let res = await fetch(`${API_BASE}${url}`, { ...opts, headers, credentials: 'include' });
|
||||
|
||||
if (res.status === 401 && token) {
|
||||
const newToken = await refreshToken();
|
||||
if (newToken) {
|
||||
headers['Authorization'] = `Bearer ${newToken}`;
|
||||
res = await fetch(`${API_BASE}${url}`, { ...opts, headers });
|
||||
res = await fetch(`${API_BASE}${url}`, { ...opts, headers, credentials: 'include' });
|
||||
}
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
export async function apiLogout(): Promise<void> {
|
||||
try {
|
||||
await fetch(`${API_BASE}/auth/logout`, { method: 'POST', credentials: 'include' });
|
||||
} catch {}
|
||||
clearTokens();
|
||||
}
|
||||
|
||||
@@ -1,4 +1,12 @@
|
||||
export const API_BASE = process.env.NEXT_PUBLIC_API_URL || '/api/v1';
|
||||
export const API_BASE = (() => {
|
||||
const configured = process.env.NEXT_PUBLIC_API_URL;
|
||||
// During SSG/build in Node.js, relative paths like /api/v1 won't resolve
|
||||
// Use the local backend directly (assumes backend runs on port 4000 during build)
|
||||
if (typeof window === 'undefined' && configured?.startsWith('/')) {
|
||||
return 'http://localhost:4000/api/v1';
|
||||
}
|
||||
return configured || '/api/v1';
|
||||
})();
|
||||
|
||||
export const SITE_URL = process.env.NEXT_PUBLIC_SITE_URL || 'https://yuzhiran.com';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user