123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>项目管理系统</title>
- <script src="https://cdn.tailwindcss.com"></script>
- <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
- <script>
- tailwind.config = {
- theme: {
- extend: {
- colors: {
- primary: '#165DFF',
- secondary: '#36CFC9',
- success: '#52C41A',
- warning: '#FAAD14',
- danger: '#FF4D4F',
- neutral: '#8C8C8C',
- 'neutral-light': '#F5F5F5',
- 'neutral-dark': '#434343',
- },
- fontFamily: {
- sans: ['Inter', 'system-ui', 'sans-serif'],
- },
- }
- }
- }
- </script>
- <style type="text/tailwindcss">
- @layer utilities {
- .content-auto {
- content-visibility: auto;
- }
- .table-shadow {
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
- }
- .transition-bg {
- transition: background-color 0.2s ease;
- }
- .transition-transform {
- transition: transform 0.2s ease;
- }
- }
- </style>
- </head>
- <body class="bg-gray-50 font-sans text-neutral-dark">
- <!-- 顶部导航栏 -->
- <header class="bg-white shadow-sm fixed top-0 left-0 right-0 z-10">
- <div class="container mx-auto px-4 py-3 flex items-center justify-between">
- <div class="flex items-center space-x-2">
- <i class="fa fa-tasks text-primary text-2xl"></i>
- <h1 class="text-xl font-bold text-primary">项目管理系统</h1>
- </div>
- <div class="flex items-center space-x-4">
- <button id="theme-toggle" class="p-2 rounded-full hover:bg-gray-100 transition-bg">
- <i class="fa fa-moon-o text-neutral"></i>
- </button>
- <div class="flex items-center space-x-2">
- <img src="https://picsum.photos/id/1005/40/40" alt="用户头像" class="w-8 h-8 rounded-full object-cover border border-gray-200">
- <span class="hidden md:inline">管理员</span>
- </div>
- </div>
- </div>
- </header>
- <!-- 主内容区 -->
- <div class="container mx-auto pt-16 px-4 pb-20 flex flex-col md:flex-row gap-6">
- <!-- 侧边栏 -->
- <aside class="w-full md:w-64 bg-white rounded-lg shadow-sm p-4 mt-6 md:mt-0">
- <nav class="space-y-1">
- <p class="text-xs font-semibold text-neutral uppercase tracking-wider mb-2 px-3">主菜单</p>
- <a href="#projects" class="nav-link block px-3 py-2 rounded-md text-primary bg-primary/10 font-medium">
- <i class="fa fa-briefcase w-5 inline-block"></i>
- <span class="ml-2">项目管理</span>
- </a>
- <a href="#profiles" class="nav-link block px-3 py-2 rounded-md text-neutral-dark hover:bg-gray-100 transition-bg">
- <i class="fa fa-users w-5 inline-block"></i>
- <span class="ml-2">成员档案</span>
- </a>
- <a href="#project-teams" class="nav-link block px-3 py-2 rounded-md text-neutral-dark hover:bg-gray-100 transition-bg">
- <i class="fa fa-sitemap w-5 inline-block"></i>
- <span class="ml-2">项目成员</span>
- </a>
- </nav>
- </aside>
- <!-- 主要内容 -->
- <main class="flex-1 mt-6">
- <!-- 项目管理部分 -->
- <section id="projects" class="content-section">
- <div class="bg-white rounded-lg shadow-sm p-6 mb-6">
- <div class="flex flex-col sm:flex-row sm:items-center justify-between mb-6 gap-4">
- <h2 class="text-xl font-bold">项目管理</h2>
- <button id="add-project-btn" class="bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-md flex items-center transition-bg">
- <i class="fa fa-plus mr-2"></i>
- <span>添加项目</span>
- </button>
- </div>
-
- <div class="overflow-x-auto">
- <table class="min-w-full table-shadow rounded-lg overflow-hidden">
- <thead class="bg-neutral-light">
- <tr>
- <th class="py-3 px-4 text-left text-sm font-semibold text-neutral-dark">ID</th>
- <th class="py-3 px-4 text-left text-sm font-semibold text-neutral-dark">项目标题</th>
- <th class="py-3 px-4 text-left text-sm font-semibold text-neutral-dark">描述</th>
- <th class="py-3 px-4 text-left text-sm font-semibold text-neutral-dark">项目天数</th>
- <th class="py-3 px-4 text-left text-sm font-semibold text-neutral-dark">操作</th>
- </tr>
- </thead>
- <tbody id="projects-table-body">
- <!-- 项目数据将通过JavaScript动态加载 -->
- <tr>
- <td colspan="5" class="py-10 text-center text-neutral">
- <div class="flex flex-col items-center">
- <i class="fa fa-spinner fa-spin text-primary text-2xl mb-2"></i>
- <span>加载项目数据中...</span>
- </div>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- </div>
- </section>
- <!-- 成员档案部分 -->
- <section id="profiles" class="content-section hidden">
- <div class="bg-white rounded-lg shadow-sm p-6 mb-6">
- <div class="flex flex-col sm:flex-row sm:items-center justify-between mb-6 gap-4">
- <h2 class="text-xl font-bold">成员档案</h2>
- <button id="add-profile-btn" class="bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-md flex items-center transition-bg">
- <i class="fa fa-plus mr-2"></i>
- <span>添加成员</span>
- </button>
- </div>
-
- <div class="overflow-x-auto">
- <table class="min-w-full table-shadow rounded-lg overflow-hidden">
- <thead class="bg-neutral-light">
- <tr>
- <th class="py-3 px-4 text-left text-sm font-semibold text-neutral-dark">ID</th>
- <th class="py-3 px-4 text-left text-sm font-semibold text-neutral-dark">姓名</th>
- <th class="py-3 px-4 text-left text-sm font-semibold text-neutral-dark">性别</th>
- <th class="py-3 px-4 text-left text-sm font-semibold text-neutral-dark">擅长技能</th>
- <th class="py-3 px-4 text-left text-sm font-semibold text-neutral-dark">操作</th>
- </tr>
- </thead>
- <tbody id="profiles-table-body">
- <!-- 成员数据将通过JavaScript动态加载 -->
- <tr>
- <td colspan="5" class="py-10 text-center text-neutral">
- <div class="flex flex-col items-center">
- <i class="fa fa-spinner fa-spin text-primary text-2xl mb-2"></i>
- <span>加载成员数据中...</span>
- </div>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- </div>
- </section>
- <!-- 项目成员部分 -->
- <section id="project-teams" class="content-section hidden">
- <div class="bg-white rounded-lg shadow-sm p-6 mb-6">
- <div class="flex flex-col sm:flex-row sm:items-center justify-between mb-6 gap-4">
- <h2 class="text-xl font-bold">项目成员管理</h2>
- <div class="flex flex-col sm:flex-row gap-3">
- <select id="project-select" class="border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
- <option value="">选择项目</option>
- <!-- 项目选项将通过JavaScript动态加载 -->
- </select>
- <button id="add-team-member-btn" class="bg-primary hover:bg-primary/90 text-white px-4 py-2 rounded-md flex items-center transition-bg" disabled>
- <i class="fa fa-plus mr-2"></i>
- <span>添加成员</span>
- </button>
- </div>
- </div>
-
- <div id="project-team-details" class="hidden">
- <h3 class="text-lg font-semibold mb-4" id="current-project-name"></h3>
- <div class="overflow-x-auto">
- <table class="min-w-full table-shadow rounded-lg overflow-hidden">
- <thead class="bg-neutral-light">
- <tr>
- <th class="py-3 px-4 text-left text-sm font-semibold text-neutral-dark">成员ID</th>
- <th class="py-3 px-4 text-left text-sm font-semibold text-neutral-dark">姓名</th>
- <th class="py-3 px-4 text-left text-sm font-semibold text-neutral-dark">性别</th>
- <th class="py-3 px-4 text-left text-sm font-semibold text-neutral-dark">擅长技能</th>
- <th class="py-3 px-4 text-left text-sm font-semibold text-neutral-dark">操作</th>
- </tr>
- </thead>
- <tbody id="team-members-table-body">
- <!-- 项目成员数据将通过JavaScript动态加载 -->
- </tbody>
- </table>
- </div>
- </div>
-
- <div id="no-project-selected" class="py-10 text-center text-neutral">
- <p>请先选择一个项目查看和管理成员</p>
- </div>
- </div>
- </section>
- </main>
- </div>
- <!-- 添加/编辑项目模态框 -->
- <div id="project-modal" class="fixed inset-0 bg-black/50 z-50 hidden items-center justify-center p-4">
- <div class="bg-white rounded-lg shadow-lg w-full max-w-md">
- <div class="p-5 border-b">
- <h3 id="project-modal-title" class="text-lg font-bold">添加项目</h3>
- </div>
- <div class="p-5">
- <form id="project-form">
- <input type="hidden" id="project-id">
- <div class="mb-4">
- <label for="project-title" class="block text-sm font-medium text-neutral-dark mb-1">项目标题</label>
- <input type="text" id="project-title" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary" required>
- </div>
- <div class="mb-4">
- <label for="project-desc" class="block text-sm font-medium text-neutral-dark mb-1">项目描述</label>
- <textarea id="project-desc" rows="3" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary"></textarea>
- </div>
- <div class="mb-4">
- <label for="project-avatar" class="block text-sm font-medium text-neutral-dark mb-1">项目图片地址</label>
- <input type="url" id="project-avatar" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary">
- </div>
- <div class="mb-4">
- <label for="project-duration" class="block text-sm font-medium text-neutral-dark mb-1">项目天数</label>
- <input type="number" id="project-duration" min="1" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary" required>
- </div>
- </form>
- </div>
- <div class="p-5 border-t flex justify-end gap-3">
- <button id="cancel-project-btn" class="px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-50 transition-bg">取消</button>
- <button id="save-project-btn" class="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-bg">保存</button>
- </div>
- </div>
- </div>
- <!-- 添加/编辑成员模态框 -->
- <div id="profile-modal" class="fixed inset-0 bg-black/50 z-50 hidden items-center justify-center p-4">
- <div class="bg-white rounded-lg shadow-lg w-full max-w-md">
- <div class="p-5 border-b">
- <h3 id="profile-modal-title" class="text-lg font-bold">添加成员</h3>
- </div>
- <div class="p-5">
- <form id="profile-form">
- <input type="hidden" id="profile-id">
- <div class="mb-4">
- <label for="profile-name" class="block text-sm font-medium text-neutral-dark mb-1">姓名</label>
- <input type="text" id="profile-name" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary" required>
- </div>
- <div class="mb-4">
- <label for="profile-gender" class="block text-sm font-medium text-neutral-dark mb-1">性别</label>
- <select id="profile-gender" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary" required>
- <option value="">请选择</option>
- <option value="男">男</option>
- <option value="女">女</option>
- </select>
- </div>
- <div class="mb-4">
- <label for="profile-desc" class="block text-sm font-medium text-neutral-dark mb-1">擅长技能</label>
- <textarea id="profile-desc" rows="3" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary"></textarea>
- </div>
- </form>
- </div>
- <div class="p-5 border-t flex justify-end gap-3">
- <button id="cancel-profile-btn" class="px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-50 transition-bg">取消</button>
- <button id="save-profile-btn" class="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-bg">保存</button>
- </div>
- </div>
- </div>
- <!-- 添加项目成员模态框 -->
- <div id="add-team-member-modal" class="fixed inset-0 bg-black/50 z-50 hidden items-center justify-center p-4">
- <div class="bg-white rounded-lg shadow-lg w-full max-w-md">
- <div class="p-5 border-b">
- <h3 class="text-lg font-bold">添加项目成员</h3>
- </div>
- <div class="p-5">
- <form id="team-member-form">
- <input type="hidden" id="team-project-id">
- <div class="mb-4">
- <label for="team-profile-id" class="block text-sm font-medium text-neutral-dark mb-1">选择成员</label>
- <select id="team-profile-id" class="w-full border border-gray-300 rounded-md px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary/50 focus:border-primary" required>
- <option value="">请选择成员</option>
- <!-- 成员选项将通过JavaScript动态加载 -->
- </select>
- </div>
- </form>
- </div>
- <div class="p-5 border-t flex justify-end gap-3">
- <button id="cancel-team-member-btn" class="px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-50 transition-bg">取消</button>
- <button id="save-team-member-btn" class="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-bg">保存</button>
- </div>
- </div>
- </div>
- <!-- 确认删除模态框 -->
- <div id="confirm-delete-modal" class="fixed inset-0 bg-black/50 z-50 hidden items-center justify-center p-4">
- <div class="bg-white rounded-lg shadow-lg w-full max-w-md">
- <div class="p-5 border-b">
- <h3 class="text-lg font-bold text-danger">确认删除</h3>
- </div>
- <div class="p-5">
- <p id="delete-confirmation-message">您确定要删除这项记录吗?此操作不可撤销。</p>
- </div>
- <div class="p-5 border-t flex justify-end gap-3">
- <button id="cancel-delete-btn" class="px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-50 transition-bg">取消</button>
- <button id="confirm-delete-btn" class="px-4 py-2 bg-danger text-white rounded-md hover:bg-danger/90 transition-bg">确认删除</button>
- </div>
- </div>
- </div>
- <!-- 通知提示 -->
- <div id="notification" class="fixed bottom-4 right-4 px-6 py-3 rounded-md shadow-lg transform translate-y-20 opacity-0 transition-all duration-300 z-50 flex items-center">
- <i id="notification-icon" class="mr-2 text-xl"></i>
- <span id="notification-message"></span>
- </div>
- <script>
- // 面向对象的API交互类
- class FmodeQuery {
- className;
- where;
- include;
- constructor(className) {
- this.className = className;
- this.where = {};
- }
- equalTo(key, value) {
- this.where[key] = value;
- }
- lessThanAndEqualTo(key, value) {
- this.where[key] = { $lte: value };
- }
- greaterThan(key, value) {
- this.where[key] = { $gt: value };
- }
- async get() {
- try {
- let whereStr = '';
- if (Object.keys(this.where).length > 0) {
- whereStr = 'where='+encodeURIComponent(JSON.stringify(this.where));
- }
- const response = await fetch(`http://dev.fmode.cn:1337/parse/classes/${this.className}?${whereStr}`, {
- "headers": {
- "accept": "*/*",
- "x-parse-application-id": "dev"
- },
- "method": "GET",
- "mode": "cors",
- "credentials": "omit"
- });
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
- const data = await response.json();
- return data.results.map(item => {
- const fmodeObject = new FmodeObject(this.className);
- fmodeObject.set(item);
- return fmodeObject;
- });
- } catch (error) {
- console.error('Error in get:', error);
- showNotification('获取数据失败', 'error');
- return [];
- }
- }
- async find() {
- return this.get();
- }
- }
- class FmodeObject {
- className;
- id;
- data;
- constructor(className) {
- this.className = className;
- this.data = {};
- this.id = null;
- }
- set(data) {
- this.data = {
- ...this.data,
- ...data
- };
- if (data?.objectId) {
- this.id = data.objectId;
- this.data.objectId = data.objectId;
- }
- }
- async save() {
- try {
- const data = { ...this.data };
- delete data.objectId;
- delete data.updatedAt;
- delete data.createdAt;
- delete data.ACL;
- const updateId = this.id ? `/${this.id}` : '';
- const method = this.id ? "PUT" : "POST";
-
- const response = await fetch(`http://dev.fmode.cn:1337/parse/classes/${this.className}${updateId}`, {
- "headers": {
- "accept": "*/*",
- "content-type": "application/json",
- "x-parse-application-id": "dev"
- },
- "body": JSON.stringify(data),
- "method": method,
- "mode": "cors",
- "credentials": "omit"
- });
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
- const result = await response.json();
- if (result.objectId) {
- this.id = result.objectId;
- this.data.objectId = result.objectId;
- }
-
- return this;
- } catch (error) {
- console.error('Error in save:', error);
- showNotification('保存失败', 'error');
- throw error;
- }
- }
- async destroy() {
- try {
- if (!this.id) {
- throw new Error('No object ID to delete');
- }
-
- const response = await fetch(`http://dev.fmode.cn:1337/parse/classes/${this.className}/${this.id}`, {
- "headers": {
- "accept": "*/*",
- "x-parse-application-id": "dev"
- },
- "method": "DELETE",
- "mode": "cors",
- "credentials": "omit"
- });
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
-
- return true;
- } catch (error) {
- console.error('Error in destroy:', error);
- showNotification('删除失败', 'error');
- return false;
- }
- }
- }
- // 全局变量
- let currentDeleteItem = null;
- let currentDeleteType = null;
- // DOM 元素加载完成后执行
- document.addEventListener('DOMContentLoaded', () => {
- // 加载所有数据
- loadAllData();
- // 导航链接切换
- document.querySelectorAll('.nav-link').forEach(link => {
- link.addEventListener('click', (e) => {
- e.preventDefault();
-
- // 更新导航样式
- document.querySelectorAll('.nav-link').forEach(item => {
- item.classList.remove('text-primary', 'bg-primary/10');
- item.classList.add('text-neutral-dark', 'hover:bg-gray-100');
- });
- link.classList.remove('text-neutral-dark', 'hover:bg-gray-100');
- link.classList.add('text-primary', 'bg-primary/10');
-
- // 显示对应的内容区域
- const targetId = link.getAttribute('href').substring(1);
- document.querySelectorAll('.content-section').forEach(section => {
- section.classList.add('hidden');
- });
- document.getElementById(targetId).classList.remove('hidden');
- });
- });
- // 项目相关事件
- document.getElementById('add-project-btn').addEventListener('click', () => {
- openProjectModal();
- });
- document.getElementById('cancel-project-btn').addEventListener('click', () => {
- closeProjectModal();
- });
- document.getElementById('save-project-btn').addEventListener('click', saveProject);
- // 成员相关事件
- document.getElementById('add-profile-btn').addEventListener('click', () => {
- openProfileModal();
- });
- document.getElementById('cancel-profile-btn').addEventListener('click', () => {
- closeProfileModal();
- });
- document.getElementById('save-profile-btn').addEventListener('click', saveProfile);
- // 项目成员相关事件
- document.getElementById('project-select').addEventListener('change', (e) => {
- const projectId = e.target.value;
- if (projectId) {
- document.getElementById('project-team-details').classList.remove('hidden');
- document.getElementById('no-project-selected').classList.add('hidden');
- document.getElementById('add-team-member-btn').removeAttribute('disabled');
-
- // 加载项目成员
- loadProjectMembers(projectId);
-
- // 更新当前项目名称
- const option = e.target.options[e.target.selectedIndex];
- document.getElementById('current-project-name').textContent = `项目: ${option.textContent}`;
- } else {
- document.getElementById('project-team-details').classList.add('hidden');
- document.getElementById('no-project-selected').classList.remove('hidden');
- document.getElementById('add-team-member-btn').setAttribute('disabled', 'true');
- }
- });
- document.getElementById('add-team-member-btn').addEventListener('click', () => {
- const projectId = document.getElementById('project-select').value;
- if (projectId) {
- openAddTeamMemberModal(projectId);
- }
- });
- document.getElementById('cancel-team-member-btn').addEventListener('click', () => {
- closeAddTeamMemberModal();
- });
- document.getElementById('save-team-member-btn').addEventListener('click', saveTeamMember);
- // 确认删除相关事件
- document.getElementById('cancel-delete-btn').addEventListener('click', closeDeleteModal);
- document.getElementById('confirm-delete-btn').addEventListener('click', confirmDelete);
- });
- // 加载所有数据
- async function loadAllData() {
- await Promise.all([
- loadProjects(),
- loadProfiles()
- ]);
- }
- // 加载项目列表
- async function loadProjects() {
- try {
- const query = new FmodeQuery("Project");
- const projects = await query.find();
-
- const tableBody = document.getElementById('projects-table-body');
- tableBody.innerHTML = '';
-
- if (projects.length === 0) {
- tableBody.innerHTML = `
- <tr>
- <td colspan="5" class="py-10 text-center text-neutral">
- <p>暂无项目数据</p>
- </td>
- </tr>
- `;
- return;
- }
-
- projects.forEach(project => {
- const row = document.createElement('tr');
- row.className = 'border-b hover:bg-gray-50 transition-bg';
- row.innerHTML = `
- <td class="py-3 px-4">${project.id}</td>
- <td class="py-3 px-4">${project.data.title || '-'}</td>
- <td class="py-3 px-4 max-w-xs truncate">${project.data.desc || '-'}</td>
- <td class="py-3 px-4">${project.data.duration || '-'}</td>
- <td class="py-3 px-4">
- <div class="flex space-x-2">
- <button class="text-primary hover:text-primary/80 p-1" onclick="editProject('${project.id}')">
- <i class="fa fa-edit"></i>
- </button>
- <button class="text-danger hover:text-danger/80 p-1" onclick="confirmDeleteProject('${project.id}')">
- <i class="fa fa-trash"></i>
- </button>
- </div>
- </td>
- `;
- tableBody.appendChild(row);
- });
-
- // 更新项目选择下拉框
- const projectSelect = document.getElementById('project-select');
- // 保存当前选中值
- const currentValue = projectSelect.value;
- // 清除现有选项(保留第一个)
- while (projectSelect.options.length > 1) {
- projectSelect.remove(1);
- }
-
- projects.forEach(project => {
- const option = document.createElement('option');
- option.value = project.id;
- option.textContent = project.data.title || `项目 ${project.id}`;
- projectSelect.appendChild(option);
- });
-
- // 恢复选中值
- if (currentValue) {
- projectSelect.value = currentValue;
- }
- } catch (error) {
- console.error('Error loading projects:', error);
- showNotification('加载项目失败', 'error');
- }
- }
- // 加载成员列表
- async function loadProfiles() {
- try {
- const query = new FmodeQuery("Profile");
- const profiles = await query.find();
-
- const tableBody = document.getElementById('profiles-table-body');
- tableBody.innerHTML = '';
-
- if (profiles.length === 0) {
- tableBody.innerHTML = `
- <tr>
- <td colspan="5" class="py-10 text-center text-neutral">
- <p>暂无成员数据</p>
- </td>
- </tr>
- `;
- return;
- }
-
- profiles.forEach(profile => {
- const row = document.createElement('tr');
- row.className = 'border-b hover:bg-gray-50 transition-bg';
- row.innerHTML = `
- <td class="py-3 px-4">${profile.id}</td>
- <td class="py-3 px-4">${profile.data.name || '-'}</td>
- <td class="py-3 px-4">${profile.data.gender || '-'}</td>
- <td class="py-3 px-4">${profile.data.desc || '-'}</td>
- <td class="py-3 px-4">
- <div class="flex space-x-2">
- <button class="text-primary hover:text-primary/80 p-1" onclick="editProfile('${profile.id}')">
- <i class="fa fa-edit"></i>
- </button>
- <button class="text-danger hover:text-danger/80 p-1" onclick="confirmDeleteProfile('${profile.id}')">
- <i class="fa fa-trash"></i>
- </button>
- </div>
- </td>
- `;
- tableBody.appendChild(row);
- });
-
- // 更新成员选择下拉框(用于添加项目成员)
- const profileSelect = document.getElementById('team-profile-id');
- // 清除现有选项(保留第一个)
- while (profileSelect.options.length > 1) {
- profileSelect.remove(1);
- }
-
- profiles.forEach(profile => {
- const option = document.createElement('option');
- option.value = profile.id;
- option.textContent = profile.data.name || `成员 ${profile.id}`;
- profileSelect.appendChild(option);
- });
- } catch (error) {
- console.error('Error loading profiles:', error);
- showNotification('加载成员失败', 'error');
- }
- }
- // 加载项目成员
- async function loadProjectMembers(projectId) {
- try {
- const query = new FmodeQuery("ProjectTeam");
- query.equalTo("project", { __type: "Pointer", className: "Project", objectId: projectId });
- const teamMembers = await query.find();
-
- // 获取所有成员ID
- const profileIds = teamMembers.map(member => member.data.profile?.objectId).filter(Boolean);
-
- // 获取成员详细信息
- let profiles = [];
- if (profileIds.length > 0) {
- const profileQuery = new FmodeQuery("Profile");
- profileQuery.where = { objectId: { $in: profileIds } };
- profiles = await profileQuery.find();
- }
-
- const tableBody = document.getElementById('team-members-table-body');
- tableBody.innerHTML = '';
-
- if (profiles.length === 0) {
- tableBody.innerHTML = `
- <tr>
- <td colspan="5" class="py-10 text-center text-neutral">
- <p>该项目暂无成员,请添加成员</p>
- </td>
- </tr>
- `;
- return;
- }
-
- // 为每个成员找到对应的team记录ID,以便删除
- profiles.forEach(profile => {
- const teamMember = teamMembers.find(m => m.data.profile?.objectId === profile.id);
- const row = document.createElement('tr');
- row.className = 'border-b hover:bg-gray-50 transition-bg';
- row.innerHTML = `
- <td class="py-3 px-4">${profile.id}</td>
- <td class="py-3 px-4">${profile.data.name || '-'}</td>
- <td class="py-3 px-4">${profile.data.gender || '-'}</td>
- <td class="py-3 px-4">${profile.data.desc || '-'}</td>
- <td class="py-3 px-4">
- <button class="text-danger hover:text-danger/80 p-1" onclick="confirmDeleteTeamMember('${teamMember.id}', '${projectId}')">
- <i class="fa fa-trash"></i>
- </button>
- </td>
- `;
- tableBody.appendChild(row);
- });
- } catch (error) {
- console.error('Error loading project members:', error);
- showNotification('加载项目成员失败', 'error');
- }
- }
- // 打开项目模态框
- function openProjectModal(projectId = null) {
- document.getElementById('project-modal-title').textContent = projectId ? '编辑项目' : '添加项目';
- document.getElementById('project-form').reset();
- document.getElementById('project-id').value = '';
-
- if (projectId) {
- // 加载项目数据
- const query = new FmodeQuery("Project");
- query.equalTo("objectId", projectId);
- query.find().then(projects => {
- if (projects.length > 0) {
- const project = projects[0];
- document.getElementById('project-id').value = project.id;
- document.getElementById('project-title').value = project.data.title || '';
- document.getElementById('project-desc').value = project.data.desc || '';
- document.getElementById('project-avatar').value = project.data.avatar || '';
- document.getElementById('project-duration').value = project.data.duration || '';
- }
- });
- }
-
- document.getElementById('project-modal').classList.remove('hidden');
- document.getElementById('project-modal').classList.add('flex');
- }
- // 关闭项目模态框
- function closeProjectModal() {
- document.getElementById('project-modal').classList.remove('flex');
- document.getElementById('project-modal').classList.add('hidden');
- }
- // 保存项目
- async function saveProject() {
- const projectId = document.getElementById('project-id').value;
- const title = document.getElementById('project-title').value;
- const desc = document.getElementById('project-desc').value;
- const avatar = document.getElementById('project-avatar').value;
- const duration = document.getElementById('project-duration').value;
-
- if (!title || !duration) {
- showNotification('请填写必填字段', 'warning');
- return;
- }
-
- try {
- const project = new FmodeObject("Project");
- if (projectId) {
- project.id = projectId;
- }
-
- project.set({
- title,
- desc,
- avatar,
- duration: parseInt(duration)
- });
-
- await project.save();
- closeProjectModal();
- await loadProjects();
- showNotification(projectId ? '项目更新成功' : '项目创建成功', 'success');
- } catch (error) {
- console.error('Error saving project:', error);
- showNotification('保存项目失败', 'error');
- }
- }
- // 打开成员模态框
- function openProfileModal(profileId = null) {
- document.getElementById('profile-modal-title').textContent = profileId ? '编辑成员' : '添加成员';
- document.getElementById('profile-form').reset();
- document.getElementById('profile-id').value = '';
-
- if (profileId) {
- // 加载成员数据
- const query = new FmodeQuery("Profile");
- query.equalTo("objectId", profileId);
- query.find().then(profiles => {
- if (profiles.length > 0) {
- const profile = profiles[0];
- document.getElementById('profile-id').value = profile.id;
- document.getElementById('profile-name').value = profile.data.name || '';
- document.getElementById('profile-gender').value = profile.data.gender || '';
- document.getElementById('profile-desc').value = profile.data.desc || '';
- }
- });
- }
-
- document.getElementById('profile-modal').classList.remove('hidden');
- document.getElementById('profile-modal').classList.add('flex');
- }
- // 关闭成员模态框
- function closeProfileModal() {
- document.getElementById('profile-modal').classList.remove('flex');
- document.getElementById('profile-modal').classList.add('hidden');
- }
- // 保存成员
- async function saveProfile() {
- const profileId = document.getElementById('profile-id').value;
- const name = document.getElementById('profile-name').value;
- const gender = document.getElementById('profile-gender').value;
- const desc = document.getElementById('profile-desc').value;
-
- if (!name || !gender) {
- showNotification('请填写必填字段', 'warning');
- return;
- }
-
- try {
- const profile = new FmodeObject("Profile");
- if (profileId) {
- profile.id = profileId;
- }
-
- profile.set({
- name,
- gender,
- desc
- });
-
- await profile.save();
- closeProfileModal();
- await loadProfiles();
- showNotification(profileId ? '成员更新成功' : '成员创建成功', 'success');
- } catch (error) {
- console.error('Error saving profile:', error);
- showNotification('保存成员失败', 'error');
- }
- }
- // 打开添加项目成员模态框
- function openAddTeamMemberModal(projectId) {
- document.getElementById('team-project-id').value = projectId;
- document.getElementById('team-profile-id').value = '';
- document.getElementById('add-team-member-modal').classList.remove('hidden');
- document.getElementById('add-team-member-modal').classList.add('flex');
- }
- // 关闭添加项目成员模态框
- function closeAddTeamMemberModal() {
- document.getElementById('add-team-member-modal').classList.remove('flex');
- document.getElementById('add-team-member-modal').classList.add('hidden');
- }
- // 保存项目成员
- async function saveTeamMember() {
- const projectId = document.getElementById('team-project-id').value;
- const profileId = document.getElementById('team-profile-id').value;
-
- if (!projectId || !profileId) {
- showNotification('请选择项目和成员', 'warning');
- return;
- }
-
- try {
- // 检查是否已存在该关联
- const query = new FmodeQuery("ProjectTeam");
- query.equalTo("project", { __type: "Pointer", className: "Project", objectId: projectId });
- query.equalTo("profile", { __type: "Pointer", className: "Profile", objectId: profileId });
- const existing = await query.find();
-
- if (existing.length > 0) {
- showNotification('该成员已在项目中', 'warning');
- return;
- }
-
- const teamMember = new FmodeObject("ProjectTeam");
- teamMember.set({
- project: { __type: "Pointer", className: "Project", objectId: projectId },
- profile: { __type: "Pointer", className: "Profile", objectId: profileId }
- });
-
- await teamMember.save();
- closeAddTeamMemberModal();
- await loadProjectMembers(projectId);
- showNotification('成员已添加到项目', 'success');
- } catch (error) {
- console.error('Error saving team member:', error);
- showNotification('添加项目成员失败', 'error');
- }
- }
- // 打开删除确认模态框
- function openDeleteModal(itemId, type, extraData = null) {
- currentDeleteItem = { id: itemId, extraData };
- currentDeleteType = type;
-
- let message = '';
- switch (type) {
- case 'project':
- message = '您确定要删除这个项目吗?相关的项目成员关联也将被删除。';
- break;
- case 'profile':
- message = '您确定要删除这个成员吗?该成员在所有项目中的关联也将被删除。';
- break;
- case 'teamMember':
- message = '您确定要将这个成员从项目中移除吗?';
- break;
- default:
- message = '您确定要删除这项记录吗?此操作不可撤销。';
- }
-
- document.getElementById('delete-confirmation-message').textContent = message;
- document.getElementById('confirm-delete-modal').classList.remove('hidden');
- document.getElementById('confirm-delete-modal').classList.add('flex');
- }
- // 关闭删除确认模态框
- function closeDeleteModal() {
- currentDeleteItem = null;
- currentDeleteType = null;
- document.getElementById('confirm-delete-modal').classList.remove('flex');
- document.getElementById('confirm-delete-modal').classList.add('hidden');
- }
- // 确认删除
- async function confirmDelete() {
- if (!currentDeleteItem || !currentDeleteType) return;
-
- try {
- switch (currentDeleteType) {
- case 'project':
- // 先删除相关的项目成员关联
- const teamQuery = new FmodeQuery("ProjectTeam");
- teamQuery.equalTo("project", { __type: "Pointer", className: "Project", objectId: currentDeleteItem.id });
- const teamMembers = await teamQuery.find();
- for (const member of teamMembers) {
- await member.destroy();
- }
-
- // 再删除项目
- const project = new FmodeObject("Project");
- project.id = currentDeleteItem.id;
- await project.destroy();
-
- await loadProjects();
- showNotification('项目已删除', 'success');
- break;
-
- case 'profile':
- // 先删除相关的项目成员关联
- const profileTeamQuery = new FmodeQuery("ProjectTeam");
- profileTeamQuery.equalTo("profile", { __type: "Pointer", className: "Profile", objectId: currentDeleteItem.id });
- const profileTeamMembers = await profileTeamQuery.find();
- for (const member of profileTeamMembers) {
- await member.destroy();
- }
-
- // 再删除成员
- const profile = new FmodeObject("Profile");
- profile.id = currentDeleteItem.id;
- await profile.destroy();
-
- await loadProfiles();
- // 如果有项目被选中,刷新项目成员列表
- const selectedProject = document.getElementById('project-select').value;
- if (selectedProject) {
- await loadProjectMembers(selectedProject);
- }
- showNotification('成员已删除', 'success');
- break;
-
- case 'teamMember':
- const teamMember = new FmodeObject("ProjectTeam");
- teamMember.id = currentDeleteItem.id;
- await teamMember.destroy();
-
- await loadProjectMembers(currentDeleteItem.extraData);
- showNotification('成员已从项目中移除', 'success');
- break;
- }
-
- closeDeleteModal();
- } catch (error) {
- console.error('Error deleting item:', error);
- showNotification('删除失败', 'error');
- }
- }
- // 显示通知
- function showNotification(message, type = 'info') {
- const notification = document.getElementById('notification');
- const icon = document.getElementById('notification-icon');
- const messageEl = document.getElementById('notification-message');
-
- // 设置通知类型样式
- notification.className = 'fixed bottom-4 right-4 px-6 py-3 rounded-md shadow-lg transform translate-y-20 opacity-0 transition-all duration-300 z-50 flex items-center';
-
- switch (type) {
- case 'success':
- notification.classList.add('bg-success', 'text-white');
- icon.className = 'fa fa-check-circle mr-2 text-xl';
- break;
- case 'error':
- notification.classList.add('bg-danger', 'text-white');
- icon.className = 'fa fa-times-circle mr-2 text-xl';
- break;
- case 'warning':
- notification.classList.add('bg-warning', 'text-white');
- icon.className = 'fa fa-exclamation-triangle mr-2 text-xl';
- break;
- default:
- notification.classList.add('bg-primary', 'text-white');
- icon.className = 'fa fa-info-circle mr-2 text-xl';
- }
-
- messageEl.textContent = message;
-
- // 显示通知
- setTimeout(() => {
- notification.classList.remove('translate-y-20', 'opacity-0');
- }, 10);
-
- // 3秒后隐藏通知
- setTimeout(() => {
- notification.classList.add('translate-y-20', 'opacity-0');
- }, 3000);
- }
- // 全局函数,用于HTML中的事件处理
- window.editProject = function(projectId) {
- openProjectModal(projectId);
- };
- window.confirmDeleteProject = function(projectId) {
- openDeleteModal(projectId, 'project');
- };
- window.editProfile = function(profileId) {
- openProfileModal(profileId);
- };
- window.confirmDeleteProfile = function(profileId) {
- openDeleteModal(profileId, 'profile');
- };
- window.confirmDeleteTeamMember = function(teamMemberId, projectId) {
- openDeleteModal(teamMemberId, 'teamMember', projectId);
- };
- </script>
- </body>
- </html>
|