app.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643
  1. // 研创·智核 - 主应用JavaScript
  2. class InnoCoreApp {
  3. constructor() {
  4. this.api = new API();
  5. this.router = new Router();
  6. this.state = new StateManager();
  7. this.init();
  8. }
  9. init() {
  10. this.setupEventListeners();
  11. this.setupRouter();
  12. this.checkAuth();
  13. }
  14. setupEventListeners() {
  15. // 全局事件监听
  16. document.addEventListener('click', (e) => {
  17. if (e.target.matches('[data-action]')) {
  18. this.handleAction(e.target.dataset.action, e.target);
  19. }
  20. });
  21. // 表单提交
  22. document.addEventListener('submit', (e) => {
  23. if (e.target.matches('.ajax-form')) {
  24. e.preventDefault();
  25. this.handleFormSubmit(e.target);
  26. }
  27. });
  28. // 模态框关闭
  29. document.addEventListener('click', (e) => {
  30. if (e.target.matches('.modal') || e.target.matches('.modal-close')) {
  31. this.closeModal(e.target.closest('.modal'));
  32. }
  33. });
  34. }
  35. setupRouter() {
  36. // 路由配置
  37. this.router.addRoute('/', () => this.showDashboard());
  38. this.router.addRoute('/papers', () => this.showPapers());
  39. this.router.addRoute('/papers/new', () => this.showNewPaper());
  40. this.router.addRoute('/papers/:id', (params) => this.showPaperDetail(params.id));
  41. this.router.addRoute('/tasks', () => this.showTasks());
  42. this.router.addRoute('/tasks/:id', (params) => this.showTaskDetail(params.id));
  43. this.router.addRoute('/analysis', () => this.showAnalysis());
  44. this.router.addRoute('/writing', () => this.showWriting());
  45. this.router.addRoute('/profile', () => this.showProfile());
  46. }
  47. async checkAuth() {
  48. const token = localStorage.getItem('token');
  49. if (token) {
  50. try {
  51. const user = await this.api.get('/auth/me');
  52. this.state.setUser(user);
  53. this.updateUI();
  54. } catch (error) {
  55. localStorage.removeItem('token');
  56. this.showLogin();
  57. }
  58. } else {
  59. this.showLogin();
  60. }
  61. }
  62. updateUI() {
  63. const user = this.state.getUser();
  64. if (user) {
  65. document.querySelector('.user-name').textContent = user.username;
  66. document.querySelector('.user-email').textContent = user.email;
  67. }
  68. }
  69. // 页面显示方法
  70. async showDashboard() {
  71. const content = document.getElementById('content');
  72. content.innerHTML = await this.loadTemplate('dashboard');
  73. // 加载统计数据
  74. const stats = await this.api.get('/dashboard/stats');
  75. this.renderDashboardStats(stats);
  76. // 加载最近任务
  77. const tasks = await this.api.get('/tasks?limit=5');
  78. this.renderRecentTasks(tasks);
  79. }
  80. async showPapers() {
  81. const content = document.getElementById('content');
  82. content.innerHTML = await this.loadTemplate('papers');
  83. // 加载论文列表
  84. const papers = await this.api.get('/papers');
  85. this.renderPapersList(papers);
  86. }
  87. async showNewPaper() {
  88. const content = document.getElementById('content');
  89. content.innerHTML = await this.loadTemplate('new-paper');
  90. // 初始化表单
  91. this.initPaperForm();
  92. }
  93. async showPaperDetail(paperId) {
  94. const content = document.getElementById('content');
  95. content.innerHTML = await this.loadTemplate('paper-detail');
  96. // 加载论文详情
  97. const paper = await this.api.get(`/papers/${paperId}`);
  98. this.renderPaperDetail(paper);
  99. }
  100. async showTasks() {
  101. const content = document.getElementById('content');
  102. content.innerHTML = await this.loadTemplate('tasks');
  103. // 加载任务列表
  104. const tasks = await this.api.get('/tasks');
  105. this.renderTasksList(tasks);
  106. }
  107. async showTaskDetail(taskId) {
  108. const content = document.getElementById('content');
  109. content.innerHTML = await this.loadTemplate('task-detail');
  110. // 加载任务详情
  111. const task = await this.api.get(`/tasks/${taskId}`);
  112. this.renderTaskDetail(task);
  113. // 如果任务正在运行,开始轮询状态
  114. if (task.status === 'running') {
  115. this.startTaskPolling(taskId);
  116. }
  117. }
  118. async showAnalysis() {
  119. const content = document.getElementById('content');
  120. content.innerHTML = await this.loadTemplate('analysis');
  121. // 加载分析列表
  122. const analyses = await this.api.get('/analysis');
  123. this.renderAnalysisList(analyses);
  124. }
  125. async showWriting() {
  126. const content = document.getElementById('content');
  127. content.innerHTML = await this.loadTemplate('writing');
  128. // 加载写作列表
  129. const writings = await this.api.get('/writing');
  130. this.renderWritingList(writings);
  131. }
  132. async showProfile() {
  133. const content = document.getElementById('content');
  134. content.innerHTML = await this.loadTemplate('profile');
  135. // 加载用户信息
  136. const user = await this.api.get('/auth/me');
  137. this.renderProfile(user);
  138. }
  139. async showLogin() {
  140. const content = document.getElementById('content');
  141. content.innerHTML = await this.loadTemplate('login');
  142. // 初始化登录表单
  143. this.initLoginForm();
  144. }
  145. // 事件处理方法
  146. async handleAction(action, element) {
  147. switch (action) {
  148. case 'logout':
  149. await this.logout();
  150. break;
  151. case 'delete-paper':
  152. await this.deletePaper(element.dataset.id);
  153. break;
  154. case 'delete-task':
  155. await this.deleteTask(element.dataset.id);
  156. break;
  157. case 'cancel-task':
  158. await this.cancelTask(element.dataset.id);
  159. break;
  160. case 'retry-task':
  161. await this.retryTask(element.dataset.id);
  162. break;
  163. case 'export-paper':
  164. await this.exportPaper(element.dataset.id, element.dataset.format);
  165. break;
  166. case 'show-modal':
  167. this.showModal(element.dataset.target);
  168. break;
  169. case 'close-modal':
  170. this.closeModal(element.closest('.modal'));
  171. break;
  172. }
  173. }
  174. async handleFormSubmit(form) {
  175. const formData = new FormData(form);
  176. const data = Object.fromEntries(formData.entries());
  177. try {
  178. this.showLoading(form);
  179. const endpoint = form.dataset.endpoint;
  180. const method = form.dataset.method || 'POST';
  181. let result;
  182. if (method === 'POST') {
  183. result = await this.api.post(endpoint, data);
  184. } else if (method === 'PUT') {
  185. result = await this.api.put(endpoint, data);
  186. }
  187. this.showSuccess('操作成功!');
  188. // 根据结果跳转
  189. if (form.dataset.redirect) {
  190. this.router.navigate(form.dataset.redirect);
  191. } else if (result.id) {
  192. this.router.navigate(`/${form.dataset.resource}/${result.id}`);
  193. }
  194. } catch (error) {
  195. this.showError(error.message);
  196. } finally {
  197. this.hideLoading(form);
  198. }
  199. }
  200. // API调用方法
  201. async createLiteratureSearchTask(query, options = {}) {
  202. const data = {
  203. title: `文献搜索: ${query}`,
  204. task_type: 'literature_search',
  205. parameters: {
  206. query,
  207. max_papers: options.maxPapers || 20,
  208. year_range: options.yearRange,
  209. venues: options.venues || []
  210. }
  211. };
  212. const task = await this.api.post('/tasks', data);
  213. this.router.navigate(`/tasks/${task.id}`);
  214. return task;
  215. }
  216. async createAnalysisTask(paperIds, analysisType) {
  217. const data = {
  218. title: `论文分析: ${analysisType}`,
  219. task_type: 'analysis',
  220. parameters: {
  221. paper_ids: paperIds,
  222. analysis_type: analysisType
  223. }
  224. };
  225. const task = await this.api.post('/tasks', data);
  226. this.router.navigate(`/tasks/${task.id}`);
  227. return task;
  228. }
  229. async createWritingTask(paperIds, writingType, outline) {
  230. const data = {
  231. title: `学术写作: ${writingType}`,
  232. task_type: 'writing',
  233. parameters: {
  234. paper_ids: paperIds,
  235. writing_type: writingType,
  236. outline: outline
  237. }
  238. };
  239. const task = await this.api.post('/tasks', data);
  240. this.router.navigate(`/tasks/${task.id}`);
  241. return task;
  242. }
  243. // 渲染方法
  244. renderDashboardStats(stats) {
  245. const container = document.getElementById('stats-container');
  246. container.innerHTML = `
  247. <div class="stats-grid">
  248. <div class="stat-card">
  249. <h3>${stats.total_papers}</h3>
  250. <p>论文总数</p>
  251. </div>
  252. <div class="stat-card">
  253. <h3>${stats.total_tasks}</h3>
  254. <p>任务总数</p>
  255. </div>
  256. <div class="stat-card">
  257. <h3>${stats.total_analyses}</h3>
  258. <p>分析报告</p>
  259. </div>
  260. <div class="stat-card">
  261. <h3>${stats.total_writings}</h3>
  262. <p>写作文档</p>
  263. </div>
  264. </div>
  265. `;
  266. }
  267. renderPapersList(papers) {
  268. const container = document.getElementById('papers-list');
  269. container.innerHTML = papers.map(paper => `
  270. <div class="paper-card" data-id="${paper.id}">
  271. <h3>${paper.title}</h3>
  272. <p class="authors">${paper.authors.join(', ')}</p>
  273. <p class="abstract">${paper.abstract || '暂无摘要'}</p>
  274. <div class="paper-meta">
  275. <span class="badge badge-primary">${paper.publication_year || '未知年份'}</span>
  276. <span class="badge badge-secondary">${paper.journal || '未知期刊'}</span>
  277. </div>
  278. <div class="paper-actions">
  279. <button class="btn btn-sm btn-primary" data-action="view-paper" data-id="${paper.id}">查看</button>
  280. <button class="btn btn-sm btn-outline" data-action="export-paper" data-id="${paper.id}">导出</button>
  281. <button class="btn btn-sm btn-danger" data-action="delete-paper" data-id="${paper.id}">删除</button>
  282. </div>
  283. </div>
  284. `).join('');
  285. }
  286. renderTasksList(tasks) {
  287. const container = document.getElementById('tasks-list');
  288. container.innerHTML = tasks.map(task => `
  289. <div class="task-card" data-id="${task.id}">
  290. <h3>${task.title}</h3>
  291. <p class="task-description">${task.description || '暂无描述'}</p>
  292. <div class="task-meta">
  293. <span class="badge badge-${this.getStatusClass(task.status)}">${this.getStatusText(task.status)}</span>
  294. <span class="task-type">${this.getTaskTypeText(task.task_type)}</span>
  295. </div>
  296. <div class="task-progress">
  297. <div class="progress">
  298. <div class="progress-bar" style="width: ${task.progress}%"></div>
  299. </div>
  300. <span class="progress-text">${task.progress}%</span>
  301. </div>
  302. <div class="task-actions">
  303. <button class="btn btn-sm btn-primary" data-action="view-task" data-id="${task.id}">查看</button>
  304. ${task.status === 'running' ? `<button class="btn btn-sm btn-warning" data-action="cancel-task" data-id="${task.id}">取消</button>` : ''}
  305. ${task.status === 'failed' ? `<button class="btn btn-sm btn-secondary" data-action="retry-task" data-id="${task.id}">重试</button>` : ''}
  306. </div>
  307. </div>
  308. `).join('');
  309. }
  310. // 工具方法
  311. getStatusClass(status) {
  312. const classes = {
  313. 'pending': 'warning',
  314. 'running': 'primary',
  315. 'completed': 'success',
  316. 'failed': 'danger'
  317. };
  318. return classes[status] || 'secondary';
  319. }
  320. getStatusText(status) {
  321. const texts = {
  322. 'pending': '等待中',
  323. 'running': '运行中',
  324. 'completed': '已完成',
  325. 'failed': '失败'
  326. };
  327. return texts[status] || status;
  328. }
  329. getTaskTypeText(type) {
  330. const texts = {
  331. 'literature_search': '文献搜索',
  332. 'analysis': '论文分析',
  333. 'writing': '学术写作'
  334. };
  335. return texts[type] || type;
  336. }
  337. async loadTemplate(templateName) {
  338. try {
  339. const response = await fetch(`/templates/${templateName}.html`);
  340. return await response.text();
  341. } catch (error) {
  342. console.error('Failed to load template:', error);
  343. return '<div>模板加载失败</div>';
  344. }
  345. }
  346. showModal(modalId) {
  347. const modal = document.getElementById(modalId);
  348. if (modal) {
  349. modal.classList.add('show');
  350. }
  351. }
  352. closeModal(modal) {
  353. if (modal) {
  354. modal.classList.remove('show');
  355. }
  356. }
  357. showLoading(element) {
  358. element.disabled = true;
  359. element.innerHTML = '<span class="spinner"></span> 处理中...';
  360. }
  361. hideLoading(element) {
  362. element.disabled = false;
  363. element.innerHTML = element.dataset.originalText || element.textContent;
  364. }
  365. showSuccess(message) {
  366. this.showNotification(message, 'success');
  367. }
  368. showError(message) {
  369. this.showNotification(message, 'error');
  370. }
  371. showNotification(message, type = 'info') {
  372. const notification = document.createElement('div');
  373. notification.className = `notification notification-${type}`;
  374. notification.textContent = message;
  375. document.body.appendChild(notification);
  376. setTimeout(() => {
  377. notification.remove();
  378. }, 3000);
  379. }
  380. startTaskPolling(taskId) {
  381. const poll = async () => {
  382. try {
  383. const task = await this.api.get(`/tasks/${taskId}`);
  384. this.updateTaskStatus(task);
  385. if (task.status === 'completed' || task.status === 'failed') {
  386. clearInterval(this.pollingInterval);
  387. }
  388. } catch (error) {
  389. console.error('Task polling error:', error);
  390. }
  391. };
  392. this.pollingInterval = setInterval(poll, 2000);
  393. }
  394. updateTaskStatus(task) {
  395. const progressBar = document.querySelector('.progress-bar');
  396. const progressText = document.querySelector('.progress-text');
  397. const statusBadge = document.querySelector('.task-meta .badge');
  398. if (progressBar) progressBar.style.width = `${task.progress}%`;
  399. if (progressText) progressText.textContent = `${task.progress}%`;
  400. if (statusBadge) {
  401. statusBadge.className = `badge badge-${this.getStatusClass(task.status)}`;
  402. statusBadge.textContent = this.getStatusText(task.status);
  403. }
  404. }
  405. async logout() {
  406. localStorage.removeItem('token');
  407. this.state.clearUser();
  408. this.router.navigate('/login');
  409. }
  410. }
  411. // API类
  412. class API {
  413. constructor() {
  414. this.baseURL = '/api/v1';
  415. }
  416. async request(endpoint, options = {}) {
  417. const token = localStorage.getItem('token');
  418. const url = `${this.baseURL}${endpoint}`;
  419. const config = {
  420. headers: {
  421. 'Content-Type': 'application/json',
  422. ...(token && { 'Authorization': `Bearer ${token}` }),
  423. ...options.headers
  424. },
  425. ...options
  426. };
  427. const response = await fetch(url, config);
  428. if (!response.ok) {
  429. const error = await response.json();
  430. throw new Error(error.message || '请求失败');
  431. }
  432. return await response.json();
  433. }
  434. get(endpoint) {
  435. return this.request(endpoint);
  436. }
  437. post(endpoint, data) {
  438. return this.request(endpoint, {
  439. method: 'POST',
  440. body: JSON.stringify(data)
  441. });
  442. }
  443. put(endpoint, data) {
  444. return this.request(endpoint, {
  445. method: 'PUT',
  446. body: JSON.stringify(data)
  447. });
  448. }
  449. delete(endpoint) {
  450. return this.request(endpoint, {
  451. method: 'DELETE'
  452. });
  453. }
  454. }
  455. // 路由类
  456. class Router {
  457. constructor() {
  458. this.routes = new Map();
  459. this.currentPath = window.location.pathname;
  460. window.addEventListener('popstate', () => {
  461. this.handleRoute();
  462. });
  463. }
  464. addRoute(path, handler) {
  465. this.routes.set(path, handler);
  466. }
  467. navigate(path) {
  468. window.history.pushState({}, '', path);
  469. this.currentPath = path;
  470. this.handleRoute();
  471. }
  472. handleRoute() {
  473. const path = window.location.pathname;
  474. for (const [route, handler] of this.routes) {
  475. if (this.matchRoute(route, path)) {
  476. const params = this.extractParams(route, path);
  477. handler(params);
  478. return;
  479. }
  480. }
  481. // 404处理
  482. this.show404();
  483. }
  484. matchRoute(route, path) {
  485. const routeParts = route.split('/');
  486. const pathParts = path.split('/');
  487. if (routeParts.length !== pathParts.length) {
  488. return false;
  489. }
  490. return routeParts.every((part, index) => {
  491. return part.startsWith(':') || part === pathParts[index];
  492. });
  493. }
  494. extractParams(route, path) {
  495. const routeParts = route.split('/');
  496. const pathParts = path.split('/');
  497. const params = {};
  498. routeParts.forEach((part, index) => {
  499. if (part.startsWith(':')) {
  500. const paramName = part.substring(1);
  501. params[paramName] = pathParts[index];
  502. }
  503. });
  504. return params;
  505. }
  506. show404() {
  507. document.getElementById('content').innerHTML = '<h1>页面未找到</h1>';
  508. }
  509. }
  510. // 状态管理类
  511. class StateManager {
  512. constructor() {
  513. this.state = {
  514. user: null,
  515. currentTask: null,
  516. notifications: []
  517. };
  518. }
  519. setUser(user) {
  520. this.state.user = user;
  521. }
  522. getUser() {
  523. return this.state.user;
  524. }
  525. clearUser() {
  526. this.state.user = null;
  527. }
  528. setCurrentTask(task) {
  529. this.state.currentTask = task;
  530. }
  531. getCurrentTask() {
  532. return this.state.currentTask;
  533. }
  534. addNotification(notification) {
  535. this.state.notifications.push(notification);
  536. }
  537. getNotifications() {
  538. return this.state.notifications;
  539. }
  540. }
  541. // 初始化应用
  542. document.addEventListener('DOMContentLoaded', () => {
  543. window.app = new InnoCoreApp();
  544. });