update-project-status-aftercare.html 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  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. <style>
  8. * {
  9. margin: 0;
  10. padding: 0;
  11. box-sizing: border-box;
  12. }
  13. body {
  14. font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
  15. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  16. min-height: 100vh;
  17. display: flex;
  18. justify-content: center;
  19. align-items: center;
  20. padding: 20px;
  21. }
  22. .container {
  23. background: white;
  24. border-radius: 20px;
  25. box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
  26. max-width: 800px;
  27. width: 100%;
  28. padding: 40px;
  29. }
  30. h1 {
  31. color: #333;
  32. margin-bottom: 10px;
  33. font-size: 28px;
  34. }
  35. .subtitle {
  36. color: #666;
  37. margin-bottom: 30px;
  38. font-size: 14px;
  39. }
  40. .info-box {
  41. background: #f8f9fa;
  42. border-left: 4px solid #667eea;
  43. padding: 15px;
  44. margin-bottom: 20px;
  45. border-radius: 4px;
  46. }
  47. .info-box p {
  48. margin: 5px 0;
  49. color: #555;
  50. font-size: 14px;
  51. }
  52. button {
  53. background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  54. color: white;
  55. border: none;
  56. padding: 15px 40px;
  57. border-radius: 10px;
  58. font-size: 16px;
  59. cursor: pointer;
  60. transition: transform 0.2s, box-shadow 0.2s;
  61. width: 100%;
  62. margin-bottom: 15px;
  63. }
  64. button:hover:not(:disabled) {
  65. transform: translateY(-2px);
  66. box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
  67. }
  68. button:disabled {
  69. opacity: 0.6;
  70. cursor: not-allowed;
  71. }
  72. #progress {
  73. margin-top: 20px;
  74. padding: 20px;
  75. background: #f8f9fa;
  76. border-radius: 10px;
  77. max-height: 400px;
  78. overflow-y: auto;
  79. display: none;
  80. }
  81. #progress.show {
  82. display: block;
  83. }
  84. .log-entry {
  85. padding: 8px 12px;
  86. margin: 5px 0;
  87. border-radius: 4px;
  88. font-size: 13px;
  89. font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
  90. }
  91. .log-info {
  92. background: #e3f2fd;
  93. color: #1976d2;
  94. border-left: 3px solid #1976d2;
  95. }
  96. .log-success {
  97. background: #e8f5e9;
  98. color: #388e3c;
  99. border-left: 3px solid #388e3c;
  100. }
  101. .log-warning {
  102. background: #fff3e0;
  103. color: #f57c00;
  104. border-left: 3px solid #f57c00;
  105. }
  106. .log-error {
  107. background: #ffebee;
  108. color: #d32f2f;
  109. border-left: 3px solid #d32f2f;
  110. }
  111. .summary {
  112. margin-top: 20px;
  113. padding: 20px;
  114. background: #e8f5e9;
  115. border-radius: 10px;
  116. border: 2px solid #4caf50;
  117. display: none;
  118. }
  119. .summary.show {
  120. display: block;
  121. }
  122. .summary h3 {
  123. color: #2e7d32;
  124. margin-bottom: 15px;
  125. }
  126. .summary-stats {
  127. display: grid;
  128. grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
  129. gap: 15px;
  130. }
  131. .summary-stat {
  132. text-align: center;
  133. padding: 15px;
  134. background: white;
  135. border-radius: 8px;
  136. }
  137. .summary-stat-value {
  138. font-size: 32px;
  139. font-weight: bold;
  140. color: #667eea;
  141. }
  142. .summary-stat-label {
  143. font-size: 14px;
  144. color: #666;
  145. margin-top: 5px;
  146. }
  147. </style>
  148. </head>
  149. <body>
  150. <div class="container">
  151. <h1>🔄 更新售后归档项目状态</h1>
  152. <p class="subtitle">将所有 currentStage = '售后归档' 的项目状态更新为 '已完成'</p>
  153. <div class="info-box">
  154. <p><strong>📋 操作说明:</strong></p>
  155. <p>• 此工具将扫描所有 currentStage 为 '售后归档' 的项目</p>
  156. <p>• 自动将这些项目的 status 字段更新为 '已完成'</p>
  157. <p>• 同时记录完成时间(completedAt 字段)</p>
  158. <p>• 更新后,员工的已完成项目统计将自动更新</p>
  159. </div>
  160. <button id="updateBtn" onclick="updateAfterCareProjects()">开始更新售后归档项目</button>
  161. <button id="refreshBtn" onclick="refreshEmployeeStats()" style="display: none;">刷新员工统计数据</button>
  162. <div id="progress"></div>
  163. <div id="summary" class="summary"></div>
  164. </div>
  165. <script type="module">
  166. import { FmodeParse } from 'fmode-ng/parse';
  167. const Parse = FmodeParse.with("nova");
  168. const companyId = localStorage.getItem('company') || '';
  169. window.Parse = Parse;
  170. window.companyId = companyId;
  171. console.log('✅ Parse SDK 初始化完成', { companyId });
  172. </script>
  173. <script>
  174. let updatedCount = 0;
  175. let skippedCount = 0;
  176. let errorCount = 0;
  177. let processedProjects = [];
  178. function log(message, type = 'info') {
  179. const progress = document.getElementById('progress');
  180. progress.classList.add('show');
  181. const entry = document.createElement('div');
  182. entry.className = `log-entry log-${type}`;
  183. const timestamp = new Date().toLocaleTimeString('zh-CN');
  184. entry.textContent = `[${timestamp}] ${message}`;
  185. progress.appendChild(entry);
  186. progress.scrollTop = progress.scrollHeight;
  187. console.log(`[${type.toUpperCase()}]`, message);
  188. }
  189. async function updateAfterCareProjects() {
  190. const btn = document.getElementById('updateBtn');
  191. const refreshBtn = document.getElementById('refreshBtn');
  192. btn.disabled = true;
  193. btn.textContent = '正在处理...';
  194. updatedCount = 0;
  195. skippedCount = 0;
  196. errorCount = 0;
  197. processedProjects = [];
  198. document.getElementById('progress').innerHTML = '';
  199. document.getElementById('summary').classList.remove('show');
  200. log('🚀 开始扫描售后归档项目...', 'info');
  201. try {
  202. if (!window.Parse) {
  203. throw new Error('Parse SDK 未初始化');
  204. }
  205. if (!window.companyId) {
  206. throw new Error('未找到公司ID(localStorage的company字段为空)');
  207. }
  208. // 查询所有 currentStage 为 '售后归档' 的项目
  209. const query = new window.Parse.Query('Project');
  210. query.equalTo('company', window.companyId);
  211. query.equalTo('currentStage', '售后归档');
  212. query.notEqualTo('isDeleted', true);
  213. query.include('assignee'); // 包含负责人信息
  214. query.select('title', 'status', 'currentStage', 'assignee', 'data', 'completedAt', 'updatedAt');
  215. query.limit(1000);
  216. log('🔍 查询条件:currentStage = "售后归档"', 'info');
  217. const projects = await query.find();
  218. log(`✅ 找到 ${projects.length} 个售后归档项目`, 'success');
  219. if (projects.length === 0) {
  220. log('⚠️ 没有需要更新的项目', 'warning');
  221. btn.disabled = false;
  222. btn.textContent = '开始更新售后归档项目';
  223. return;
  224. }
  225. // 逐个更新项目状态
  226. for (let i = 0; i < projects.length; i++) {
  227. const project = projects[i];
  228. const projectName = project.get('title') || '未命名项目';
  229. const currentStatus = project.get('status');
  230. const assignee = project.get('assignee');
  231. const assigneeName = assignee ? (assignee.get('name') || '未知') : '未分配';
  232. try {
  233. log(`[${i + 1}/${projects.length}] 处理项目: ${projectName} (当前状态: ${currentStatus})`, 'info');
  234. // 检查是否已经是"已完成"状态
  235. if (currentStatus === '已完成') {
  236. log(` ⏭️ 跳过:项目已经是"已完成"状态`, 'warning');
  237. skippedCount++;
  238. processedProjects.push({
  239. name: projectName,
  240. assignee: assigneeName,
  241. status: 'skipped',
  242. reason: '已经是已完成状态'
  243. });
  244. continue;
  245. }
  246. // 更新项目状态
  247. project.set('status', '已完成');
  248. // 如果没有 completedAt 字段,添加完成时间
  249. if (!project.get('completedAt')) {
  250. project.set('completedAt', new Date());
  251. }
  252. // 保存更新
  253. await project.save();
  254. log(` ✅ 成功更新: ${projectName} -> 状态改为"已完成"`, 'success');
  255. log(` 👤 负责人: ${assigneeName}`, 'info');
  256. updatedCount++;
  257. processedProjects.push({
  258. name: projectName,
  259. assignee: assigneeName,
  260. status: 'updated',
  261. previousStatus: currentStatus
  262. });
  263. // 避免请求过快
  264. await new Promise(resolve => setTimeout(resolve, 200));
  265. } catch (error) {
  266. log(` ❌ 更新失败: ${projectName} - ${error.message}`, 'error');
  267. errorCount++;
  268. processedProjects.push({
  269. name: projectName,
  270. assignee: assigneeName,
  271. status: 'error',
  272. error: error.message
  273. });
  274. }
  275. }
  276. // 显示汇总
  277. showSummary();
  278. log('🎉 所有项目处理完成!', 'success');
  279. // 显示刷新按钮
  280. refreshBtn.style.display = 'block';
  281. } catch (error) {
  282. log(`❌ 操作失败: ${error.message}`, 'error');
  283. console.error('详细错误:', error);
  284. } finally {
  285. btn.disabled = false;
  286. btn.textContent = '重新运行更新';
  287. }
  288. }
  289. function showSummary() {
  290. const summary = document.getElementById('summary');
  291. summary.classList.add('show');
  292. summary.innerHTML = `
  293. <h3>📊 更新汇总</h3>
  294. <div class="summary-stats">
  295. <div class="summary-stat">
  296. <div class="summary-stat-value">${updatedCount}</div>
  297. <div class="summary-stat-label">成功更新</div>
  298. </div>
  299. <div class="summary-stat">
  300. <div class="summary-stat-value">${skippedCount}</div>
  301. <div class="summary-stat-label">跳过</div>
  302. </div>
  303. <div class="summary-stat">
  304. <div class="summary-stat-value">${errorCount}</div>
  305. <div class="summary-stat-label">失败</div>
  306. </div>
  307. <div class="summary-stat">
  308. <div class="summary-stat-value">${updatedCount + skippedCount + errorCount}</div>
  309. <div class="summary-stat-label">总计</div>
  310. </div>
  311. </div>
  312. `;
  313. }
  314. async function refreshEmployeeStats() {
  315. log('🔄 刷新员工统计数据...', 'info');
  316. log('💡 提示:现在可以回到员工管理页面,刷新页面查看更新后的数据', 'info');
  317. log('📊 每个员工的"已完成项目数"应该会增加', 'success');
  318. }
  319. // 页面加载时检查 Parse 和公司ID
  320. window.addEventListener('load', () => {
  321. setTimeout(() => {
  322. if (!window.Parse) {
  323. log('❌ Parse SDK 未初始化,请刷新页面重试', 'error');
  324. document.getElementById('updateBtn').disabled = true;
  325. } else if (!window.companyId) {
  326. log('⚠️ 未找到公司ID,请确保已登录系统', 'warning');
  327. document.getElementById('updateBtn').disabled = true;
  328. } else {
  329. log(`✅ 就绪:公司ID = ${window.companyId}`, 'success');
  330. }
  331. }, 500);
  332. });
  333. </script>
  334. </body>
  335. </html>