project-object-class.html 52 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110
  1. <!DOCTYPE html>
  2. <html lang="zh-CN">
  3. <head>
  4. <meta charset="UTF-8">
  5. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  6. <title>项目管理系统</title>
  7. <script src="https://cdn.tailwindcss.com"></script>
  8. <link href="https://cdn.jsdelivr.net/npm/font-awesome@4.7.0/css/font-awesome.min.css" rel="stylesheet">
  9. <script>
  10. tailwind.config = {
  11. theme: {
  12. extend: {
  13. colors: {
  14. primary: '#165DFF',
  15. secondary: '#36CFC9',
  16. success: '#52C41A',
  17. warning: '#FAAD14',
  18. danger: '#FF4D4F',
  19. neutral: '#8C8C8C',
  20. 'neutral-light': '#F5F5F5',
  21. 'neutral-dark': '#434343',
  22. },
  23. fontFamily: {
  24. sans: ['Inter', 'system-ui', 'sans-serif'],
  25. },
  26. }
  27. }
  28. }
  29. </script>
  30. <style type="text/tailwindcss">
  31. @layer utilities {
  32. .content-auto {
  33. content-visibility: auto;
  34. }
  35. .table-shadow {
  36. box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
  37. }
  38. .transition-bg {
  39. transition: background-color 0.2s ease;
  40. }
  41. .transition-transform {
  42. transition: transform 0.2s ease;
  43. }
  44. }
  45. </style>
  46. </head>
  47. <body class="bg-gray-50 font-sans text-neutral-dark">
  48. <!-- 顶部导航栏 -->
  49. <header class="bg-white shadow-sm fixed top-0 left-0 right-0 z-10">
  50. <div class="container mx-auto px-4 py-3 flex items-center justify-between">
  51. <div class="flex items-center space-x-2">
  52. <i class="fa fa-tasks text-primary text-2xl"></i>
  53. <h1 class="text-xl font-bold text-primary">项目管理系统</h1>
  54. </div>
  55. <div class="flex items-center space-x-4">
  56. <button id="theme-toggle" class="p-2 rounded-full hover:bg-gray-100 transition-bg">
  57. <i class="fa fa-moon-o text-neutral"></i>
  58. </button>
  59. <div class="flex items-center space-x-2">
  60. <img src="https://picsum.photos/id/1005/40/40" alt="用户头像" class="w-8 h-8 rounded-full object-cover border border-gray-200">
  61. <span class="hidden md:inline">管理员</span>
  62. </div>
  63. </div>
  64. </div>
  65. </header>
  66. <!-- 主内容区 -->
  67. <div class="container mx-auto pt-16 px-4 pb-20 flex flex-col md:flex-row gap-6">
  68. <!-- 侧边栏 -->
  69. <aside class="w-full md:w-64 bg-white rounded-lg shadow-sm p-4 mt-6 md:mt-0">
  70. <nav class="space-y-1">
  71. <p class="text-xs font-semibold text-neutral uppercase tracking-wider mb-2 px-3">主菜单</p>
  72. <a href="#projects" class="nav-link block px-3 py-2 rounded-md text-primary bg-primary/10 font-medium">
  73. <i class="fa fa-briefcase w-5 inline-block"></i>
  74. <span class="ml-2">项目管理</span>
  75. </a>
  76. <a href="#profiles" class="nav-link block px-3 py-2 rounded-md text-neutral-dark hover:bg-gray-100 transition-bg">
  77. <i class="fa fa-users w-5 inline-block"></i>
  78. <span class="ml-2">成员档案</span>
  79. </a>
  80. <a href="#project-teams" class="nav-link block px-3 py-2 rounded-md text-neutral-dark hover:bg-gray-100 transition-bg">
  81. <i class="fa fa-sitemap w-5 inline-block"></i>
  82. <span class="ml-2">项目成员</span>
  83. </a>
  84. </nav>
  85. </aside>
  86. <!-- 主要内容 -->
  87. <main class="flex-1 mt-6">
  88. <!-- 项目管理部分 -->
  89. <section id="projects" class="content-section">
  90. <div class="bg-white rounded-lg shadow-sm p-6 mb-6">
  91. <div class="flex flex-col sm:flex-row sm:items-center justify-between mb-6 gap-4">
  92. <h2 class="text-xl font-bold">项目管理</h2>
  93. <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">
  94. <i class="fa fa-plus mr-2"></i>
  95. <span>添加项目</span>
  96. </button>
  97. </div>
  98. <div class="overflow-x-auto">
  99. <table class="min-w-full table-shadow rounded-lg overflow-hidden">
  100. <thead class="bg-neutral-light">
  101. <tr>
  102. <th class="py-3 px-4 text-left text-sm font-semibold text-neutral-dark">ID</th>
  103. <th class="py-3 px-4 text-left text-sm font-semibold text-neutral-dark">项目标题</th>
  104. <th class="py-3 px-4 text-left text-sm font-semibold text-neutral-dark">描述</th>
  105. <th class="py-3 px-4 text-left text-sm font-semibold text-neutral-dark">项目天数</th>
  106. <th class="py-3 px-4 text-left text-sm font-semibold text-neutral-dark">操作</th>
  107. </tr>
  108. </thead>
  109. <tbody id="projects-table-body">
  110. <!-- 项目数据将通过JavaScript动态加载 -->
  111. <tr>
  112. <td colspan="5" class="py-10 text-center text-neutral">
  113. <div class="flex flex-col items-center">
  114. <i class="fa fa-spinner fa-spin text-primary text-2xl mb-2"></i>
  115. <span>加载项目数据中...</span>
  116. </div>
  117. </td>
  118. </tr>
  119. </tbody>
  120. </table>
  121. </div>
  122. </div>
  123. </section>
  124. <!-- 成员档案部分 -->
  125. <section id="profiles" class="content-section hidden">
  126. <div class="bg-white rounded-lg shadow-sm p-6 mb-6">
  127. <div class="flex flex-col sm:flex-row sm:items-center justify-between mb-6 gap-4">
  128. <h2 class="text-xl font-bold">成员档案</h2>
  129. <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">
  130. <i class="fa fa-plus mr-2"></i>
  131. <span>添加成员</span>
  132. </button>
  133. </div>
  134. <div class="overflow-x-auto">
  135. <table class="min-w-full table-shadow rounded-lg overflow-hidden">
  136. <thead class="bg-neutral-light">
  137. <tr>
  138. <th class="py-3 px-4 text-left text-sm font-semibold text-neutral-dark">ID</th>
  139. <th class="py-3 px-4 text-left text-sm font-semibold text-neutral-dark">姓名</th>
  140. <th class="py-3 px-4 text-left text-sm font-semibold text-neutral-dark">性别</th>
  141. <th class="py-3 px-4 text-left text-sm font-semibold text-neutral-dark">擅长技能</th>
  142. <th class="py-3 px-4 text-left text-sm font-semibold text-neutral-dark">操作</th>
  143. </tr>
  144. </thead>
  145. <tbody id="profiles-table-body">
  146. <!-- 成员数据将通过JavaScript动态加载 -->
  147. <tr>
  148. <td colspan="5" class="py-10 text-center text-neutral">
  149. <div class="flex flex-col items-center">
  150. <i class="fa fa-spinner fa-spin text-primary text-2xl mb-2"></i>
  151. <span>加载成员数据中...</span>
  152. </div>
  153. </td>
  154. </tr>
  155. </tbody>
  156. </table>
  157. </div>
  158. </div>
  159. </section>
  160. <!-- 项目成员部分 -->
  161. <section id="project-teams" class="content-section hidden">
  162. <div class="bg-white rounded-lg shadow-sm p-6 mb-6">
  163. <div class="flex flex-col sm:flex-row sm:items-center justify-between mb-6 gap-4">
  164. <h2 class="text-xl font-bold">项目成员管理</h2>
  165. <div class="flex flex-col sm:flex-row gap-3">
  166. <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">
  167. <option value="">选择项目</option>
  168. <!-- 项目选项将通过JavaScript动态加载 -->
  169. </select>
  170. <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>
  171. <i class="fa fa-plus mr-2"></i>
  172. <span>添加成员</span>
  173. </button>
  174. </div>
  175. </div>
  176. <div id="project-team-details" class="hidden">
  177. <h3 class="text-lg font-semibold mb-4" id="current-project-name"></h3>
  178. <div class="overflow-x-auto">
  179. <table class="min-w-full table-shadow rounded-lg overflow-hidden">
  180. <thead class="bg-neutral-light">
  181. <tr>
  182. <th class="py-3 px-4 text-left text-sm font-semibold text-neutral-dark">成员ID</th>
  183. <th class="py-3 px-4 text-left text-sm font-semibold text-neutral-dark">姓名</th>
  184. <th class="py-3 px-4 text-left text-sm font-semibold text-neutral-dark">性别</th>
  185. <th class="py-3 px-4 text-left text-sm font-semibold text-neutral-dark">擅长技能</th>
  186. <th class="py-3 px-4 text-left text-sm font-semibold text-neutral-dark">操作</th>
  187. </tr>
  188. </thead>
  189. <tbody id="team-members-table-body">
  190. <!-- 项目成员数据将通过JavaScript动态加载 -->
  191. </tbody>
  192. </table>
  193. </div>
  194. </div>
  195. <div id="no-project-selected" class="py-10 text-center text-neutral">
  196. <p>请先选择一个项目查看和管理成员</p>
  197. </div>
  198. </div>
  199. </section>
  200. </main>
  201. </div>
  202. <!-- 添加/编辑项目模态框 -->
  203. <div id="project-modal" class="fixed inset-0 bg-black/50 z-50 hidden items-center justify-center p-4">
  204. <div class="bg-white rounded-lg shadow-lg w-full max-w-md">
  205. <div class="p-5 border-b">
  206. <h3 id="project-modal-title" class="text-lg font-bold">添加项目</h3>
  207. </div>
  208. <div class="p-5">
  209. <form id="project-form">
  210. <input type="hidden" id="project-id">
  211. <div class="mb-4">
  212. <label for="project-title" class="block text-sm font-medium text-neutral-dark mb-1">项目标题</label>
  213. <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>
  214. </div>
  215. <div class="mb-4">
  216. <label for="project-desc" class="block text-sm font-medium text-neutral-dark mb-1">项目描述</label>
  217. <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>
  218. </div>
  219. <div class="mb-4">
  220. <label for="project-avatar" class="block text-sm font-medium text-neutral-dark mb-1">项目图片地址</label>
  221. <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">
  222. </div>
  223. <div class="mb-4">
  224. <label for="project-duration" class="block text-sm font-medium text-neutral-dark mb-1">项目天数</label>
  225. <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>
  226. </div>
  227. </form>
  228. </div>
  229. <div class="p-5 border-t flex justify-end gap-3">
  230. <button id="cancel-project-btn" class="px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-50 transition-bg">取消</button>
  231. <button id="save-project-btn" class="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-bg">保存</button>
  232. </div>
  233. </div>
  234. </div>
  235. <!-- 添加/编辑成员模态框 -->
  236. <div id="profile-modal" class="fixed inset-0 bg-black/50 z-50 hidden items-center justify-center p-4">
  237. <div class="bg-white rounded-lg shadow-lg w-full max-w-md">
  238. <div class="p-5 border-b">
  239. <h3 id="profile-modal-title" class="text-lg font-bold">添加成员</h3>
  240. </div>
  241. <div class="p-5">
  242. <form id="profile-form">
  243. <input type="hidden" id="profile-id">
  244. <div class="mb-4">
  245. <label for="profile-name" class="block text-sm font-medium text-neutral-dark mb-1">姓名</label>
  246. <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>
  247. </div>
  248. <div class="mb-4">
  249. <label for="profile-gender" class="block text-sm font-medium text-neutral-dark mb-1">性别</label>
  250. <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>
  251. <option value="">请选择</option>
  252. <option value="男">男</option>
  253. <option value="女">女</option>
  254. </select>
  255. </div>
  256. <div class="mb-4">
  257. <label for="profile-desc" class="block text-sm font-medium text-neutral-dark mb-1">擅长技能</label>
  258. <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>
  259. </div>
  260. </form>
  261. </div>
  262. <div class="p-5 border-t flex justify-end gap-3">
  263. <button id="cancel-profile-btn" class="px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-50 transition-bg">取消</button>
  264. <button id="save-profile-btn" class="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-bg">保存</button>
  265. </div>
  266. </div>
  267. </div>
  268. <!-- 添加项目成员模态框 -->
  269. <div id="add-team-member-modal" class="fixed inset-0 bg-black/50 z-50 hidden items-center justify-center p-4">
  270. <div class="bg-white rounded-lg shadow-lg w-full max-w-md">
  271. <div class="p-5 border-b">
  272. <h3 class="text-lg font-bold">添加项目成员</h3>
  273. </div>
  274. <div class="p-5">
  275. <form id="team-member-form">
  276. <input type="hidden" id="team-project-id">
  277. <div class="mb-4">
  278. <label for="team-profile-id" class="block text-sm font-medium text-neutral-dark mb-1">选择成员</label>
  279. <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>
  280. <option value="">请选择成员</option>
  281. <!-- 成员选项将通过JavaScript动态加载 -->
  282. </select>
  283. </div>
  284. </form>
  285. </div>
  286. <div class="p-5 border-t flex justify-end gap-3">
  287. <button id="cancel-team-member-btn" class="px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-50 transition-bg">取消</button>
  288. <button id="save-team-member-btn" class="px-4 py-2 bg-primary text-white rounded-md hover:bg-primary/90 transition-bg">保存</button>
  289. </div>
  290. </div>
  291. </div>
  292. <!-- 确认删除模态框 -->
  293. <div id="confirm-delete-modal" class="fixed inset-0 bg-black/50 z-50 hidden items-center justify-center p-4">
  294. <div class="bg-white rounded-lg shadow-lg w-full max-w-md">
  295. <div class="p-5 border-b">
  296. <h3 class="text-lg font-bold text-danger">确认删除</h3>
  297. </div>
  298. <div class="p-5">
  299. <p id="delete-confirmation-message">您确定要删除这项记录吗?此操作不可撤销。</p>
  300. </div>
  301. <div class="p-5 border-t flex justify-end gap-3">
  302. <button id="cancel-delete-btn" class="px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-50 transition-bg">取消</button>
  303. <button id="confirm-delete-btn" class="px-4 py-2 bg-danger text-white rounded-md hover:bg-danger/90 transition-bg">确认删除</button>
  304. </div>
  305. </div>
  306. </div>
  307. <!-- 通知提示 -->
  308. <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">
  309. <i id="notification-icon" class="mr-2 text-xl"></i>
  310. <span id="notification-message"></span>
  311. </div>
  312. <script>
  313. // 面向对象的API交互类
  314. class FmodeQuery {
  315. className;
  316. where;
  317. include;
  318. constructor(className) {
  319. this.className = className;
  320. this.where = {};
  321. }
  322. equalTo(key, value) {
  323. this.where[key] = value;
  324. }
  325. lessThanAndEqualTo(key, value) {
  326. this.where[key] = { $lte: value };
  327. }
  328. greaterThan(key, value) {
  329. this.where[key] = { $gt: value };
  330. }
  331. async get() {
  332. try {
  333. let whereStr = '';
  334. if (Object.keys(this.where).length > 0) {
  335. whereStr = 'where='+encodeURIComponent(JSON.stringify(this.where));
  336. }
  337. const response = await fetch(`http://dev.fmode.cn:1337/parse/classes/${this.className}?${whereStr}`, {
  338. "headers": {
  339. "accept": "*/*",
  340. "x-parse-application-id": "dev"
  341. },
  342. "method": "GET",
  343. "mode": "cors",
  344. "credentials": "omit"
  345. });
  346. if (!response.ok) {
  347. throw new Error(`HTTP error! status: ${response.status}`);
  348. }
  349. const data = await response.json();
  350. return data.results.map(item => {
  351. const fmodeObject = new FmodeObject(this.className);
  352. fmodeObject.set(item);
  353. return fmodeObject;
  354. });
  355. } catch (error) {
  356. console.error('Error in get:', error);
  357. showNotification('获取数据失败', 'error');
  358. return [];
  359. }
  360. }
  361. async find() {
  362. return this.get();
  363. }
  364. }
  365. class FmodeObject {
  366. className;
  367. id;
  368. data;
  369. constructor(className) {
  370. this.className = className;
  371. this.data = {};
  372. this.id = null;
  373. }
  374. set(data) {
  375. this.data = {
  376. ...this.data,
  377. ...data
  378. };
  379. if (data?.objectId) {
  380. this.id = data.objectId;
  381. this.data.objectId = data.objectId;
  382. }
  383. }
  384. async save() {
  385. try {
  386. const data = { ...this.data };
  387. delete data.objectId;
  388. delete data.updatedAt;
  389. delete data.createdAt;
  390. delete data.ACL;
  391. const updateId = this.id ? `/${this.id}` : '';
  392. const method = this.id ? "PUT" : "POST";
  393. const response = await fetch(`http://dev.fmode.cn:1337/parse/classes/${this.className}${updateId}`, {
  394. "headers": {
  395. "accept": "*/*",
  396. "content-type": "application/json",
  397. "x-parse-application-id": "dev"
  398. },
  399. "body": JSON.stringify(data),
  400. "method": method,
  401. "mode": "cors",
  402. "credentials": "omit"
  403. });
  404. if (!response.ok) {
  405. throw new Error(`HTTP error! status: ${response.status}`);
  406. }
  407. const result = await response.json();
  408. if (result.objectId) {
  409. this.id = result.objectId;
  410. this.data.objectId = result.objectId;
  411. }
  412. return this;
  413. } catch (error) {
  414. console.error('Error in save:', error);
  415. showNotification('保存失败', 'error');
  416. throw error;
  417. }
  418. }
  419. async destroy() {
  420. try {
  421. if (!this.id) {
  422. throw new Error('No object ID to delete');
  423. }
  424. const response = await fetch(`http://dev.fmode.cn:1337/parse/classes/${this.className}/${this.id}`, {
  425. "headers": {
  426. "accept": "*/*",
  427. "x-parse-application-id": "dev"
  428. },
  429. "method": "DELETE",
  430. "mode": "cors",
  431. "credentials": "omit"
  432. });
  433. if (!response.ok) {
  434. throw new Error(`HTTP error! status: ${response.status}`);
  435. }
  436. return true;
  437. } catch (error) {
  438. console.error('Error in destroy:', error);
  439. showNotification('删除失败', 'error');
  440. return false;
  441. }
  442. }
  443. }
  444. // 全局变量
  445. let currentDeleteItem = null;
  446. let currentDeleteType = null;
  447. // DOM 元素加载完成后执行
  448. document.addEventListener('DOMContentLoaded', () => {
  449. // 加载所有数据
  450. loadAllData();
  451. // 导航链接切换
  452. document.querySelectorAll('.nav-link').forEach(link => {
  453. link.addEventListener('click', (e) => {
  454. e.preventDefault();
  455. // 更新导航样式
  456. document.querySelectorAll('.nav-link').forEach(item => {
  457. item.classList.remove('text-primary', 'bg-primary/10');
  458. item.classList.add('text-neutral-dark', 'hover:bg-gray-100');
  459. });
  460. link.classList.remove('text-neutral-dark', 'hover:bg-gray-100');
  461. link.classList.add('text-primary', 'bg-primary/10');
  462. // 显示对应的内容区域
  463. const targetId = link.getAttribute('href').substring(1);
  464. document.querySelectorAll('.content-section').forEach(section => {
  465. section.classList.add('hidden');
  466. });
  467. document.getElementById(targetId).classList.remove('hidden');
  468. });
  469. });
  470. // 项目相关事件
  471. document.getElementById('add-project-btn').addEventListener('click', () => {
  472. openProjectModal();
  473. });
  474. document.getElementById('cancel-project-btn').addEventListener('click', () => {
  475. closeProjectModal();
  476. });
  477. document.getElementById('save-project-btn').addEventListener('click', saveProject);
  478. // 成员相关事件
  479. document.getElementById('add-profile-btn').addEventListener('click', () => {
  480. openProfileModal();
  481. });
  482. document.getElementById('cancel-profile-btn').addEventListener('click', () => {
  483. closeProfileModal();
  484. });
  485. document.getElementById('save-profile-btn').addEventListener('click', saveProfile);
  486. // 项目成员相关事件
  487. document.getElementById('project-select').addEventListener('change', (e) => {
  488. const projectId = e.target.value;
  489. if (projectId) {
  490. document.getElementById('project-team-details').classList.remove('hidden');
  491. document.getElementById('no-project-selected').classList.add('hidden');
  492. document.getElementById('add-team-member-btn').removeAttribute('disabled');
  493. // 加载项目成员
  494. loadProjectMembers(projectId);
  495. // 更新当前项目名称
  496. const option = e.target.options[e.target.selectedIndex];
  497. document.getElementById('current-project-name').textContent = `项目: ${option.textContent}`;
  498. } else {
  499. document.getElementById('project-team-details').classList.add('hidden');
  500. document.getElementById('no-project-selected').classList.remove('hidden');
  501. document.getElementById('add-team-member-btn').setAttribute('disabled', 'true');
  502. }
  503. });
  504. document.getElementById('add-team-member-btn').addEventListener('click', () => {
  505. const projectId = document.getElementById('project-select').value;
  506. if (projectId) {
  507. openAddTeamMemberModal(projectId);
  508. }
  509. });
  510. document.getElementById('cancel-team-member-btn').addEventListener('click', () => {
  511. closeAddTeamMemberModal();
  512. });
  513. document.getElementById('save-team-member-btn').addEventListener('click', saveTeamMember);
  514. // 确认删除相关事件
  515. document.getElementById('cancel-delete-btn').addEventListener('click', closeDeleteModal);
  516. document.getElementById('confirm-delete-btn').addEventListener('click', confirmDelete);
  517. });
  518. // 加载所有数据
  519. async function loadAllData() {
  520. await Promise.all([
  521. loadProjects(),
  522. loadProfiles()
  523. ]);
  524. }
  525. // 加载项目列表
  526. async function loadProjects() {
  527. try {
  528. const query = new FmodeQuery("Project");
  529. const projects = await query.find();
  530. const tableBody = document.getElementById('projects-table-body');
  531. tableBody.innerHTML = '';
  532. if (projects.length === 0) {
  533. tableBody.innerHTML = `
  534. <tr>
  535. <td colspan="5" class="py-10 text-center text-neutral">
  536. <p>暂无项目数据</p>
  537. </td>
  538. </tr>
  539. `;
  540. return;
  541. }
  542. projects.forEach(project => {
  543. const row = document.createElement('tr');
  544. row.className = 'border-b hover:bg-gray-50 transition-bg';
  545. row.innerHTML = `
  546. <td class="py-3 px-4">${project.id}</td>
  547. <td class="py-3 px-4">${project.data.title || '-'}</td>
  548. <td class="py-3 px-4 max-w-xs truncate">${project.data.desc || '-'}</td>
  549. <td class="py-3 px-4">${project.data.duration || '-'}</td>
  550. <td class="py-3 px-4">
  551. <div class="flex space-x-2">
  552. <button class="text-primary hover:text-primary/80 p-1" onclick="editProject('${project.id}')">
  553. <i class="fa fa-edit"></i>
  554. </button>
  555. <button class="text-danger hover:text-danger/80 p-1" onclick="confirmDeleteProject('${project.id}')">
  556. <i class="fa fa-trash"></i>
  557. </button>
  558. </div>
  559. </td>
  560. `;
  561. tableBody.appendChild(row);
  562. });
  563. // 更新项目选择下拉框
  564. const projectSelect = document.getElementById('project-select');
  565. // 保存当前选中值
  566. const currentValue = projectSelect.value;
  567. // 清除现有选项(保留第一个)
  568. while (projectSelect.options.length > 1) {
  569. projectSelect.remove(1);
  570. }
  571. projects.forEach(project => {
  572. const option = document.createElement('option');
  573. option.value = project.id;
  574. option.textContent = project.data.title || `项目 ${project.id}`;
  575. projectSelect.appendChild(option);
  576. });
  577. // 恢复选中值
  578. if (currentValue) {
  579. projectSelect.value = currentValue;
  580. }
  581. } catch (error) {
  582. console.error('Error loading projects:', error);
  583. showNotification('加载项目失败', 'error');
  584. }
  585. }
  586. // 加载成员列表
  587. async function loadProfiles() {
  588. try {
  589. const query = new FmodeQuery("Profile");
  590. const profiles = await query.find();
  591. const tableBody = document.getElementById('profiles-table-body');
  592. tableBody.innerHTML = '';
  593. if (profiles.length === 0) {
  594. tableBody.innerHTML = `
  595. <tr>
  596. <td colspan="5" class="py-10 text-center text-neutral">
  597. <p>暂无成员数据</p>
  598. </td>
  599. </tr>
  600. `;
  601. return;
  602. }
  603. profiles.forEach(profile => {
  604. const row = document.createElement('tr');
  605. row.className = 'border-b hover:bg-gray-50 transition-bg';
  606. row.innerHTML = `
  607. <td class="py-3 px-4">${profile.id}</td>
  608. <td class="py-3 px-4">${profile.data.name || '-'}</td>
  609. <td class="py-3 px-4">${profile.data.gender || '-'}</td>
  610. <td class="py-3 px-4">${profile.data.desc || '-'}</td>
  611. <td class="py-3 px-4">
  612. <div class="flex space-x-2">
  613. <button class="text-primary hover:text-primary/80 p-1" onclick="editProfile('${profile.id}')">
  614. <i class="fa fa-edit"></i>
  615. </button>
  616. <button class="text-danger hover:text-danger/80 p-1" onclick="confirmDeleteProfile('${profile.id}')">
  617. <i class="fa fa-trash"></i>
  618. </button>
  619. </div>
  620. </td>
  621. `;
  622. tableBody.appendChild(row);
  623. });
  624. // 更新成员选择下拉框(用于添加项目成员)
  625. const profileSelect = document.getElementById('team-profile-id');
  626. // 清除现有选项(保留第一个)
  627. while (profileSelect.options.length > 1) {
  628. profileSelect.remove(1);
  629. }
  630. profiles.forEach(profile => {
  631. const option = document.createElement('option');
  632. option.value = profile.id;
  633. option.textContent = profile.data.name || `成员 ${profile.id}`;
  634. profileSelect.appendChild(option);
  635. });
  636. } catch (error) {
  637. console.error('Error loading profiles:', error);
  638. showNotification('加载成员失败', 'error');
  639. }
  640. }
  641. // 加载项目成员
  642. async function loadProjectMembers(projectId) {
  643. try {
  644. const query = new FmodeQuery("ProjectTeam");
  645. query.equalTo("project", { __type: "Pointer", className: "Project", objectId: projectId });
  646. const teamMembers = await query.find();
  647. // 获取所有成员ID
  648. const profileIds = teamMembers.map(member => member.data.profile?.objectId).filter(Boolean);
  649. // 获取成员详细信息
  650. let profiles = [];
  651. if (profileIds.length > 0) {
  652. const profileQuery = new FmodeQuery("Profile");
  653. profileQuery.where = { objectId: { $in: profileIds } };
  654. profiles = await profileQuery.find();
  655. }
  656. const tableBody = document.getElementById('team-members-table-body');
  657. tableBody.innerHTML = '';
  658. if (profiles.length === 0) {
  659. tableBody.innerHTML = `
  660. <tr>
  661. <td colspan="5" class="py-10 text-center text-neutral">
  662. <p>该项目暂无成员,请添加成员</p>
  663. </td>
  664. </tr>
  665. `;
  666. return;
  667. }
  668. // 为每个成员找到对应的team记录ID,以便删除
  669. profiles.forEach(profile => {
  670. const teamMember = teamMembers.find(m => m.data.profile?.objectId === profile.id);
  671. const row = document.createElement('tr');
  672. row.className = 'border-b hover:bg-gray-50 transition-bg';
  673. row.innerHTML = `
  674. <td class="py-3 px-4">${profile.id}</td>
  675. <td class="py-3 px-4">${profile.data.name || '-'}</td>
  676. <td class="py-3 px-4">${profile.data.gender || '-'}</td>
  677. <td class="py-3 px-4">${profile.data.desc || '-'}</td>
  678. <td class="py-3 px-4">
  679. <button class="text-danger hover:text-danger/80 p-1" onclick="confirmDeleteTeamMember('${teamMember.id}', '${projectId}')">
  680. <i class="fa fa-trash"></i>
  681. </button>
  682. </td>
  683. `;
  684. tableBody.appendChild(row);
  685. });
  686. } catch (error) {
  687. console.error('Error loading project members:', error);
  688. showNotification('加载项目成员失败', 'error');
  689. }
  690. }
  691. // 打开项目模态框
  692. function openProjectModal(projectId = null) {
  693. document.getElementById('project-modal-title').textContent = projectId ? '编辑项目' : '添加项目';
  694. document.getElementById('project-form').reset();
  695. document.getElementById('project-id').value = '';
  696. if (projectId) {
  697. // 加载项目数据
  698. const query = new FmodeQuery("Project");
  699. query.equalTo("objectId", projectId);
  700. query.find().then(projects => {
  701. if (projects.length > 0) {
  702. const project = projects[0];
  703. document.getElementById('project-id').value = project.id;
  704. document.getElementById('project-title').value = project.data.title || '';
  705. document.getElementById('project-desc').value = project.data.desc || '';
  706. document.getElementById('project-avatar').value = project.data.avatar || '';
  707. document.getElementById('project-duration').value = project.data.duration || '';
  708. }
  709. });
  710. }
  711. document.getElementById('project-modal').classList.remove('hidden');
  712. document.getElementById('project-modal').classList.add('flex');
  713. }
  714. // 关闭项目模态框
  715. function closeProjectModal() {
  716. document.getElementById('project-modal').classList.remove('flex');
  717. document.getElementById('project-modal').classList.add('hidden');
  718. }
  719. // 保存项目
  720. async function saveProject() {
  721. const projectId = document.getElementById('project-id').value;
  722. const title = document.getElementById('project-title').value;
  723. const desc = document.getElementById('project-desc').value;
  724. const avatar = document.getElementById('project-avatar').value;
  725. const duration = document.getElementById('project-duration').value;
  726. if (!title || !duration) {
  727. showNotification('请填写必填字段', 'warning');
  728. return;
  729. }
  730. try {
  731. const project = new FmodeObject("Project");
  732. if (projectId) {
  733. project.id = projectId;
  734. }
  735. project.set({
  736. title,
  737. desc,
  738. avatar,
  739. duration: parseInt(duration)
  740. });
  741. await project.save();
  742. closeProjectModal();
  743. await loadProjects();
  744. showNotification(projectId ? '项目更新成功' : '项目创建成功', 'success');
  745. } catch (error) {
  746. console.error('Error saving project:', error);
  747. showNotification('保存项目失败', 'error');
  748. }
  749. }
  750. // 打开成员模态框
  751. function openProfileModal(profileId = null) {
  752. document.getElementById('profile-modal-title').textContent = profileId ? '编辑成员' : '添加成员';
  753. document.getElementById('profile-form').reset();
  754. document.getElementById('profile-id').value = '';
  755. if (profileId) {
  756. // 加载成员数据
  757. const query = new FmodeQuery("Profile");
  758. query.equalTo("objectId", profileId);
  759. query.find().then(profiles => {
  760. if (profiles.length > 0) {
  761. const profile = profiles[0];
  762. document.getElementById('profile-id').value = profile.id;
  763. document.getElementById('profile-name').value = profile.data.name || '';
  764. document.getElementById('profile-gender').value = profile.data.gender || '';
  765. document.getElementById('profile-desc').value = profile.data.desc || '';
  766. }
  767. });
  768. }
  769. document.getElementById('profile-modal').classList.remove('hidden');
  770. document.getElementById('profile-modal').classList.add('flex');
  771. }
  772. // 关闭成员模态框
  773. function closeProfileModal() {
  774. document.getElementById('profile-modal').classList.remove('flex');
  775. document.getElementById('profile-modal').classList.add('hidden');
  776. }
  777. // 保存成员
  778. async function saveProfile() {
  779. const profileId = document.getElementById('profile-id').value;
  780. const name = document.getElementById('profile-name').value;
  781. const gender = document.getElementById('profile-gender').value;
  782. const desc = document.getElementById('profile-desc').value;
  783. if (!name || !gender) {
  784. showNotification('请填写必填字段', 'warning');
  785. return;
  786. }
  787. try {
  788. const profile = new FmodeObject("Profile");
  789. if (profileId) {
  790. profile.id = profileId;
  791. }
  792. profile.set({
  793. name,
  794. gender,
  795. desc
  796. });
  797. await profile.save();
  798. closeProfileModal();
  799. await loadProfiles();
  800. showNotification(profileId ? '成员更新成功' : '成员创建成功', 'success');
  801. } catch (error) {
  802. console.error('Error saving profile:', error);
  803. showNotification('保存成员失败', 'error');
  804. }
  805. }
  806. // 打开添加项目成员模态框
  807. function openAddTeamMemberModal(projectId) {
  808. document.getElementById('team-project-id').value = projectId;
  809. document.getElementById('team-profile-id').value = '';
  810. document.getElementById('add-team-member-modal').classList.remove('hidden');
  811. document.getElementById('add-team-member-modal').classList.add('flex');
  812. }
  813. // 关闭添加项目成员模态框
  814. function closeAddTeamMemberModal() {
  815. document.getElementById('add-team-member-modal').classList.remove('flex');
  816. document.getElementById('add-team-member-modal').classList.add('hidden');
  817. }
  818. // 保存项目成员
  819. async function saveTeamMember() {
  820. const projectId = document.getElementById('team-project-id').value;
  821. const profileId = document.getElementById('team-profile-id').value;
  822. if (!projectId || !profileId) {
  823. showNotification('请选择项目和成员', 'warning');
  824. return;
  825. }
  826. try {
  827. // 检查是否已存在该关联
  828. const query = new FmodeQuery("ProjectTeam");
  829. query.equalTo("project", { __type: "Pointer", className: "Project", objectId: projectId });
  830. query.equalTo("profile", { __type: "Pointer", className: "Profile", objectId: profileId });
  831. const existing = await query.find();
  832. if (existing.length > 0) {
  833. showNotification('该成员已在项目中', 'warning');
  834. return;
  835. }
  836. const teamMember = new FmodeObject("ProjectTeam");
  837. teamMember.set({
  838. project: { __type: "Pointer", className: "Project", objectId: projectId },
  839. profile: { __type: "Pointer", className: "Profile", objectId: profileId }
  840. });
  841. await teamMember.save();
  842. closeAddTeamMemberModal();
  843. await loadProjectMembers(projectId);
  844. showNotification('成员已添加到项目', 'success');
  845. } catch (error) {
  846. console.error('Error saving team member:', error);
  847. showNotification('添加项目成员失败', 'error');
  848. }
  849. }
  850. // 打开删除确认模态框
  851. function openDeleteModal(itemId, type, extraData = null) {
  852. currentDeleteItem = { id: itemId, extraData };
  853. currentDeleteType = type;
  854. let message = '';
  855. switch (type) {
  856. case 'project':
  857. message = '您确定要删除这个项目吗?相关的项目成员关联也将被删除。';
  858. break;
  859. case 'profile':
  860. message = '您确定要删除这个成员吗?该成员在所有项目中的关联也将被删除。';
  861. break;
  862. case 'teamMember':
  863. message = '您确定要将这个成员从项目中移除吗?';
  864. break;
  865. default:
  866. message = '您确定要删除这项记录吗?此操作不可撤销。';
  867. }
  868. document.getElementById('delete-confirmation-message').textContent = message;
  869. document.getElementById('confirm-delete-modal').classList.remove('hidden');
  870. document.getElementById('confirm-delete-modal').classList.add('flex');
  871. }
  872. // 关闭删除确认模态框
  873. function closeDeleteModal() {
  874. currentDeleteItem = null;
  875. currentDeleteType = null;
  876. document.getElementById('confirm-delete-modal').classList.remove('flex');
  877. document.getElementById('confirm-delete-modal').classList.add('hidden');
  878. }
  879. // 确认删除
  880. async function confirmDelete() {
  881. if (!currentDeleteItem || !currentDeleteType) return;
  882. try {
  883. switch (currentDeleteType) {
  884. case 'project':
  885. // 先删除相关的项目成员关联
  886. const teamQuery = new FmodeQuery("ProjectTeam");
  887. teamQuery.equalTo("project", { __type: "Pointer", className: "Project", objectId: currentDeleteItem.id });
  888. const teamMembers = await teamQuery.find();
  889. for (const member of teamMembers) {
  890. await member.destroy();
  891. }
  892. // 再删除项目
  893. const project = new FmodeObject("Project");
  894. project.id = currentDeleteItem.id;
  895. await project.destroy();
  896. await loadProjects();
  897. showNotification('项目已删除', 'success');
  898. break;
  899. case 'profile':
  900. // 先删除相关的项目成员关联
  901. const profileTeamQuery = new FmodeQuery("ProjectTeam");
  902. profileTeamQuery.equalTo("profile", { __type: "Pointer", className: "Profile", objectId: currentDeleteItem.id });
  903. const profileTeamMembers = await profileTeamQuery.find();
  904. for (const member of profileTeamMembers) {
  905. await member.destroy();
  906. }
  907. // 再删除成员
  908. const profile = new FmodeObject("Profile");
  909. profile.id = currentDeleteItem.id;
  910. await profile.destroy();
  911. await loadProfiles();
  912. // 如果有项目被选中,刷新项目成员列表
  913. const selectedProject = document.getElementById('project-select').value;
  914. if (selectedProject) {
  915. await loadProjectMembers(selectedProject);
  916. }
  917. showNotification('成员已删除', 'success');
  918. break;
  919. case 'teamMember':
  920. const teamMember = new FmodeObject("ProjectTeam");
  921. teamMember.id = currentDeleteItem.id;
  922. await teamMember.destroy();
  923. await loadProjectMembers(currentDeleteItem.extraData);
  924. showNotification('成员已从项目中移除', 'success');
  925. break;
  926. }
  927. closeDeleteModal();
  928. } catch (error) {
  929. console.error('Error deleting item:', error);
  930. showNotification('删除失败', 'error');
  931. }
  932. }
  933. // 显示通知
  934. function showNotification(message, type = 'info') {
  935. const notification = document.getElementById('notification');
  936. const icon = document.getElementById('notification-icon');
  937. const messageEl = document.getElementById('notification-message');
  938. // 设置通知类型样式
  939. 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';
  940. switch (type) {
  941. case 'success':
  942. notification.classList.add('bg-success', 'text-white');
  943. icon.className = 'fa fa-check-circle mr-2 text-xl';
  944. break;
  945. case 'error':
  946. notification.classList.add('bg-danger', 'text-white');
  947. icon.className = 'fa fa-times-circle mr-2 text-xl';
  948. break;
  949. case 'warning':
  950. notification.classList.add('bg-warning', 'text-white');
  951. icon.className = 'fa fa-exclamation-triangle mr-2 text-xl';
  952. break;
  953. default:
  954. notification.classList.add('bg-primary', 'text-white');
  955. icon.className = 'fa fa-info-circle mr-2 text-xl';
  956. }
  957. messageEl.textContent = message;
  958. // 显示通知
  959. setTimeout(() => {
  960. notification.classList.remove('translate-y-20', 'opacity-0');
  961. }, 10);
  962. // 3秒后隐藏通知
  963. setTimeout(() => {
  964. notification.classList.add('translate-y-20', 'opacity-0');
  965. }, 3000);
  966. }
  967. // 全局函数,用于HTML中的事件处理
  968. window.editProject = function(projectId) {
  969. openProjectModal(projectId);
  970. };
  971. window.confirmDeleteProject = function(projectId) {
  972. openDeleteModal(projectId, 'project');
  973. };
  974. window.editProfile = function(profileId) {
  975. openProfileModal(profileId);
  976. };
  977. window.confirmDeleteProfile = function(profileId) {
  978. openDeleteModal(profileId, 'profile');
  979. };
  980. window.confirmDeleteTeamMember = function(teamMemberId, projectId) {
  981. openDeleteModal(teamMemberId, 'teamMember', projectId);
  982. };
  983. </script>
  984. </body>
  985. </html>