| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401 |
- <!DOCTYPE html>
- <html lang="zh-CN">
- <head>
- <meta charset="UTF-8">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <title>更新售后归档项目状态为已完成</title>
- <style>
- * {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
- }
-
- body {
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- min-height: 100vh;
- display: flex;
- justify-content: center;
- align-items: center;
- padding: 20px;
- }
-
- .container {
- background: white;
- border-radius: 20px;
- box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
- max-width: 800px;
- width: 100%;
- padding: 40px;
- }
-
- h1 {
- color: #333;
- margin-bottom: 10px;
- font-size: 28px;
- }
-
- .subtitle {
- color: #666;
- margin-bottom: 30px;
- font-size: 14px;
- }
-
- .info-box {
- background: #f8f9fa;
- border-left: 4px solid #667eea;
- padding: 15px;
- margin-bottom: 20px;
- border-radius: 4px;
- }
-
- .info-box p {
- margin: 5px 0;
- color: #555;
- font-size: 14px;
- }
-
- button {
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- color: white;
- border: none;
- padding: 15px 40px;
- border-radius: 10px;
- font-size: 16px;
- cursor: pointer;
- transition: transform 0.2s, box-shadow 0.2s;
- width: 100%;
- margin-bottom: 15px;
- }
-
- button:hover:not(:disabled) {
- transform: translateY(-2px);
- box-shadow: 0 10px 20px rgba(102, 126, 234, 0.3);
- }
-
- button:disabled {
- opacity: 0.6;
- cursor: not-allowed;
- }
-
- #progress {
- margin-top: 20px;
- padding: 20px;
- background: #f8f9fa;
- border-radius: 10px;
- max-height: 400px;
- overflow-y: auto;
- display: none;
- }
-
- #progress.show {
- display: block;
- }
-
- .log-entry {
- padding: 8px 12px;
- margin: 5px 0;
- border-radius: 4px;
- font-size: 13px;
- font-family: 'Monaco', 'Menlo', 'Consolas', monospace;
- }
-
- .log-info {
- background: #e3f2fd;
- color: #1976d2;
- border-left: 3px solid #1976d2;
- }
-
- .log-success {
- background: #e8f5e9;
- color: #388e3c;
- border-left: 3px solid #388e3c;
- }
-
- .log-warning {
- background: #fff3e0;
- color: #f57c00;
- border-left: 3px solid #f57c00;
- }
-
- .log-error {
- background: #ffebee;
- color: #d32f2f;
- border-left: 3px solid #d32f2f;
- }
-
- .summary {
- margin-top: 20px;
- padding: 20px;
- background: #e8f5e9;
- border-radius: 10px;
- border: 2px solid #4caf50;
- display: none;
- }
-
- .summary.show {
- display: block;
- }
-
- .summary h3 {
- color: #2e7d32;
- margin-bottom: 15px;
- }
-
- .summary-stats {
- display: grid;
- grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
- gap: 15px;
- }
-
- .summary-stat {
- text-align: center;
- padding: 15px;
- background: white;
- border-radius: 8px;
- }
-
- .summary-stat-value {
- font-size: 32px;
- font-weight: bold;
- color: #667eea;
- }
-
- .summary-stat-label {
- font-size: 14px;
- color: #666;
- margin-top: 5px;
- }
- </style>
- </head>
- <body>
- <div class="container">
- <h1>🔄 更新售后归档项目状态</h1>
- <p class="subtitle">将所有 currentStage = '售后归档' 的项目状态更新为 '已完成'</p>
-
- <div class="info-box">
- <p><strong>📋 操作说明:</strong></p>
- <p>• 此工具将扫描所有 currentStage 为 '售后归档' 的项目</p>
- <p>• 自动将这些项目的 status 字段更新为 '已完成'</p>
- <p>• 同时记录完成时间(completedAt 字段)</p>
- <p>• 更新后,员工的已完成项目统计将自动更新</p>
- </div>
-
- <button id="updateBtn" onclick="updateAfterCareProjects()">开始更新售后归档项目</button>
- <button id="refreshBtn" onclick="refreshEmployeeStats()" style="display: none;">刷新员工统计数据</button>
-
- <div id="progress"></div>
- <div id="summary" class="summary"></div>
- </div>
- <script type="module">
- import { FmodeParse } from 'fmode-ng/parse';
-
- const Parse = FmodeParse.with("nova");
- const companyId = localStorage.getItem('company') || '';
-
- window.Parse = Parse;
- window.companyId = companyId;
-
- console.log('✅ Parse SDK 初始化完成', { companyId });
- </script>
-
- <script>
- let updatedCount = 0;
- let skippedCount = 0;
- let errorCount = 0;
- let processedProjects = [];
-
- function log(message, type = 'info') {
- const progress = document.getElementById('progress');
- progress.classList.add('show');
-
- const entry = document.createElement('div');
- entry.className = `log-entry log-${type}`;
-
- const timestamp = new Date().toLocaleTimeString('zh-CN');
- entry.textContent = `[${timestamp}] ${message}`;
-
- progress.appendChild(entry);
- progress.scrollTop = progress.scrollHeight;
-
- console.log(`[${type.toUpperCase()}]`, message);
- }
-
- async function updateAfterCareProjects() {
- const btn = document.getElementById('updateBtn');
- const refreshBtn = document.getElementById('refreshBtn');
- btn.disabled = true;
- btn.textContent = '正在处理...';
-
- updatedCount = 0;
- skippedCount = 0;
- errorCount = 0;
- processedProjects = [];
-
- document.getElementById('progress').innerHTML = '';
- document.getElementById('summary').classList.remove('show');
-
- log('🚀 开始扫描售后归档项目...', 'info');
-
- try {
- if (!window.Parse) {
- throw new Error('Parse SDK 未初始化');
- }
-
- if (!window.companyId) {
- throw new Error('未找到公司ID(localStorage的company字段为空)');
- }
-
- // 查询所有 currentStage 为 '售后归档' 的项目
- const query = new window.Parse.Query('Project');
- query.equalTo('company', window.companyId);
- query.equalTo('currentStage', '售后归档');
- query.notEqualTo('isDeleted', true);
- query.include('assignee'); // 包含负责人信息
- query.select('title', 'status', 'currentStage', 'assignee', 'data', 'completedAt', 'updatedAt');
- query.limit(1000);
-
- log('🔍 查询条件:currentStage = "售后归档"', 'info');
-
- const projects = await query.find();
-
- log(`✅ 找到 ${projects.length} 个售后归档项目`, 'success');
-
- if (projects.length === 0) {
- log('⚠️ 没有需要更新的项目', 'warning');
- btn.disabled = false;
- btn.textContent = '开始更新售后归档项目';
- return;
- }
-
- // 逐个更新项目状态
- for (let i = 0; i < projects.length; i++) {
- const project = projects[i];
- const projectName = project.get('title') || '未命名项目';
- const currentStatus = project.get('status');
- const assignee = project.get('assignee');
- const assigneeName = assignee ? (assignee.get('name') || '未知') : '未分配';
-
- try {
- log(`[${i + 1}/${projects.length}] 处理项目: ${projectName} (当前状态: ${currentStatus})`, 'info');
-
- // 检查是否已经是"已完成"状态
- if (currentStatus === '已完成') {
- log(` ⏭️ 跳过:项目已经是"已完成"状态`, 'warning');
- skippedCount++;
- processedProjects.push({
- name: projectName,
- assignee: assigneeName,
- status: 'skipped',
- reason: '已经是已完成状态'
- });
- continue;
- }
-
- // 更新项目状态
- project.set('status', '已完成');
-
- // 如果没有 completedAt 字段,添加完成时间
- if (!project.get('completedAt')) {
- project.set('completedAt', new Date());
- }
-
- // 保存更新
- await project.save();
-
- log(` ✅ 成功更新: ${projectName} -> 状态改为"已完成"`, 'success');
- log(` 👤 负责人: ${assigneeName}`, 'info');
-
- updatedCount++;
- processedProjects.push({
- name: projectName,
- assignee: assigneeName,
- status: 'updated',
- previousStatus: currentStatus
- });
-
- // 避免请求过快
- await new Promise(resolve => setTimeout(resolve, 200));
-
- } catch (error) {
- log(` ❌ 更新失败: ${projectName} - ${error.message}`, 'error');
- errorCount++;
- processedProjects.push({
- name: projectName,
- assignee: assigneeName,
- status: 'error',
- error: error.message
- });
- }
- }
-
- // 显示汇总
- showSummary();
-
- log('🎉 所有项目处理完成!', 'success');
-
- // 显示刷新按钮
- refreshBtn.style.display = 'block';
-
- } catch (error) {
- log(`❌ 操作失败: ${error.message}`, 'error');
- console.error('详细错误:', error);
- } finally {
- btn.disabled = false;
- btn.textContent = '重新运行更新';
- }
- }
-
- function showSummary() {
- const summary = document.getElementById('summary');
- summary.classList.add('show');
-
- summary.innerHTML = `
- <h3>📊 更新汇总</h3>
- <div class="summary-stats">
- <div class="summary-stat">
- <div class="summary-stat-value">${updatedCount}</div>
- <div class="summary-stat-label">成功更新</div>
- </div>
- <div class="summary-stat">
- <div class="summary-stat-value">${skippedCount}</div>
- <div class="summary-stat-label">跳过</div>
- </div>
- <div class="summary-stat">
- <div class="summary-stat-value">${errorCount}</div>
- <div class="summary-stat-label">失败</div>
- </div>
- <div class="summary-stat">
- <div class="summary-stat-value">${updatedCount + skippedCount + errorCount}</div>
- <div class="summary-stat-label">总计</div>
- </div>
- </div>
- `;
- }
-
- async function refreshEmployeeStats() {
- log('🔄 刷新员工统计数据...', 'info');
- log('💡 提示:现在可以回到员工管理页面,刷新页面查看更新后的数据', 'info');
- log('📊 每个员工的"已完成项目数"应该会增加', 'success');
- }
-
- // 页面加载时检查 Parse 和公司ID
- window.addEventListener('load', () => {
- setTimeout(() => {
- if (!window.Parse) {
- log('❌ Parse SDK 未初始化,请刷新页面重试', 'error');
- document.getElementById('updateBtn').disabled = true;
- } else if (!window.companyId) {
- log('⚠️ 未找到公司ID,请确保已登录系统', 'warning');
- document.getElementById('updateBtn').disabled = true;
- } else {
- log(`✅ 就绪:公司ID = ${window.companyId}`, 'success');
- }
- }, 500);
- });
- </script>
- </body>
- </html>
|