init: TEALab 2025 实验室网站项目

从百度网盘 /apps/bypy/dofile/ 下载的完整项目结构,包含:
- dofile/: Flask 后端(端口 5000)+ 静态前端(端口 6002)
- officefile/: 项目相关文档
- .gitignore: Python venv / 日志 / 缓存

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-06-14 01:07:19 +08:00
commit a3eb5c7b2d
39 changed files with 7110 additions and 0 deletions
+390
View File
@@ -0,0 +1,390 @@
// Admin Panel JavaScript
let currentArticles = [];
let currentResources = [];
let currentTeamMembers = [];
// 检查管理员权限
async function checkAdminAccess() {
try {
const response = await window.DAELAPI.auth.checkAuth();
if (!response.authenticated || !response.user.is_admin) {
alert('需要管理员权限才能访问此页面');
window.location.href = 'website.html';
return false;
}
document.getElementById('admin-user').textContent = `管理员: ${response.user.username}`;
return true;
} catch (error) {
alert('无法验证权限,请先登录');
window.location.href = 'login.html';
return false;
}
}
// Tab切换
document.querySelectorAll('.admin-tab').forEach(tab => {
tab.addEventListener('click', () => {
const tabName = tab.dataset.tab;
// 更新tab样式
document.querySelectorAll('.admin-tab').forEach(t => {
t.classList.remove('active', 'border-black', 'text-black');
t.classList.add('text-gray-500');
});
tab.classList.add('active', 'border-black', 'text-black');
tab.classList.remove('text-gray-500');
// 更新内容
document.querySelectorAll('.admin-tab-content').forEach(content => {
content.classList.add('hidden');
});
document.getElementById(`tab-${tabName}`).classList.remove('hidden');
// 加载对应内容
if (tabName === 'articles') loadArticles();
else if (tabName === 'resources') loadResources();
else if (tabName === 'team') loadTeamMembers();
});
});
// 加载文章列表
async function loadArticles() {
try {
const response = await window.DAELAPI.content.getArticles('news', false);
currentArticles = response.articles;
displayArticles(currentArticles);
} catch (error) {
console.error('Failed to load articles:', error);
document.getElementById('articles-list').innerHTML = '<p class="text-red-600">加载失败</p>';
}
}
// 显示文章列表
function displayArticles(articles) {
const container = document.getElementById('articles-list');
if (articles.length === 0) {
container.innerHTML = '<p class="text-gray-500">暂无文章</p>';
return;
}
container.innerHTML = articles.map(article => `
<div class="bg-white border border-gray-200 p-6 rounded">
<div class="flex justify-between items-start">
<div class="flex-1">
<h3 class="text-lg font-bold mb-2">${article.title}</h3>
<p class="text-sm text-gray-500 mb-2">${article.summary || article.content.substring(0, 100)}...</p>
<div class="flex gap-4 text-xs text-gray-400">
<span>分类: ${article.category}</span>
<span>${article.is_protected ? '🔒 受保护' : '公开'}</span>
<span>${article.published ? '已发布' : '草稿'}</span>
<span>${new Date(article.created_at).toLocaleDateString()}</span>
</div>
</div>
<div class="flex gap-2 ml-4">
<button onclick="editArticle(${article.id})" class="text-sm text-blue-600 hover:text-blue-800">编辑</button>
<button onclick="deleteArticle(${article.id})" class="text-sm text-red-600 hover:text-red-800">删除</button>
</div>
</div>
</div>
`).join('');
}
// 显示文章表单
function showArticleForm(articleId = null) {
const modal = document.getElementById('article-modal');
const form = document.getElementById('article-form');
const title = document.getElementById('article-modal-title');
if (articleId) {
const article = currentArticles.find(a => a.id === articleId);
if (article) {
title.textContent = '编辑文章';
document.getElementById('article-id').value = article.id;
document.getElementById('article-title').value = article.title;
document.getElementById('article-summary').value = article.summary || '';
document.getElementById('article-content').value = article.content;
document.getElementById('article-category').value = article.category;
document.getElementById('article-published').value = article.published.toString();
document.getElementById('article-protected').checked = article.is_protected;
}
} else {
title.textContent = '新建文章';
form.reset();
document.getElementById('article-id').value = '';
}
modal.classList.remove('hidden');
}
// 关闭文章表单
function closeArticleForm() {
document.getElementById('article-modal').classList.add('hidden');
}
// 保存文章
async function saveArticle(event) {
event.preventDefault();
const articleId = document.getElementById('article-id').value;
const articleData = {
title: document.getElementById('article-title').value,
summary: document.getElementById('article-summary').value,
content: document.getElementById('article-content').value,
category: document.getElementById('article-category').value,
published: document.getElementById('article-published').value === 'true',
is_protected: document.getElementById('article-protected').checked
};
try {
if (articleId) {
await window.DAELAPI.content.updateArticle(articleId, articleData);
} else {
await window.DAELAPI.content.createArticle(articleData);
}
closeArticleForm();
loadArticles();
} catch (error) {
alert('保存失败: ' + error.message);
}
}
// 编辑文章
function editArticle(articleId) {
showArticleForm(articleId);
}
// 删除文章
async function deleteArticle(articleId) {
if (!confirm('确定要删除这篇文章吗?')) return;
try {
await window.DAELAPI.content.deleteArticle(articleId);
loadArticles();
} catch (error) {
alert('删除失败: ' + error.message);
}
}
// 加载资源列表
async function loadResources() {
try {
const response = await window.DAELAPI.content.getResources();
currentResources = response.resources;
displayResources(currentResources);
} catch (error) {
console.error('Failed to load resources:', error);
document.getElementById('resources-list').innerHTML = '<p class="text-red-600">加载失败</p>';
}
}
// 显示资源列表
function displayResources(resources) {
const container = document.getElementById('resources-list');
if (resources.length === 0) {
container.innerHTML = '<p class="text-gray-500">暂无资源</p>';
return;
}
container.innerHTML = resources.map(resource => `
<div class="bg-white border border-gray-200 p-6 rounded">
<div class="flex justify-between items-start">
<div class="flex-1">
<h3 class="text-lg font-bold mb-2">${resource.name}</h3>
<p class="text-sm text-gray-500 mb-2">${resource.description || ''}</p>
<div class="flex gap-4 text-xs text-gray-400">
<span>分类: ${resource.category}</span>
<span>${resource.is_protected ? '🔒 受保护' : '公开'}</span>
<span>大小: ${(resource.file_size / 1024 / 1024).toFixed(2)} MB</span>
<span>下载: ${resource.download_count} 次</span>
</div>
</div>
<div class="flex gap-2 ml-4">
<button onclick="window.DAELAPI.api.downloadResource(${resource.id})" class="text-sm text-blue-600 hover:text-blue-800">下载</button>
<button onclick="deleteResource(${resource.id})" class="text-sm text-red-600 hover:text-red-800">删除</button>
</div>
</div>
</div>
`).join('');
}
// 显示资源表单
function showResourceForm() {
document.getElementById('resource-modal').classList.remove('hidden');
}
// 关闭资源表单
function closeResourceForm() {
document.getElementById('resource-modal').classList.add('hidden');
document.getElementById('resource-form').reset();
}
// 保存资源
async function saveResource(event) {
event.preventDefault();
const fileInput = document.getElementById('resource-file');
if (!fileInput.files[0]) {
alert('请选择文件');
return;
}
try {
await window.DAELAPI.content.uploadResource(
fileInput.files[0],
document.getElementById('resource-name').value,
document.getElementById('resource-description').value,
document.getElementById('resource-category').value,
document.getElementById('resource-protected').checked
);
closeResourceForm();
loadResources();
} catch (error) {
alert('上传失败: ' + error.message);
}
}
// 删除资源
async function deleteResource(resourceId) {
if (!confirm('确定要删除这个资源吗?')) return;
// 注意:需要添加删除资源的API端点
alert('删除功能需要后端支持');
}
// 加载团队成员
async function loadTeamMembers() {
try {
const response = await window.DAELAPI.content.getTeam();
currentTeamMembers = response.members;
displayTeamMembers(currentTeamMembers);
} catch (error) {
console.error('Failed to load team members:', error);
document.getElementById('team-list').innerHTML = '<p class="text-red-600">加载失败</p>';
}
}
// 显示团队成员
function displayTeamMembers(members) {
const container = document.getElementById('team-list');
if (members.length === 0) {
container.innerHTML = '<p class="text-gray-500">暂无成员</p>';
return;
}
container.innerHTML = members.map(member => `
<div class="bg-white border border-gray-200 p-6 rounded">
<div class="flex justify-between items-start">
<div class="flex-1">
<h3 class="text-lg font-bold mb-2">${member.name}</h3>
<p class="text-sm text-gray-600 mb-2">${member.title || ''}</p>
<p class="text-sm text-gray-500 mb-2">${member.bio || ''}</p>
<div class="flex gap-4 text-xs text-gray-400">
<span>角色: ${member.role}</span>
<span>排序: ${member.order}</span>
</div>
</div>
<div class="flex gap-2 ml-4">
<button onclick="editTeamMember(${member.id})" class="text-sm text-blue-600 hover:text-blue-800">编辑</button>
<button onclick="deleteTeamMember(${member.id})" class="text-sm text-red-600 hover:text-red-800">删除</button>
</div>
</div>
</div>
`).join('');
}
// 显示团队成员表单
function showTeamForm(memberId = null) {
const modal = document.getElementById('team-modal');
const form = document.getElementById('team-form');
if (memberId) {
const member = currentTeamMembers.find(m => m.id === memberId);
if (member) {
document.getElementById('team-id').value = member.id;
document.getElementById('team-name').value = member.name;
document.getElementById('team-title').value = member.title || '';
document.getElementById('team-title-en').value = member.title_en || '';
document.getElementById('team-bio').value = member.bio || '';
document.getElementById('team-bio-en').value = member.bio_en || '';
document.getElementById('team-role').value = member.role;
document.getElementById('team-order').value = member.order;
document.getElementById('team-email').value = member.email || '';
document.getElementById('team-website').value = member.website || '';
}
} else {
form.reset();
document.getElementById('team-id').value = '';
}
modal.classList.remove('hidden');
}
// 关闭团队成员表单
function closeTeamForm() {
document.getElementById('team-modal').classList.add('hidden');
}
// 保存团队成员
async function saveTeamMember(event) {
event.preventDefault();
const memberId = document.getElementById('team-id').value;
const memberData = {
name: document.getElementById('team-name').value,
title: document.getElementById('team-title').value,
title_en: document.getElementById('team-title-en').value,
bio: document.getElementById('team-bio').value,
bio_en: document.getElementById('team-bio-en').value,
role: document.getElementById('team-role').value,
order: parseInt(document.getElementById('team-order').value) || 0,
email: document.getElementById('team-email').value,
website: document.getElementById('team-website').value
};
try {
if (memberId) {
// 更新功能需要后端支持
alert('更新功能需要后端支持');
} else {
await window.DAELAPI.content.createTeamMember(memberData);
}
closeTeamForm();
loadTeamMembers();
} catch (error) {
alert('保存失败: ' + error.message);
}
}
// 编辑团队成员
function editTeamMember(memberId) {
showTeamForm(memberId);
}
// 删除团队成员
async function deleteTeamMember(memberId) {
if (!confirm('确定要删除这个成员吗?')) return;
// 注意:需要添加删除成员的API端点
alert('删除功能需要后端支持');
}
// 初始化
document.addEventListener('DOMContentLoaded', async () => {
if (await checkAdminAccess()) {
loadArticles();
// 退出按钮
document.getElementById('logout-btn').addEventListener('click', async () => {
await logout();
});
}
});
+362
View File
@@ -0,0 +1,362 @@
// API Client for DAEL Lab Backend
const API_BASE_URL = 'http://localhost:5000/api';
// 辅助函数:发送请求
async function apiRequest(endpoint, options = {}) {
const url = `${API_BASE_URL}${endpoint}`;
const config = {
credentials: 'include', // 包含cookies
headers: {
'Content-Type': 'application/json',
...options.headers
},
...options
};
if (config.body && typeof config.body === 'object' && !(config.body instanceof FormData)) {
config.body = JSON.stringify(config.body);
}
try {
const response = await fetch(url, config);
// 检查响应内容类型
const contentType = response.headers.get('content-type');
let data;
if (contentType && contentType.includes('application/json')) {
data = await response.json();
} else {
const text = await response.text();
try {
data = JSON.parse(text);
} catch {
throw new Error(text || 'Invalid response');
}
}
if (!response.ok) {
throw new Error(data.error || `Request failed with status ${response.status}`);
}
return data;
} catch (error) {
console.error('API request failed:', error);
console.error('URL:', url);
console.error('Config:', config);
throw error;
}
}
// 认证API
const authAPI = {
async login(username, password, remember = false) {
return apiRequest('/auth/login', {
method: 'POST',
body: { username, password, remember }
});
},
async logout() {
return apiRequest('/auth/logout', {
method: 'POST'
});
},
async register(username, email, password) {
return apiRequest('/auth/register', {
method: 'POST',
body: { username, email, password }
});
},
async checkAuth() {
return apiRequest('/auth/check', {
method: 'GET'
});
},
async getCurrentUser() {
return apiRequest('/auth/me', {
method: 'GET'
});
}
};
// 内容API
const contentAPI = {
async getArticles(category = 'news', publishedOnly = true) {
return apiRequest(`/cms/articles?category=${category}&published_only=${publishedOnly}`, {
method: 'GET'
});
},
async getArticle(articleId) {
return apiRequest(`/cms/articles/${articleId}`, {
method: 'GET'
});
},
async createArticle(articleData) {
return apiRequest('/cms/articles', {
method: 'POST',
body: articleData
});
},
async updateArticle(articleId, articleData) {
return apiRequest(`/cms/articles/${articleId}`, {
method: 'PUT',
body: articleData
});
},
async deleteArticle(articleId) {
return apiRequest(`/cms/articles/${articleId}`, {
method: 'DELETE'
});
},
async getResources(category = null) {
const url = category ? `/cms/resources?category=${category}` : '/cms/resources';
return apiRequest(url, {
method: 'GET'
});
},
async uploadResource(file, name, description, category, isProtected) {
const formData = new FormData();
formData.append('file', file);
formData.append('name', name);
formData.append('description', description || '');
formData.append('category', category || 'document');
formData.append('is_protected', isProtected ? 'true' : 'false');
return apiRequest('/cms/resources', {
method: 'POST',
headers: {}, // 让浏览器设置Content-Type
body: formData
});
},
async getTeam(role = null) {
const url = role ? `/cms/team?role=${role}` : '/cms/team';
return apiRequest(url, {
method: 'GET'
});
},
async createTeamMember(memberData) {
return apiRequest('/cms/team', {
method: 'POST',
body: memberData
});
}
};
// 其他API
const api = {
async downloadResource(resourceId) {
window.location.href = `${API_BASE_URL}/download/${resourceId}`;
},
async getStats() {
return apiRequest('/stats', {
method: 'GET'
});
},
async getProtectedContent() {
return apiRequest('/protected-content', {
method: 'GET'
});
}
};
// 组会管理API
const meetingsAPI = {
async getMeetings(status = null, type = null) {
let url = '/meetings/meetings';
const params = [];
if (status) params.push(`status=${status}`);
if (type) params.push(`type=${type}`);
if (params.length > 0) url += '?' + params.join('&');
return apiRequest(url, { method: 'GET' });
},
async getMeeting(meetingId) {
return apiRequest(`/meetings/meetings/${meetingId}`, { method: 'GET' });
},
async createMeeting(meetingData) {
return apiRequest('/meetings/meetings', {
method: 'POST',
body: meetingData
});
},
async updateMeeting(meetingId, meetingData) {
return apiRequest(`/meetings/meetings/${meetingId}`, {
method: 'PUT',
body: meetingData
});
},
async deleteMeeting(meetingId) {
return apiRequest(`/meetings/meetings/${meetingId}`, {
method: 'DELETE'
});
},
async registerMeeting(meetingId) {
return apiRequest(`/meetings/meetings/${meetingId}/register`, {
method: 'POST'
});
},
async checkinMeeting(meetingId) {
return apiRequest(`/meetings/meetings/${meetingId}/checkin`, {
method: 'POST'
});
},
async uploadMeetingFile(meetingId, file, name) {
const formData = new FormData();
formData.append('file', file);
formData.append('name', name || file.name);
return apiRequest(`/meetings/meetings/${meetingId}/files`, {
method: 'POST',
headers: {},
body: formData
});
},
async downloadMeetingFile(meetingId, fileId) {
window.location.href = `${API_BASE_URL}/meetings/meetings/${meetingId}/files/${fileId}`;
}
};
// 研究管理API
const researchAPI = {
async getProjects(status = null, ownerId = null) {
let url = '/research/projects';
const params = [];
if (status) params.push(`status=${status}`);
if (ownerId) params.push(`owner_id=${ownerId}`);
if (params.length > 0) url += '?' + params.join('&');
return apiRequest(url, { method: 'GET' });
},
async getProject(projectId) {
return apiRequest(`/research/projects/${projectId}`, { method: 'GET' });
},
async createProject(projectData) {
return apiRequest('/research/projects', {
method: 'POST',
body: projectData
});
},
async updateProject(projectId, projectData) {
return apiRequest(`/research/projects/${projectId}`, {
method: 'PUT',
body: projectData
});
},
async deleteProject(projectId) {
return apiRequest(`/research/projects/${projectId}`, {
method: 'DELETE'
});
},
async getNotes(projectId, noteType = null) {
let url = `/research/projects/${projectId}/notes`;
if (noteType) url += `?type=${noteType}`;
return apiRequest(url, { method: 'GET' });
},
async createNote(projectId, noteData) {
return apiRequest(`/research/projects/${projectId}/notes`, {
method: 'POST',
body: noteData
});
},
async updateNote(noteId, noteData) {
return apiRequest(`/research/notes/${noteId}`, {
method: 'PUT',
body: noteData
});
},
async deleteNote(noteId) {
return apiRequest(`/research/notes/${noteId}`, {
method: 'DELETE'
});
},
async getTasks(projectId, status = null, assigneeId = null) {
let url = `/research/projects/${projectId}/tasks`;
const params = [];
if (status) params.push(`status=${status}`);
if (assigneeId) params.push(`assignee_id=${assigneeId}`);
if (params.length > 0) url += '?' + params.join('&');
return apiRequest(url, { method: 'GET' });
},
async createTask(projectId, taskData) {
return apiRequest(`/research/projects/${projectId}/tasks`, {
method: 'POST',
body: taskData
});
},
async updateTask(taskId, taskData) {
return apiRequest(`/research/tasks/${taskId}`, {
method: 'PUT',
body: taskData
});
},
async deleteTask(taskId) {
return apiRequest(`/research/tasks/${taskId}`, {
method: 'DELETE'
});
},
async uploadResearchFile(projectId, file, name) {
const formData = new FormData();
formData.append('file', file);
formData.append('name', name || file.name);
return apiRequest(`/research/projects/${projectId}/files`, {
method: 'POST',
headers: {},
body: formData
});
},
async downloadResearchFile(projectId, fileId) {
window.location.href = `${API_BASE_URL}/research/projects/${projectId}/files/${fileId}`;
},
async deleteResearchFile(projectId, fileId) {
return apiRequest(`/research/projects/${projectId}/files/${fileId}`, {
method: 'DELETE'
});
}
};
// 导出API
window.DAELAPI = {
auth: authAPI,
content: contentAPI,
api: api,
meetings: meetingsAPI,
research: researchAPI
};
+208
View File
@@ -0,0 +1,208 @@
// User Authentication Functions
let currentUser = null;
// Check if user is logged in
async function isLoggedIn() {
try {
const response = await window.DAELAPI.auth.checkAuth();
if (response.authenticated) {
currentUser = response.user;
return true;
}
return false;
} catch (error) {
return false;
}
}
// Get current user
function getCurrentUser() {
return currentUser;
}
// Login function
async function login(username, password, remember = false) {
try {
const response = await window.DAELAPI.auth.login(username, password, remember);
currentUser = response.user;
return true;
} catch (error) {
console.error('Login failed:', error);
return false;
}
}
// Logout function
async function logout() {
try {
await window.DAELAPI.auth.logout();
currentUser = null;
window.location.href = 'website.html';
} catch (error) {
console.error('Logout failed:', error);
// 即使API失败也清除本地状态
currentUser = null;
window.location.href = 'website.html';
}
}
// Check protected content access
async function checkProtectedContent() {
const loggedIn = await isLoggedIn();
const protectedElements = document.querySelectorAll('.protected-content');
if (loggedIn) {
protectedElements.forEach(el => {
el.classList.remove('protected-content');
});
// 加载受保护内容
try {
const response = await window.DAELAPI.api.getProtectedContent();
displayProtectedContent(response);
} catch (error) {
console.error('Failed to load protected content:', error);
}
} else {
protectedElements.forEach(el => {
el.classList.add('protected-content');
});
}
}
// 显示受保护内容
async function displayProtectedContent(data) {
// 更新受保护的文章
const protectedArticlesContainer = document.getElementById('protected-articles');
if (protectedArticlesContainer && data.articles && data.articles.length > 0) {
protectedArticlesContainer.innerHTML = data.articles.map(article => `
<div class="border-b border-gray-200 pb-6 mb-6">
<h3 class="text-xl font-bold mb-2">${article.title}</h3>
<p class="text-gray-500 text-sm mb-2">${article.summary || article.content.substring(0, 100) + '...'}</p>
<div class="text-xs text-gray-400">${new Date(article.created_at).toLocaleDateString()}</div>
</div>
`).join('');
protectedArticlesContainer.classList.remove('hidden');
}
// 更新受保护的资源
const protectedResourcesContainer = document.getElementById('protected-resources');
if (protectedResourcesContainer && data.resources && data.resources.length > 0) {
protectedResourcesContainer.innerHTML = data.resources.map(resource => `
<div class="border border-gray-200 p-4 mb-4">
<h4 class="font-bold mb-2">${resource.name}</h4>
<p class="text-sm text-gray-500 mb-2">${resource.description || ''}</p>
<p class="text-xs text-gray-400 mb-2">大小: ${(resource.file_size / 1024 / 1024).toFixed(2)} MB</p>
<button onclick="window.DAELAPI.api.downloadResource(${resource.id})"
class="text-sm text-black border-b border-black hover:text-gray-600">
下载 ->
</button>
</div>
`).join('');
protectedResourcesContainer.classList.remove('hidden');
}
// 更新统计信息
try {
const statsResponse = await window.DAELAPI.api.getStats();
const stats = statsResponse.stats;
const statsContainer = document.getElementById('protected-stats');
if (statsContainer) {
const statElements = statsContainer.querySelectorAll('.text-3xl');
if (statElements.length >= 4) {
statElements[0].textContent = stats.articles || '0';
statElements[1].textContent = stats.resources || '0';
statElements[2].textContent = stats.team_members || '0';
statElements[3].textContent = stats.total_downloads || '0';
}
}
} catch (error) {
console.error('Failed to load stats:', error);
}
}
// Initialize auth on page load
document.addEventListener('DOMContentLoaded', async () => {
// 等待API加载
if (typeof window.DAELAPI === 'undefined') {
console.warn('API not loaded, using fallback authentication');
// 使用本地存储作为后备
const localAuth = localStorage.getItem('dael_user_logged_in') === 'true';
updateUI(localAuth);
return;
}
await checkProtectedContent();
await updateAuthUI();
// Logout button handler
const logoutBtn = document.getElementById('logout-btn');
if (logoutBtn) {
logoutBtn.addEventListener('click', async (e) => {
e.preventDefault();
if (confirm('确定要退出登录吗?')) {
await logout();
}
});
}
});
// 更新认证UI
async function updateAuthUI() {
const loggedIn = await isLoggedIn();
const loginBtn = document.getElementById('login-btn');
const logoutBtn = document.getElementById('logout-btn');
const adminBtn = document.getElementById('admin-btn');
const meetingsLink = document.getElementById('meetings-link');
const researchLink = document.getElementById('research-link');
const userInfo = document.getElementById('user-info');
if (loggedIn) {
const user = getCurrentUser();
if (loginBtn) loginBtn.classList.add('hidden');
if (logoutBtn) logoutBtn.classList.remove('hidden');
if (adminBtn && user && user.is_admin) {
adminBtn.classList.remove('hidden');
} else if (adminBtn) {
adminBtn.classList.add('hidden');
}
// 显示组会和研究管理链接(所有登录用户)
if (meetingsLink) meetingsLink.classList.remove('hidden');
if (researchLink) researchLink.classList.remove('hidden');
const mobileMeetingsLink = document.getElementById('mobile-meetings-link');
const mobileResearchLink = document.getElementById('mobile-research-link');
if (mobileMeetingsLink) mobileMeetingsLink.classList.remove('hidden');
if (mobileResearchLink) mobileResearchLink.classList.remove('hidden');
if (userInfo && user) {
userInfo.textContent = user.username;
userInfo.classList.remove('hidden');
}
} else {
if (loginBtn) loginBtn.classList.remove('hidden');
if (logoutBtn) logoutBtn.classList.add('hidden');
if (adminBtn) adminBtn.classList.add('hidden');
if (meetingsLink) meetingsLink.classList.add('hidden');
if (researchLink) researchLink.classList.add('hidden');
const mobileMeetingsLink = document.getElementById('mobile-meetings-link');
const mobileResearchLink = document.getElementById('mobile-research-link');
if (mobileMeetingsLink) mobileMeetingsLink.classList.add('hidden');
if (mobileResearchLink) mobileResearchLink.classList.add('hidden');
if (userInfo) userInfo.classList.add('hidden');
}
}
// 后备UI更新函数
function updateUI(loggedIn) {
const loginBtn = document.getElementById('login-btn');
const logoutBtn = document.getElementById('logout-btn');
if (loggedIn) {
if (loginBtn) loginBtn.classList.add('hidden');
if (logoutBtn) logoutBtn.classList.remove('hidden');
} else {
if (loginBtn) loginBtn.classList.remove('hidden');
if (logoutBtn) logoutBtn.classList.add('hidden');
}
}
+156
View File
@@ -0,0 +1,156 @@
// Common JavaScript Functions for DAEL Lab Website
// Initialize Lucide Icons
function initIcons() {
if (window.lucide) {
lucide.createIcons();
} else {
console.warn("Lucide icons failed to load.");
}
}
// Mobile Menu Toggle
function initMobileMenu() {
const menuBtn = document.getElementById('mobile-menu-btn');
const mobileMenu = document.getElementById('mobile-menu');
if (menuBtn && mobileMenu) {
menuBtn.addEventListener('click', () => {
mobileMenu.classList.toggle('hidden');
});
}
}
// Navbar scroll effect
function initNavbarScroll() {
window.addEventListener('scroll', () => {
const nav = document.getElementById('navbar');
if (nav) {
if (window.scrollY > 50) {
nav.classList.add('shadow-sm');
if (nav.classList.contains('h-20')) {
nav.classList.replace('h-20', 'h-16');
}
} else {
nav.classList.remove('shadow-sm');
if (nav.classList.contains('h-16')) {
nav.classList.replace('h-16', 'h-20');
}
}
}
});
}
// Canvas Animation: Organic Connection Nodes
function initCanvasAnimation() {
const canvas = document.getElementById('heroCanvas');
if (!canvas) return;
const ctx = canvas.getContext('2d');
let width, height;
let particles = [];
function resize() {
width = window.innerWidth;
height = window.innerHeight;
canvas.width = width;
canvas.height = height;
}
class Particle {
constructor() {
this.x = Math.random() * width;
this.y = Math.random() * height;
this.vx = (Math.random() - 0.5) * 0.5;
this.vy = (Math.random() - 0.5) * 0.5;
this.size = Math.random() * 2 + 1;
}
update() {
this.x += this.vx;
this.y += this.vy;
if (this.x < 0 || this.x > width) this.vx *= -1;
if (this.y < 0 || this.y > height) this.vy *= -1;
}
draw() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.size, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(0, 0, 0, 0.1)';
ctx.fill();
}
}
function initParticles() {
particles = [];
const particleCount = Math.min(window.innerWidth / 10, 100);
for (let i = 0; i < particleCount; i++) {
particles.push(new Particle());
}
}
function animate() {
ctx.clearRect(0, 0, width, height);
for (let i = 0; i < particles.length; i++) {
particles[i].update();
particles[i].draw();
for (let j = i + 1; j < particles.length; j++) {
const dx = particles[i].x - particles[j].x;
const dy = particles[i].y - particles[j].y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < 150) {
ctx.beginPath();
ctx.strokeStyle = `rgba(0, 0, 0, ${0.05 - dist/3000})`;
ctx.lineWidth = 0.5;
ctx.moveTo(particles[i].x, particles[i].y);
ctx.lineTo(particles[j].x, particles[j].y);
ctx.stroke();
}
}
}
requestAnimationFrame(animate);
}
window.addEventListener('resize', () => {
resize();
initParticles();
});
resize();
initParticles();
animate();
}
// Visit Counter
function initVisitCounter() {
const counterElement = document.getElementById('visit-counter');
if (!counterElement) return;
let count = parseInt(localStorage.getItem('dael_visit_count') || '0');
count++;
localStorage.setItem('dael_visit_count', count.toString());
counterElement.textContent = count.toLocaleString();
}
// Initialize all common functions
document.addEventListener('DOMContentLoaded', () => {
initIcons();
initMobileMenu();
initNavbarScroll();
initCanvasAnimation();
initVisitCounter();
});
+369
View File
@@ -0,0 +1,369 @@
// Meetings Management JavaScript
let currentMeetings = [];
let currentFilter = 'all';
// 检查登录状态
async function checkLogin() {
try {
const response = await window.DAELAPI.auth.checkAuth();
if (!response.authenticated) {
window.location.href = 'login.html';
return false;
}
return true;
} catch (error) {
window.location.href = 'login.html';
return false;
}
}
// 加载组会列表
async function loadMeetings() {
try {
const response = await window.DAELAPI.meetings.getMeetings();
currentMeetings = response.meetings;
displayMeetings(currentMeetings);
} catch (error) {
console.error('Failed to load meetings:', error);
document.getElementById('meetings-list').innerHTML = '<p class="text-red-600">加载失败</p>';
}
}
// 显示组会列表
function displayMeetings(meetings) {
const container = document.getElementById('meetings-list');
// 根据筛选器过滤
let filteredMeetings = meetings;
if (currentFilter !== 'all') {
filteredMeetings = meetings.filter(m => m.status === currentFilter);
}
if (filteredMeetings.length === 0) {
container.innerHTML = '<p class="text-gray-500 text-center py-12">暂无组会</p>';
return;
}
container.innerHTML = filteredMeetings.map(meeting => {
const date = new Date(meeting.date);
const isOwner = meeting.organizer_id === (window.currentUser?.id);
return `
<div class="bg-white border border-gray-200 p-6 rounded-lg hover:shadow-md transition-shadow">
<div class="flex justify-between items-start">
<div class="flex-1">
<div class="flex items-center gap-3 mb-2">
<h3 class="text-xl font-bold">${meeting.title}</h3>
<span class="text-xs px-2 py-1 rounded ${getStatusColor(meeting.status)}">${getStatusText(meeting.status)}</span>
<span class="text-xs px-2 py-1 rounded bg-gray-100">${getTypeText(meeting.meeting_type)}</span>
</div>
<p class="text-gray-600 text-sm mb-3">${meeting.description || ''}</p>
<div class="flex gap-6 text-sm text-gray-500">
<span>📅 ${date.toLocaleDateString('zh-CN')}</span>
<span>🕐 ${meeting.time || ''}</span>
<span>📍 ${meeting.location || '未指定'}</span>
<span>👤 组织者: ${meeting.organizer_name || ''}</span>
<span>👥 报名: ${meeting.attendee_count || 0} 人</span>
</div>
</div>
<div class="flex gap-2 ml-4">
<button onclick="viewMeeting(${meeting.id})" class="text-sm text-blue-600 hover:text-blue-800 px-3 py-1 border border-blue-600 rounded">查看</button>
${isOwner ? `
<button onclick="editMeeting(${meeting.id})" class="text-sm text-gray-600 hover:text-gray-800 px-3 py-1 border border-gray-600 rounded">编辑</button>
<button onclick="deleteMeeting(${meeting.id})" class="text-sm text-red-600 hover:text-red-800 px-3 py-1 border border-red-600 rounded">删除</button>
` : ''}
</div>
</div>
</div>
`;
}).join('');
}
// 状态颜色
function getStatusColor(status) {
const colors = {
'planned': 'bg-blue-100 text-blue-800',
'ongoing': 'bg-green-100 text-green-800',
'completed': 'bg-gray-100 text-gray-800',
'cancelled': 'bg-red-100 text-red-800'
};
return colors[status] || 'bg-gray-100 text-gray-800';
}
// 状态文本
function getStatusText(status) {
const texts = {
'planned': '计划中',
'ongoing': '进行中',
'completed': '已结束',
'cancelled': '已取消'
};
return texts[status] || status;
}
// 类型文本
function getTypeText(type) {
const texts = {
'weekly': '周会',
'monthly': '月会',
'special': '专题会'
};
return texts[type] || type;
}
// 显示组会表单
function showMeetingForm(meetingId = null) {
const modal = document.getElementById('meeting-modal');
const form = document.getElementById('meeting-form');
const title = document.getElementById('meeting-modal-title');
if (meetingId) {
const meeting = currentMeetings.find(m => m.id === meetingId);
if (meeting) {
title.textContent = '编辑组会';
document.getElementById('meeting-id').value = meeting.id;
document.getElementById('meeting-title').value = meeting.title;
document.getElementById('meeting-description').value = meeting.description || '';
document.getElementById('meeting-date').value = meeting.date;
document.getElementById('meeting-time').value = meeting.time || '';
document.getElementById('meeting-location').value = meeting.location || '';
document.getElementById('meeting-type').value = meeting.meeting_type;
document.getElementById('meeting-status').value = meeting.status;
}
} else {
title.textContent = '创建组会';
form.reset();
document.getElementById('meeting-id').value = '';
// 设置默认日期为今天
const today = new Date().toISOString().split('T')[0];
document.getElementById('meeting-date').value = today;
}
modal.classList.remove('hidden');
}
// 关闭组会表单
function closeMeetingForm() {
document.getElementById('meeting-modal').classList.add('hidden');
}
// 保存组会
async function saveMeeting(event) {
event.preventDefault();
const meetingId = document.getElementById('meeting-id').value;
const meetingData = {
title: document.getElementById('meeting-title').value,
description: document.getElementById('meeting-description').value,
date: document.getElementById('meeting-date').value,
time: document.getElementById('meeting-time').value,
location: document.getElementById('meeting-location').value,
meeting_type: document.getElementById('meeting-type').value,
status: document.getElementById('meeting-status').value
};
try {
if (meetingId) {
await window.DAELAPI.meetings.updateMeeting(meetingId, meetingData);
} else {
await window.DAELAPI.meetings.createMeeting(meetingData);
}
closeMeetingForm();
loadMeetings();
} catch (error) {
alert('保存失败: ' + error.message);
}
}
// 查看组会详情
async function viewMeeting(meetingId) {
try {
const response = await window.DAELAPI.meetings.getMeeting(meetingId);
const meeting = response.meeting;
displayMeetingDetail(meeting);
} catch (error) {
alert('加载失败: ' + error.message);
}
}
// 显示组会详情
function displayMeetingDetail(meeting) {
const modal = document.getElementById('meeting-detail-modal');
const container = document.getElementById('meeting-detail-content');
const date = new Date(meeting.date);
const isOwner = meeting.organizer_id === (window.currentUser?.id);
container.innerHTML = `
<div class="flex justify-between items-start mb-6">
<h3 class="text-2xl font-bold">${meeting.title}</h3>
<button onclick="closeMeetingDetail()" class="text-gray-400 hover:text-gray-600">✕</button>
</div>
<div class="space-y-4 mb-6">
<div>
<span class="text-sm text-gray-500">日期时间:</span>
<span class="text-sm">${date.toLocaleDateString('zh-CN')} ${meeting.time || ''}</span>
</div>
<div>
<span class="text-sm text-gray-500">地点:</span>
<span class="text-sm">${meeting.location || '未指定'}</span>
</div>
<div>
<span class="text-sm text-gray-500">组织者:</span>
<span class="text-sm">${meeting.organizer_name || ''}</span>
</div>
<div>
<span class="text-sm text-gray-500">类型:</span>
<span class="text-sm">${getTypeText(meeting.meeting_type)}</span>
</div>
<div>
<span class="text-sm text-gray-500">状态:</span>
<span class="text-sm ${getStatusColor(meeting.status)} px-2 py-1 rounded">${getStatusText(meeting.status)}</span>
</div>
${meeting.description ? `
<div>
<span class="text-sm text-gray-500">描述:</span>
<p class="text-sm mt-1">${meeting.description}</p>
</div>
` : ''}
</div>
<div class="mb-6">
<h4 class="font-bold mb-3">参会人员 (${meeting.attendees?.length || 0})</h4>
<div class="space-y-2">
${meeting.attendees && meeting.attendees.length > 0 ?
meeting.attendees.map(attendee => `
<div class="flex items-center justify-between p-2 bg-gray-50 rounded">
<span class="text-sm">${attendee.username}</span>
<span class="text-xs text-gray-500">${getStatusText(attendee.status)}</span>
</div>
`).join('') :
'<p class="text-sm text-gray-500">暂无报名</p>'
}
</div>
${!isOwner && meeting.status === 'planned' ? `
<button onclick="registerForMeeting(${meeting.id})" class="mt-3 bg-black text-white px-4 py-2 rounded text-sm hover:bg-gray-800">
报名参会
</button>
` : ''}
</div>
${meeting.meeting_notes ? `
<div class="mb-6">
<h4 class="font-bold mb-3">会议记录</h4>
<div class="p-4 bg-gray-50 rounded text-sm whitespace-pre-wrap">${meeting.meeting_notes}</div>
</div>
` : ''}
${meeting.files && meeting.files.length > 0 ? `
<div class="mb-6">
<h4 class="font-bold mb-3">附件 (${meeting.files.length})</h4>
<div class="space-y-2">
${meeting.files.map(file => `
<div class="flex items-center justify-between p-2 bg-gray-50 rounded">
<span class="text-sm">${file.name}</span>
<button onclick="window.DAELAPI.meetings.downloadMeetingFile(${meeting.id}, ${file.id})"
class="text-sm text-blue-600 hover:text-blue-800">
下载
</button>
</div>
`).join('')}
</div>
</div>
` : ''}
${isOwner ? `
<div class="flex gap-4">
<button onclick="editMeeting(${meeting.id})" class="bg-black text-white px-4 py-2 rounded text-sm hover:bg-gray-800">
编辑
</button>
<button onclick="deleteMeeting(${meeting.id})" class="bg-red-600 text-white px-4 py-2 rounded text-sm hover:bg-red-700">
删除
</button>
</div>
` : ''}
`;
modal.classList.remove('hidden');
}
// 关闭组会详情
function closeMeetingDetail() {
document.getElementById('meeting-detail-modal').classList.add('hidden');
}
// 编辑组会
function editMeeting(meetingId) {
closeMeetingDetail();
showMeetingForm(meetingId);
}
// 删除组会
async function deleteMeeting(meetingId) {
if (!confirm('确定要删除这个组会吗?')) return;
try {
await window.DAELAPI.meetings.deleteMeeting(meetingId);
closeMeetingDetail();
loadMeetings();
} catch (error) {
alert('删除失败: ' + error.message);
}
}
// 报名参会
async function registerForMeeting(meetingId) {
try {
await window.DAELAPI.meetings.registerMeeting(meetingId);
alert('报名成功!');
viewMeeting(meetingId); // 刷新详情
} catch (error) {
alert('报名失败: ' + error.message);
}
}
// 筛选器切换
document.querySelectorAll('.filter-tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.filter-tab').forEach(t => {
t.classList.remove('active', 'border-black', 'text-black');
t.classList.add('text-gray-500');
});
tab.classList.add('active', 'border-black', 'text-black');
tab.classList.remove('text-gray-500');
currentFilter = tab.dataset.filter;
displayMeetings(currentMeetings);
});
});
// 初始化
document.addEventListener('DOMContentLoaded', async () => {
if (await checkLogin()) {
// 获取当前用户信息
try {
const authResponse = await window.DAELAPI.auth.checkAuth();
window.currentUser = authResponse.user;
} catch (error) {
console.error('Failed to get user info:', error);
}
loadMeetings();
// 退出按钮
document.getElementById('logout-btn').addEventListener('click', async () => {
await logout();
});
}
});
+573
View File
@@ -0,0 +1,573 @@
// Research Management JavaScript
let currentProjects = [];
let currentFilter = 'all';
let currentProjectDetail = null;
// 检查登录状态
async function checkLogin() {
try {
const response = await window.DAELAPI.auth.checkAuth();
if (!response.authenticated) {
window.location.href = 'login.html';
return false;
}
return true;
} catch (error) {
window.location.href = 'login.html';
return false;
}
}
// 加载项目列表
async function loadProjects() {
try {
const response = await window.DAELAPI.research.getProjects();
currentProjects = response.projects;
displayProjects(currentProjects);
} catch (error) {
console.error('Failed to load projects:', error);
document.getElementById('projects-list').innerHTML = '<p class="text-red-600">加载失败</p>';
}
}
// 显示项目列表
function displayProjects(projects) {
const container = document.getElementById('projects-list');
// 根据筛选器过滤
let filteredProjects = projects;
if (currentFilter !== 'all') {
filteredProjects = projects.filter(p => p.status === currentFilter);
}
if (filteredProjects.length === 0) {
container.innerHTML = '<p class="text-gray-500 text-center py-12">暂无项目</p>';
return;
}
container.innerHTML = filteredProjects.map(project => {
const startDate = project.start_date ? new Date(project.start_date).toLocaleDateString('zh-CN') : '未设置';
const endDate = project.end_date ? new Date(project.end_date).toLocaleDateString('zh-CN') : '未设置';
const isOwner = project.owner_id === (window.currentUser?.id);
return `
<div class="bg-white border border-gray-200 p-6 rounded-lg hover:shadow-md transition-shadow">
<div class="flex justify-between items-start">
<div class="flex-1">
<div class="flex items-center gap-3 mb-2">
<h3 class="text-xl font-bold">${project.title}</h3>
<span class="text-xs px-2 py-1 rounded ${getStatusColor(project.status)}">${getStatusText(project.status)}</span>
</div>
<p class="text-gray-600 text-sm mb-3">${project.description || ''}</p>
<div class="flex gap-6 text-sm text-gray-500 mb-3">
<span>📅 ${startDate} - ${endDate}</span>
<span>👤 ${project.owner_name || ''}</span>
<span>📊 进度: ${project.progress}%</span>
</div>
${project.tags && project.tags.length > 0 ? `
<div class="flex gap-2 flex-wrap">
${project.tags.map(tag => `<span class="text-xs px-2 py-1 bg-gray-100 rounded">${tag}</span>`).join('')}
</div>
` : ''}
</div>
<div class="flex gap-2 ml-4">
<button onclick="viewProject(${project.id})" class="text-sm text-blue-600 hover:text-blue-800 px-3 py-1 border border-blue-600 rounded">查看</button>
${isOwner ? `
<button onclick="editProject(${project.id})" class="text-sm text-gray-600 hover:text-gray-800 px-3 py-1 border border-gray-600 rounded">编辑</button>
<button onclick="deleteProject(${project.id})" class="text-sm text-red-600 hover:text-red-800 px-3 py-1 border border-red-600 rounded">删除</button>
` : ''}
</div>
</div>
<div class="mt-4 pt-4 border-t border-gray-200">
<div class="w-full bg-gray-200 rounded-full h-2">
<div class="bg-black h-2 rounded-full" style="width: ${project.progress}%"></div>
</div>
</div>
</div>
`;
}).join('');
}
// 状态颜色
function getStatusColor(status) {
const colors = {
'active': 'bg-green-100 text-green-800',
'completed': 'bg-gray-100 text-gray-800',
'paused': 'bg-yellow-100 text-yellow-800',
'cancelled': 'bg-red-100 text-red-800'
};
return colors[status] || 'bg-gray-100 text-gray-800';
}
// 状态文本
function getStatusText(status) {
const texts = {
'active': '进行中',
'completed': '已完成',
'paused': '已暂停',
'cancelled': '已取消'
};
return texts[status] || status;
}
// 显示项目表单
function showProjectForm(projectId = null) {
const modal = document.getElementById('project-modal');
const form = document.getElementById('project-form');
const title = document.getElementById('project-modal-title');
if (projectId) {
const project = currentProjects.find(p => p.id === projectId);
if (project) {
title.textContent = '编辑项目';
document.getElementById('project-id').value = project.id;
document.getElementById('project-title').value = project.title;
document.getElementById('project-description').value = project.description || '';
document.getElementById('project-start-date').value = project.start_date || '';
document.getElementById('project-end-date').value = project.end_date || '';
document.getElementById('project-status').value = project.status;
document.getElementById('project-progress').value = project.progress;
document.getElementById('project-tags').value = project.tags ? project.tags.join(', ') : '';
}
} else {
title.textContent = '新建项目';
form.reset();
document.getElementById('project-id').value = '';
document.getElementById('project-progress').value = 0;
}
modal.classList.remove('hidden');
}
// 关闭项目表单
function closeProjectForm() {
document.getElementById('project-modal').classList.add('hidden');
}
// 保存项目
async function saveProject(event) {
event.preventDefault();
const projectId = document.getElementById('project-id').value;
const tagsInput = document.getElementById('project-tags').value;
const tags = tagsInput ? tagsInput.split(',').map(t => t.trim()).filter(t => t) : [];
const projectData = {
title: document.getElementById('project-title').value,
description: document.getElementById('project-description').value,
start_date: document.getElementById('project-start-date').value || null,
end_date: document.getElementById('project-end-date').value || null,
status: document.getElementById('project-status').value,
progress: parseInt(document.getElementById('project-progress').value) || 0,
tags: tags
};
try {
if (projectId) {
await window.DAELAPI.research.updateProject(projectId, projectData);
} else {
await window.DAELAPI.research.createProject(projectData);
}
closeProjectForm();
loadProjects();
} catch (error) {
alert('保存失败: ' + error.message);
}
}
// 查看项目详情
async function viewProject(projectId) {
try {
const response = await window.DAELAPI.research.getProject(projectId);
currentProjectDetail = response.project;
displayProjectDetail(currentProjectDetail);
} catch (error) {
alert('加载失败: ' + error.message);
}
}
// 显示项目详情
function displayProjectDetail(project) {
const modal = document.getElementById('project-detail-modal');
const container = document.getElementById('project-detail-content');
const isOwner = project.owner_id === (window.currentUser?.id);
container.innerHTML = `
<div class="flex justify-between items-start mb-6">
<div>
<h3 class="text-2xl font-bold mb-2">${project.title}</h3>
<div class="flex items-center gap-3">
<span class="text-xs px-2 py-1 rounded ${getStatusColor(project.status)}">${getStatusText(project.status)}</span>
<span class="text-sm text-gray-500">进度: ${project.progress}%</span>
</div>
</div>
<button onclick="closeProjectDetail()" class="text-gray-400 hover:text-gray-600">✕</button>
</div>
<div class="space-y-4 mb-6">
${project.description ? `
<div>
<h4 class="font-bold mb-2">项目描述</h4>
<p class="text-sm text-gray-600">${project.description}</p>
</div>
` : ''}
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<span class="text-gray-500">开始日期:</span>
<span>${project.start_date ? new Date(project.start_date).toLocaleDateString('zh-CN') : '未设置'}</span>
</div>
<div>
<span class="text-gray-500">结束日期:</span>
<span>${project.end_date ? new Date(project.end_date).toLocaleDateString('zh-CN') : '未设置'}</span>
</div>
<div>
<span class="text-gray-500">负责人:</span>
<span>${project.owner_name || ''}</span>
</div>
</div>
${project.tags && project.tags.length > 0 ? `
<div>
<span class="text-gray-500 text-sm">标签:</span>
<div class="flex gap-2 flex-wrap mt-2">
${project.tags.map(tag => `<span class="text-xs px-2 py-1 bg-gray-100 rounded">${tag}</span>`).join('')}
</div>
</div>
` : ''}
</div>
<!-- Tabs for Notes, Tasks, Files -->
<div class="border-b border-gray-200 mb-4">
<button class="tab-btn active px-4 py-2 text-sm font-medium border-b-2 border-black" data-tab="notes">笔记 (${project.notes?.length || 0})</button>
<button class="tab-btn px-4 py-2 text-sm font-medium text-gray-500" data-tab="tasks">任务 (${project.tasks?.length || 0})</button>
<button class="tab-btn px-4 py-2 text-sm font-medium text-gray-500" data-tab="files">文件 (${project.files?.length || 0})</button>
</div>
<div id="tab-content-notes" class="tab-content">
${isOwner ? `
<button onclick="showNoteForm()" class="mb-4 bg-black text-white px-4 py-2 rounded text-sm hover:bg-gray-800">
+ 新建笔记
</button>
` : ''}
<div class="space-y-3">
${project.notes && project.notes.length > 0 ?
project.notes.map(note => `
<div class="border border-gray-200 p-4 rounded">
<div class="flex justify-between items-start mb-2">
<h5 class="font-bold">${note.title}</h5>
${isOwner && note.author_id === window.currentUser?.id ? `
<div class="flex gap-2">
<button onclick="editNote(${note.id})" class="text-xs text-blue-600">编辑</button>
<button onclick="deleteNote(${note.id})" class="text-xs text-red-600">删除</button>
</div>
` : ''}
</div>
<p class="text-sm text-gray-600 mb-2">${note.content.substring(0, 200)}${note.content.length > 200 ? '...' : ''}</p>
<div class="text-xs text-gray-400">
${note.author_name} · ${new Date(note.created_at).toLocaleDateString('zh-CN')}
</div>
</div>
`).join('') :
'<p class="text-sm text-gray-500">暂无笔记</p>'
}
</div>
</div>
<div id="tab-content-tasks" class="tab-content hidden">
${isOwner ? `
<button onclick="showTaskForm()" class="mb-4 bg-black text-white px-4 py-2 rounded text-sm hover:bg-gray-800">
+ 新建任务
</button>
` : ''}
<div class="space-y-3">
${project.tasks && project.tasks.length > 0 ?
project.tasks.map(task => `
<div class="border border-gray-200 p-4 rounded">
<div class="flex justify-between items-start mb-2">
<div class="flex items-center gap-2">
<input type="checkbox" ${task.status === 'completed' ? 'checked' : ''}
onchange="toggleTask(${task.id}, this.checked)" class="mr-2">
<h5 class="font-bold">${task.title}</h5>
<span class="text-xs px-2 py-1 rounded ${getPriorityColor(task.priority)}">${getPriorityText(task.priority)}</span>
</div>
${isOwner ? `
<div class="flex gap-2">
<button onclick="editTask(${task.id})" class="text-xs text-blue-600">编辑</button>
<button onclick="deleteTask(${task.id})" class="text-xs text-red-600">删除</button>
</div>
` : ''}
</div>
${task.description ? `<p class="text-sm text-gray-600 mb-2">${task.description}</p>` : ''}
<div class="text-xs text-gray-400">
${task.assignee_name || '未分配'} ·
${task.due_date ? new Date(task.due_date).toLocaleDateString('zh-CN') : '无截止日期'} ·
${getStatusText(task.status)}
</div>
</div>
`).join('') :
'<p class="text-sm text-gray-500">暂无任务</p>'
}
</div>
</div>
<div id="tab-content-files" class="tab-content hidden">
${isOwner ? `
<button onclick="showFileUpload()" class="mb-4 bg-black text-white px-4 py-2 rounded text-sm hover:bg-gray-800">
+ 上传文件
</button>
` : ''}
<div class="space-y-3">
${project.files && project.files.length > 0 ?
project.files.map(file => `
<div class="border border-gray-200 p-4 rounded flex justify-between items-center">
<div>
<span class="text-sm font-medium">${file.name}</span>
<span class="text-xs text-gray-400 ml-2">${(file.file_size / 1024 / 1024).toFixed(2)} MB</span>
</div>
<div class="flex gap-2">
<button onclick="window.DAELAPI.research.downloadResearchFile(${project.id}, ${file.id})"
class="text-xs text-blue-600">下载</button>
${isOwner && file.uploader_id === window.currentUser?.id ? `
<button onclick="deleteFile(${project.id}, ${file.id})" class="text-xs text-red-600">删除</button>
` : ''}
</div>
</div>
`).join('') :
'<p class="text-sm text-gray-500">暂无文件</p>'
}
</div>
</div>
${isOwner ? `
<div class="mt-6 flex gap-4">
<button onclick="editProject(${project.id})" class="bg-black text-white px-4 py-2 rounded text-sm hover:bg-gray-800">
编辑项目
</button>
<button onclick="deleteProject(${project.id})" class="bg-red-600 text-white px-4 py-2 rounded text-sm hover:bg-red-700">
删除项目
</button>
</div>
` : ''}
`;
// 初始化标签页切换
initDetailTabs();
modal.classList.remove('hidden');
}
// 初始化详情页标签
function initDetailTabs() {
document.querySelectorAll('#project-detail-modal .tab-btn').forEach(btn => {
btn.addEventListener('click', () => {
const tabName = btn.dataset.tab;
// 更新按钮样式
document.querySelectorAll('#project-detail-modal .tab-btn').forEach(b => {
b.classList.remove('active', 'border-black', 'text-black');
b.classList.add('text-gray-500');
});
btn.classList.add('active', 'border-black', 'text-black');
btn.classList.remove('text-gray-500');
// 更新内容
document.querySelectorAll('#project-detail-modal .tab-content').forEach(content => {
content.classList.add('hidden');
});
document.getElementById(`tab-content-${tabName}`).classList.remove('hidden');
});
});
}
// 关闭项目详情
function closeProjectDetail() {
document.getElementById('project-detail-modal').classList.add('hidden');
currentProjectDetail = null;
}
// 编辑项目
function editProject(projectId) {
closeProjectDetail();
showProjectForm(projectId);
}
// 删除项目
async function deleteProject(projectId) {
if (!confirm('确定要删除这个项目吗?所有相关的笔记、任务和文件也将被删除。')) return;
try {
await window.DAELAPI.research.deleteProject(projectId);
closeProjectDetail();
loadProjects();
} catch (error) {
alert('删除失败: ' + error.message);
}
}
// 笔记管理
function showNoteForm(noteId = null) {
// 简化版:使用prompt创建笔记
const title = prompt('笔记标题:');
if (!title) return;
const content = prompt('笔记内容:');
if (!content) return;
createNote(title, content);
}
async function createNote(title, content) {
try {
await window.DAELAPI.research.createNote(currentProjectDetail.id, {
title: title,
content: content,
note_type: 'note'
});
viewProject(currentProjectDetail.id); // 刷新详情
} catch (error) {
alert('创建失败: ' + error.message);
}
}
async function deleteNote(noteId) {
if (!confirm('确定要删除这条笔记吗?')) return;
try {
await window.DAELAPI.research.deleteNote(noteId);
viewProject(currentProjectDetail.id); // 刷新详情
} catch (error) {
alert('删除失败: ' + error.message);
}
}
// 任务管理
function showTaskForm() {
const title = prompt('任务标题:');
if (!title) return;
const description = prompt('任务描述:');
const dueDate = prompt('截止日期 (YYYY-MM-DD)');
createTask(title, description, dueDate);
}
async function createTask(title, description, dueDate) {
try {
await window.DAELAPI.research.createTask(currentProjectDetail.id, {
title: title,
description: description || '',
due_date: dueDate || null,
status: 'todo',
priority: 'medium'
});
viewProject(currentProjectDetail.id); // 刷新详情
} catch (error) {
alert('创建失败: ' + error.message);
}
}
async function toggleTask(taskId, completed) {
try {
await window.DAELAPI.research.updateTask(taskId, {
status: completed ? 'completed' : 'todo'
});
viewProject(currentProjectDetail.id); // 刷新详情
} catch (error) {
alert('更新失败: ' + error.message);
}
}
async function deleteTask(taskId) {
if (!confirm('确定要删除这个任务吗?')) return;
try {
await window.DAELAPI.research.deleteTask(taskId);
viewProject(currentProjectDetail.id); // 刷新详情
} catch (error) {
alert('删除失败: ' + error.message);
}
}
// 文件管理
function showFileUpload() {
const input = document.createElement('input');
input.type = 'file';
input.multiple = false;
input.onchange = async (e) => {
const file = e.target.files[0];
if (!file) return;
try {
await window.DAELAPI.research.uploadResearchFile(currentProjectDetail.id, file, file.name);
viewProject(currentProjectDetail.id); // 刷新详情
} catch (error) {
alert('上传失败: ' + error.message);
}
};
input.click();
}
async function deleteFile(projectId, fileId) {
if (!confirm('确定要删除这个文件吗?')) return;
try {
await window.DAELAPI.research.deleteResearchFile(projectId, fileId);
viewProject(projectId); // 刷新详情
} catch (error) {
alert('删除失败: ' + error.message);
}
}
// 优先级颜色
function getPriorityColor(priority) {
const colors = {
'high': 'bg-red-100 text-red-800',
'medium': 'bg-yellow-100 text-yellow-800',
'low': 'bg-blue-100 text-blue-800'
};
return colors[priority] || 'bg-gray-100 text-gray-800';
}
// 优先级文本
function getPriorityText(priority) {
const texts = {
'high': '高',
'medium': '中',
'low': '低'
};
return texts[priority] || priority;
}
// 筛选器切换
document.querySelectorAll('.filter-tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.filter-tab').forEach(t => {
t.classList.remove('active', 'border-black', 'text-black');
t.classList.add('text-gray-500');
});
tab.classList.add('active', 'border-black', 'text-black');
tab.classList.remove('text-gray-500');
currentFilter = tab.dataset.filter;
displayProjects(currentProjects);
});
});
// 初始化
document.addEventListener('DOMContentLoaded', async () => {
if (await checkLogin()) {
// 获取当前用户信息
try {
const authResponse = await window.DAELAPI.auth.checkAuth();
window.currentUser = authResponse.user;
} catch (error) {
console.error('Failed to get user info:', error);
}
loadProjects();
// 退出按钮
document.getElementById('logout-btn').addEventListener('click', async () => {
await logout();
});
}
});
+144
View File
@@ -0,0 +1,144 @@
// Search Functionality
// Search data structure (in production, this would come from a server)
const searchData = {
pages: [
{ title: '关于实验室', url: 'website.html#about', content: 'DAEL Design AI Ecology Lab 跨学科研究平台' },
{ title: '研究方向', url: 'website.html#research', content: '生成式生态学 生物启发式智能 数字可持续性' },
{ title: '研究团队', url: 'website.html#team', content: 'Dr. Lin Chen Sarah Wu James Zhao Yu Zhang' },
{ title: '新闻资讯', url: 'news.html', content: '最新资讯 最新文章 热点文章' },
{ title: '成果展示', url: 'publications.html', content: '专著教材 文章目录 软件开发 学位论文' },
{ title: '资源下载', url: 'resources.html', content: '软件下载 资料下载 开源代码' },
{ title: '学术资料', url: 'academic.html', content: '软件使用 教程 文档' }
],
news: [],
publications: [],
resources: []
};
// Simple search function
function performSearch(query) {
if (!query || query.trim().length === 0) {
return [];
}
const lowerQuery = query.toLowerCase().trim();
const results = [];
// Search in pages
searchData.pages.forEach(page => {
const titleMatch = page.title.toLowerCase().includes(lowerQuery);
const contentMatch = page.content.toLowerCase().includes(lowerQuery);
if (titleMatch || contentMatch) {
results.push({
type: 'page',
title: page.title,
url: page.url,
snippet: page.content.substring(0, 100) + '...'
});
}
});
return results;
}
// Initialize search
function initSearch() {
const searchInput = document.getElementById('search-input');
const searchResults = document.getElementById('search-results');
const searchForm = document.getElementById('search-form');
if (!searchInput || !searchResults) return;
// Real-time search
let searchTimeout;
searchInput.addEventListener('input', (e) => {
clearTimeout(searchTimeout);
const query = e.target.value.trim();
if (query.length === 0) {
searchResults.classList.remove('active');
searchResults.innerHTML = '';
return;
}
searchTimeout = setTimeout(() => {
const results = performSearch(query);
displaySearchResults(results, searchResults);
}, 300);
});
// Handle form submission
if (searchForm) {
searchForm.addEventListener('submit', (e) => {
e.preventDefault();
const query = searchInput.value.trim();
if (query) {
window.location.href = `search.html?q=${encodeURIComponent(query)}`;
}
});
}
// Close search results when clicking outside
document.addEventListener('click', (e) => {
if (!searchInput.contains(e.target) && !searchResults.contains(e.target)) {
searchResults.classList.remove('active');
}
});
}
// Display search results
function displaySearchResults(results, container) {
if (results.length === 0) {
container.innerHTML = '<div class="search-result-item text-gray-500 text-center py-4">未找到相关结果</div>';
container.classList.add('active');
return;
}
container.innerHTML = results.map(result => `
<a href="${result.url}" class="search-result-item block">
<div class="font-semibold text-sm mb-1">${result.title}</div>
<div class="text-xs text-gray-500">${result.snippet}</div>
</a>
`).join('');
container.classList.add('active');
}
// Get search query from URL
function getSearchQuery() {
const params = new URLSearchParams(window.location.search);
return params.get('q') || '';
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', () => {
initSearch();
// If on search page, perform search
if (window.location.pathname.includes('search.html')) {
const query = getSearchQuery();
const searchInput = document.getElementById('search-input');
const resultsContainer = document.getElementById('search-results-container');
if (query && searchInput) {
searchInput.value = query;
}
if (query && resultsContainer) {
const results = performSearch(query);
displaySearchResults(results, resultsContainer);
}
}
});