Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,108 changes: 1,108 additions & 0 deletions web/css/style.css

Large diffs are not rendered by default.

49 changes: 49 additions & 0 deletions web/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>NiuGuide - Java 面试指南</title>

<!-- Font Awesome 图标库 -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css">
<!-- Chart.js 图表库 -->
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>

<!-- 样式 -->
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<div id="app"></div>

<!-- 工具函数 & 配置 -->
<script src="js/config.js"></script>
<script src="js/utils.js"></script>

<!-- API 请求封装 -->
<script src="js/api.js"></script>

<!-- 路由 -->
<script src="js/router.js"></script>

<!-- 公共组件 -->
<script src="js/components.js"></script>

<!-- 所有页面(按需加载可通过动态 import 优化,此处为简化全部加载) -->
<script src="js/pages/login.js"></script>
<script src="js/pages/dashboard.js"></script>
<script src="js/pages/categories.js"></script>
<script src="js/pages/questionList.js"></script>
<script src="js/pages/questionDetail.js"></script>
<script src="js/pages/random.js"></script>
<script src="js/pages/interview.js"></script>
<script src="js/pages/favorites.js"></script>
<script src="js/pages/review.js"></script>

<script src="js/pages/profile.js"></script>


<!-- 应用入口 -->
<script src="js/app.js"></script>
</body>
</html>
142 changes: 142 additions & 0 deletions web/js/api.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/**
* 统一 API 请求封装
* 自动携带 Token(Authorization 头),适配 Sa-Token 认证
* 统一错误处理
*/
const API = {
/**
* 从 localStorage 获取 token
*/
getToken() {
return localStorage.getItem('token');
},

/**
* 保存 token
*/
setToken(token) {
localStorage.setItem('token', token);
},

/**
* 清除 token
*/
clearToken() {
localStorage.removeItem('token');
},

/**
* 是否已登录(有 token)
*/
isLoggedIn() {
return !!this.getToken();
},

/**
* 核心请求方法
* @param {string} method - HTTP 方法
* @param {string} url - 请求路径(相对于 API_BASE)
* @param {object} [body] - 请求体(可选)
* @param {object} [params] - URL 查询参数(可选)
* @returns {Promise<object>} 统一返回 Result.data
*/
async request(method, url, body, params) {
// 构建完整 URL
let fullUrl = CONFIG.API_BASE + url;

// 拼接查询参数
if (params) {
const query = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== '') {
query.append(key, value);
}
});
const qs = query.toString();
if (qs) fullUrl += '?' + qs;
}

// 构建请求头
const headers = {};
const token = this.getToken();
if (token) {
headers['Authorization'] = token;
}

// 构建请求配置
const options = { method, headers };

// 处理请求体
if (body !== undefined && body !== null) {
// 如果是 FormData,不设置 Content-Type
if (body instanceof FormData) {
options.body = body;
} else {
headers['Content-Type'] = 'application/json';
options.body = JSON.stringify(body);
}
}

try {
const resp = await fetch(fullUrl, options);
const result = await resp.json();

// 业务失败处理
if (result.code !== 200) {
// 401 未登录,清除 token 并跳转登录
if (result.code === 401) {
this.clearToken();
// 延迟跳转避免干扰
setTimeout(() => {
if (window.APP) {
window.APP.navigate('/login');
}
}, 100);
}
throw new Error(result.msg || '请求失败');
}

return result.data;
} catch (err) {
// 网络错误友好提示
if (err.message === 'Failed to fetch') {
throw new Error('网络连接失败,请检查后端服务是否启动');
}
throw err;
}
},

// ---- 便捷方法 ----

/** GET 请求 */
get(url, params) {
return this.request('GET', url, null, params);
},

/** POST 请求(JSON body) */
post(url, body) {
return this.request('POST', url, body);
},

/** DELETE 请求 */
del(url) {
return this.request('DELETE', url);
},

/** 文件上传(FormData) */
upload(url, formData) {
const token = this.getToken();
const headers = {};
if (token) {
headers['Authorization'] = token;
}
return fetch(CONFIG.API_BASE + url, {
method: 'POST',
headers,
body: formData
}).then(r => r.json()).then(result => {
if (result.code !== 200) throw new Error(result.msg);
return result.data;
});
}
};
154 changes: 154 additions & 0 deletions web/js/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
/**
* 应用入口
* 初始化布局、路由、事件绑定
*/
const APP = {
/** 当前用户信息缓存 */
currentUser: null,

/** 初始化应用 */
async init() {
// 渲染基础布局
this.renderLayout();

// 注册路由
this.registerRoutes();

// 路由前置钩子:登录检查
router.beforeEach((path) => {
// 公开页面无需登录
const publicPaths = ['/login'];

if (publicPaths.some(p => path.startsWith(p))) return true;

if (!API.isLoggedIn()) {
router.navigate('/login');
return false;
}
return true;
});

// 绑定全局事件
this.bindEvents();

// 启动路由
router.start();

// 如果已登录,获取用户信息
if (API.isLoggedIn()) {
try {
this.currentUser = await API.get('/api/auth/me');
this.updateUserInfo();
} catch (e) {
// token 失效,跳转登录
router.navigate('/login');
}
}
},

/** 当前路由是否为登录页 */
isLoginPage() {
const hash = window.location.hash || '';
return hash === '#/login' || hash === '#/' || hash === '';
},

/** 渲染基础布局 */
renderLayout() {
const app = $('#app');
app.innerHTML = `
<div class="sidebar-overlay" id="sidebarOverlay"></div>
${renderSidebar()}
${renderDesktopHeader()}
${renderMobileHeader()}
<div class="main-wrapper">
<div class="main-content" id="main-content"></div>
</div>
<div id="toast-container"></div>
`;
// 移动端底部导航直接挂到 body,避免 position: fixed 受父容器影响
document.body.insertAdjacentHTML('beforeend', renderMobileTabs());
},



/** 注册所有路由 */
registerRoutes() {
router.register('/login', () => renderLogin());
router.register('/dashboard', () => renderDashboard());
router.register('/categories', () => renderCategories());
router.register('/questions', (params) => renderQuestionList(params));
router.register('/question/:id', (params) => renderQuestionDetail(params));
router.register('/random', () => renderRandom());
router.register('/interview', () => renderInterview());
router.register('/favorites', () => renderFavorites());
router.register('/review', () => renderReview());

router.register('/profile', () => renderProfile());

},

/** 绑定全局事件 */
bindEvents() {
// 移动端菜单切换
$('#menuToggle')?.addEventListener('click', () => {
$('#sidebar').classList.toggle('open');
$('#sidebarOverlay').classList.toggle('show');
});

// 遮罩层关闭侧栏
$('#sidebarOverlay')?.addEventListener('click', () => {
$('#sidebar').classList.remove('open');
$('#sidebarOverlay').classList.remove('show');
});

// 移动端 Tab 点击
document.addEventListener('click', e => {
const tab = e.target.closest('.tab-item');
if (tab && tab.dataset.route) {
router.navigate(tab.dataset.route);
}
});

// 答案点击展示(点击模糊的答案区域显示内容)
document.addEventListener('click', e => {
const answerEl = e.target.closest('.answer-hidden:not(.revealed)');
if (answerEl) {
answerEl.classList.add('revealed');
}
});

},

/** 更新用户信息显示 */
updateUserInfo() {
if (this.currentUser) {
const nickname = this.currentUser.nickname || this.currentUser.username;
const el = $('#headerNickname');
if (el) el.textContent = nickname;
}
},

/** 退出登录 */
async logout() {
try {
await API.post('/api/auth/logout');
} catch (e) { /* ignore */ }
API.clearToken();
this.currentUser = null;
router.navigate('/login');
},

/** 更新面包屑 */
setBreadcrumb(text) {
const el = $('#breadcrumb');
if (el) el.textContent = text;
},

/** 获取当前用户 ID */
getUserId() {
return this.currentUser?.id;
}
};

// 启动应用
document.addEventListener('DOMContentLoaded', () => APP.init());
Loading