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:
@@ -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();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user