导出MPlus物料数据工具

从MPlus系统获取店铺列表和安装数据并导出Excel

// ==UserScript==
// @name         导出MPlus物料数据工具
// @namespace    http://tampermonkey.net/
// @version      1.9.2
// @description  从MPlus系统获取店铺列表和安装数据并导出Excel
// @author       21克的爱情提供技术支持
// @match        *://mplus.lorealchina.com/*
// @grant        GM_xmlhttpRequest
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/xlsx.full.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js
// @license MIT
// @supportURL   https://github.com/CodeGather/tampermonkey/issues
// ==/UserScript==

(function() {
    'use strict';
    // 版本控制
    const SCRIPT_VERSION = GM_info.script.version;
    console.log(`导出MPlus物料数据工具 v${SCRIPT_VERSION}`);

    // 显示版本更新通知
    function showUpdateNotification() {
        // 检查是否为新版本
        const lastVersion = localStorage.getItem('mplusExportToolVersion');
        if (lastVersion !== SCRIPT_VERSION) {
            const notification = document.createElement('div');
            notification.style.cssText = `
                position: fixed;
                bottom: 20px;
                right: 20px;
                background: #4CAF50;
                color: white;
                padding: 15px;
                border-radius: 5px;
                box-shadow: 0 4px 8px rgba(0,0,0,0.2);
                z-index: 10000;
                max-width: 300px;
                font-family: Arial, sans-serif;
            `;

            notification.innerHTML = `
                <h3 style="margin:0 0 10px 0;">MPlus导出工具已更新!</h3>
                <p>当前版本: v${SCRIPT_VERSION}</p>
                <div style="display:flex; justify-content:space-between; margin-top:10px;">
                    <button id="close-notification" style="background:#f44336;color:white;border:none;padding:5px 10px;border-radius:3px;cursor:pointer;">关闭</button>
                    <button id="show-changelog" style="background:#2196F3;color:white;border:none;padding:5px 10px;border-radius:3px;cursor:pointer;">更新日志</button>
                </div>
            `;

            document.body.appendChild(notification);

            // 关闭通知
            document.getElementById('close-notification').addEventListener('click', () => {
                notification.remove();
                localStorage.setItem('mplusExportToolVersion', SCRIPT_VERSION);
            });

            // 显示更新日志
            document.getElementById('show-changelog').addEventListener('click', () => {
                alert(`导出MPlus物料数据工具更新日志 v${SCRIPT_VERSION}:
1. 修复切换项目导出按钮消失问题
2、修复切换项目后数据没有切换`);
                notification.remove();
                localStorage.setItem('mplusExportToolVersion', SCRIPT_VERSION);
            });
        }
    }

    // 创建符合Element UI风格的按钮
    function createExportButton() {

        // 创建按钮
        const btn = document.createElement('button');
        btn.textContent = '导出安装数据';
        btn.className = 'el-button el-button--primary el-button--small';
        btn.id = 'mplus-export-btn';
        btn.style.marginLeft = '10px';

        // 添加Element UI按钮样式
        const style = document.createElement('style');
        style.textContent = `
            #mplus-export-btn {
                padding: 9px 15px;
                font-size: 12px;
                border-radius: 4px;
                transition: all .1s;
                font-weight: 500;
                color: #FFF;
                background-color: #409EFF;
                border-color: #409EFF;
            }
            #mplus-export-btn:hover {
                background: #66b1ff;
                border-color: #66b1ff;
                color: #FFF;
            }
        `;
        document.head.appendChild(style);

        const hasBtn = document.querySelector("#mplus-export-btn")
        if (hasBtn) return
        // 点击事件处理
        btn.addEventListener('click', function() {
            fetchShopListAndExportData();
        });

        // 尝试找到class为second的元素
        const secondElement = document.querySelector('.footerAction');

        if (secondElement) {
            // 如果找到.footerAction元素,将按钮插入其中
            const container = document.createElement('div');
            container.style.display = 'inline-block';
            container.style.margin = '10px';
            container.appendChild(btn);
            secondElement.appendChild(container);
            console.log('按钮已添加到.footerAction元素');
        } else {
            // 如果没找到.footerAction元素,将按钮固定在页面右下角
            btn.style.position = 'fixed';
            btn.style.bottom = '20px';
            btn.style.right = '20px';
            btn.style.zIndex = '9999';
            document.body.appendChild(btn);
            console.log('未找到.footerAction元素,按钮已添加到body');
        }
    }

    // 从宿主页面获取API基础域名
    function getBaseApiUrl() {
        // 尝试从当前页面获取API域名
        const scripts = document.querySelectorAll('script[src]');
        for (let script of scripts) {
            const src = script.src;
            if (src.includes('pmmsapi')) {
                const url = new URL(src);
                return `${url.protocol}//${url.host}`;
            }
        }
        // 默认使用原域名
        return 'https://mplus.lorealchina.com';
    }

    // 从用户信息获取请求参数
    function getUserInfoParams() {
        try {
            // 从sessionStorage获取用户信息
            const userInfo = JSON.parse(sessionStorage.getItem('userInfo'));
            if (!userInfo) {
                throw new Error('无法获取用户信息');
            }

            // 解构用户信息
            const {
                workSystem,
                roleId: permissionRoleId,
                attributes: {
                    supplierId
                }
            } = userInfo;

            return {
                workSystem,
                permissionRoleId,
                supplierId
            };
        } catch (e) {
            console.error('获取用户信息失败:', e);
            return {
                workSystem: "LD",
                permissionRoleId: "985f492c-81b5-4cec-8810-c04f885db80c",
                supplierId: "1d3323a1-80a3-494e-84d2-4e6b176f1d0f",
                error: e.message
            };
        }
    }

    // 从sessionStorage获取access_token
    function getAuthToken() {
        try {
            // 优先从sessionStorage获取access_token
            const accessToken = sessionStorage.getItem('access_token');
            if (accessToken) return accessToken;

            // 备用方案
            const metaToken = document.querySelector('meta[name="auth-token"]');
            if (metaToken) return metaToken.content;

            const storedToken = localStorage.getItem('authToken');
            if (storedToken) return storedToken;

            const cookieMatch = document.cookie.match(/auth_token=([^;]+)/);
            if (cookieMatch) return cookieMatch[1];

        } catch (e) {
            console.error('获取token失败:', e);
        }

        // 默认token
        return "3845a1c1-4e77-4cfc-9816-6e507736a889";
    }

    // 获取店铺列表
    function fetchShopList(page, size, list) {
        return new Promise((resolve, reject) => {
            const data = new URL(location.href)
            const procurementRequestId = data.searchParams.get("procurementRequestId")
            const tabType = data.searchParams.get("tabType")
            const baseUrl = getBaseApiUrl();
            let apiPath = "/pmmsapi/pmms-new-launch-bff/supplier/fetchReportInstallationCounter"
            if (tabType) {
                apiPath = "/pmmsapi/pmms-new-launch-bff/supplier/fetchInstallationCounter";
            }
            const userParams = getUserInfoParams();
            console.log(procurementRequestId)

            const requestData = {
                "requestId": crypto.randomUUID(),
                "procurementId": procurementRequestId,
                "workflowId": "WNL252RNER21",
                page,
                size, // 获取更多店铺
                "supplierId": userParams.supplierId,
                "timestamp": 0,
                "apiVersion": "string",
                "counterName": "",
                "channelIdList": [],
                "permissionRoleId": userParams.permissionRoleId,
                "workSystem": userParams.workSystem
            };

            const authToken = getAuthToken();
            const headers = {
                "Content-Type": "application/json",
                "token": authToken,
                "authorization": `Bearer ${authToken}`
            };

            GM_xmlhttpRequest({
                method: "POST",
                url: baseUrl + apiPath,
                headers: headers,
                data: JSON.stringify(requestData),
                responseType: "json",
                onload: function(response) {
                    console.log(888888, response)
                    if (response.status === 200) {
                        const data = JSON.parse(response.responseText);
                        if (data && Array.isArray(data.data.dataList)) {
                            list = list.concat(data.data.dataList)
                            if (data.data.dataList.length == size) {
                                page++
                                resolve(fetchShopList(page, size, list));
                            } else {
                                resolve(list);
                            }
                        } else {
                            reject(new Error("返回的店铺列表数据格式不正确"));
                        }
                    } else {
                        reject(new Error(`请求店铺列表失败: ${response.statusText}`));
                    }
                },
                onerror: function(error) {
                    reject(new Error(`请求店铺列表出错: ${error}`));
                }
            });
        });
    }

    // 获取灯片数据
    function fetchLightData(counterId, shopName) {
        return new Promise((resolve, reject) => {
            const data = new URL(location.href)
            const procurementRequestId = data.searchParams.get("procurementRequestId")
            console.log(procurementRequestId)

            const baseUrl = getBaseApiUrl();
            const apiPath = "/pmmsapi/pmms-new-launch-bff/supplier/fetchInstallationLight";
            const userParams = getUserInfoParams();

            const requestData = {
                "apiVersion": "string",
                "counterId": counterId,
                "page": 0,
                "procurementId": procurementRequestId,
                "requestId": crypto.randomUUID(),
                "size": 15,
                "timestamp": 0,
                "permissionRoleId": userParams.permissionRoleId,
                "workSystem": userParams.workSystem
            };

            const authToken = getAuthToken();
            const headers = {
                "Content-Type": "application/json",
                "token": authToken,
                "authorization": `Bearer ${authToken}`
            };

            GM_xmlhttpRequest({
                method: "POST",
                url: baseUrl + apiPath,
                headers: headers,
                data: JSON.stringify(requestData),
                onload: function(response) {
                    try {
                        // 1. 检查响应状态
                        if (response.status !== 200) {
                            throw new Error(`请求失败,状态码: ${response.status}`);
                        }

                        // 2. 安全解析JSON
                        let data;
                        try {
                            data = JSON.parse(response.responseText);
                        } catch (e) {
                            throw new Error("响应不是有效的JSON格式");
                        }

                        // 3. 检查数据结构
                        if (!data || typeof data !== 'object') {
                            throw new Error("返回数据不是有效对象");
                        }

                        // 4. 检查data.data是否存在
                        if (!data.data) {
                            console.warn(`店铺 ${shopName} (${counterId}) 没有灯片数据`);
                            resolve([]);
                            return;
                        }

                        // 5. 检查data.data.dataList是否是数组
                        if (!Array.isArray(data.data.dataList)) {
                            if (data.data.dataList === null || data.data.dataList === undefined) {
                                console.warn(`店铺 ${shopName} (${counterId}) 没有灯片数据`);
                                resolve([]);
                                return;
                            }
                            throw new Error("dataList不是数组");
                        }

                        // 6. 添加店铺信息并返回数据
                        const enhancedData = data.data.dataList.map(item => ({
                            ...item,
                            shopId: counterId,
                            shopName: shopName,
                            dataType: "灯片数据" // 添加数据类型标识
                        }));

                        resolve(enhancedData);
                    } catch (error) {
                        reject(new Error(`处理店铺 ${shopName} (${counterId}) 灯片数据失败: ${error.message}`));
                    }
                },
                onerror: function(error) {
                    reject(new Error(`请求店铺 ${shopName} (${counterId}) 灯片数据出错: ${error}`));
                }
            });
        });
    }

    // 获取安装数据
    function fetchInstallationData(counterId) {
        return new Promise((resolve, reject) => {
            const baseUrl = getBaseApiUrl();
            const apiPath = "/pmmsapi/pmms-new-launch-bff/supplier/fetchInstallationNewItem";
            const userParams = getUserInfoParams();
            const data = new URL(location.href)
            const procurementRequestId = data.searchParams.get("procurementRequestId")
            console.log(procurementRequestId)

            const requestData = {
                "apiVersion": "string",
                "brandIdList": [
                    "77faea5b-368f-4efe-acd3-d650f3a06d00",
                    "77faea5b-368f-4efe-acd3-d650f3a06d00"
                ],
                "counterId": counterId,
                "page": 0,
                "procurementId": procurementRequestId,
                "requestId": crypto.randomUUID(),
                "size": 15,
                "timestamp": 0,
                "permissionRoleId": userParams.permissionRoleId,
                "workSystem": userParams.workSystem
            };

            const authToken = getAuthToken();
            const headers = {
                "Content-Type": "application/json",
                "token": authToken,
                "authorization": `Bearer ${authToken}`
            };

            GM_xmlhttpRequest({
                method: "POST",
                url: baseUrl + apiPath,
                headers: headers,
                data: JSON.stringify(requestData),
                responseType: "json",
                onload: function(response) {
                    if (response.status === 200) {
                        const data = JSON.parse(response.responseText);
                        if (data && data.data && data.data.dataList) {
                            resolve(data.data.dataList);
                        } else {
                            reject(new Error("返回的安装数据格式不正确"));
                        }
                    } else {
                        reject(new Error(`请求安装数据失败: ${response.statusText}`));
                    }
                },
                onerror: function(error) {
                    reject(new Error(`请求安装数据出错: ${error}`));
                }
            });
        });
    }

    // 获取店铺列表并导出数据
    async function fetchShopListAndExportData() {
        // 显示加载指示器并初始化进度条
        const loading = showLoading();
        let progressBar = document.createElement('div');
        progressBar.style.cssText = 'width: 80%; height: 10px; background: #eee; border-radius: 5px; margin: 20px auto 0;';
        let progressInner = document.createElement('div');
        progressInner.style.cssText = 'height: 100%; width: 0%; background: #409EFF; border-radius: 5px; transition: width 0.2s;';
        progressBar.appendChild(progressInner);
        let percentText = document.createElement('div');
        percentText.style.cssText = 'text-align:center; font-size:12px; color:#409EFF; margin-top:5px;';
        percentText.textContent = '0%';
        loading.querySelector('div[style*="background: white;"]').appendChild(progressBar);
        loading.querySelector('div[style*="background: white;"]').appendChild(percentText);

        fetchShopList(0, 50, [])
            .then(shopList => {
                console.log('获取到店铺列表:', shopList);
                const totalRequests = shopList.length * 2;
                let finishedRequests = 0;
                // 并发请求所有店铺的安装数据和灯片数据
                const allPromises = shopList.flatMap(shop => [
                    fetchInstallationData(shop.counterId)
                        .then(installationData => installationData.map(item => ({
                            ...item,
                            shopId: shop.counterId,
                            shopName: shop.counterName,
                            shopChannel: shop.channelName,
                            shopCity: shop.cityName
                        })))
                        .catch(error => {
                            console.error(`获取店铺 ${shop.counterName} 安装数据失败:`, error);
                            return [];
                        })
                        .finally(() => {
                            finishedRequests++;
                            const percent = Math.round(finishedRequests / totalRequests * 100);
                            progressInner.style.width = percent + '%';
                            percentText.textContent = percent + '%';
                        }),
                    fetchLightData(shop.counterId, shop.counterName)
                        .then(lightData => lightData.map(item => ({
                            ...item,
                            shopName: shop.counterName,
                            shopChannel: shop.channelName,
                            shopCity: shop.cityName
                        })))
                        .catch(error => {
                            console.error(`获取店铺 ${shop.counterName} 灯片数据失败:`, error);
                            return [];
                        })
                        .finally(() => {
                            finishedRequests++;
                            const percent = Math.round(finishedRequests / totalRequests * 100);
                            progressInner.style.width = percent + '%';
                            percentText.textContent = percent + '%';
                        })
                ]);
                return Promise.all(allPromises)
                    .then( results => {
                        const cloneList = results.flat(Infinity).map(item => ({
                            "包装编号": item.itemCode || "",
                            "点位": item.lightPositionClass,
                            "物料名称": `${item.itemName || item.pictureContent}${item.length && item.width ? (item.length + 'x' + item.width) : ''}`,
                            "物料供应商": "",
                            [item.shopName]: 1
                        }))
                        const list = cloneList.reduce((prev, next) => {
                            prev[next['物料名称']] = {
                                ...prev[next['物料名称']] || {},
                                ...next
                            }
                            return prev
                        }, {})
                        return Object.values(list)
                    }
                );
            })
            .then(allData => {
                loading.remove();
                if (allData.length > 0) {
                    exportToExcel(allData);
                } else {
                    showError("没有获取到任何安装数据");
                }
            })
            .catch(error => {
                loading.remove();
                showError(error.message);
            });
    }

    // 显示错误信息
    function showError(message, data) {
        console.error(message, data);
        alert(`${message}\n请查看控制台获取详细信息`);
    }

    // 导出数据到Excel - 使用FileSaver.js
    function exportToExcel(dataList) {
        try {
            // 验证数据
            if (!Array.isArray(dataList)) {
                throw new Error(`期望数组数据,但得到: ${typeof dataList}`);
            }
            if (dataList.length === 0) {
                throw new Error("没有可导出的数据");
            }

            // 创建工作簿
            const wb = XLSX.utils.book_new();

            // 处理数据 - 确保所有对象都有相同的字段
            const processedData = uniformData(dataList);

            // 将数据转换为工作表
            const ws = XLSX.utils.json_to_sheet(processedData);

            // 将工作表添加到工作簿
            XLSX.utils.book_append_sheet(wb, ws, "安装数据");

            // 生成Excel文件
            const wbout = XLSX.write(wb, {bookType: 'xlsx', type: 'array'});

            // 创建Blob对象
            const blob = new Blob([wbout], {type: 'application/octet-stream'});

            // 使用FileSaver.js保存文件
            const fileName = `MPlus安装数据_${new Date().toISOString().slice(0, 10)}.xlsx`;
            saveAs(blob, fileName);

            console.log('导出成功', processedData);
        } catch (error) {
            showError("导出Excel时出错: " + error.message, dataList);
        }
    }

    // 统一数据字段
    function uniformData(dataList) {
        // 收集所有可能的字段
        const allFields = new Set();
        dataList.forEach(item => {
            Object.keys(item).forEach(key => allFields.add(key));
        });

        // 确保每条数据都有所有字段
        return dataList.map(item => {
            const newItem = {};
            allFields.forEach(field => {
                newItem[field] = item[field] !== undefined ? item[field] : '';
            });
            return newItem;
        });
    }

    // 显示加载指示器
    function showLoading() {
        const loading = document.createElement('div');
        loading.innerHTML = `
            <div style="
                position: fixed;
                top: 0;
                left: 0;
                width: 100%;
                height: 100%;
                background: rgba(0,0,0,0.5);
                display: flex;
                justify-content: center;
                align-items: center;
                z-index: 10000;
            ">
                <div style="
                    background: white;
                    padding: 20px;
                    border-radius: 5px;
                    display: flex;
                    flex-direction: column;
                    align-items: center;
                ">
                    <div class="spinner" style="
                        border: 5px solid #f3f3f3;
                        border-top: 5px solid #3498db;
                        border-radius: 50%;
                        width: 50px;
                        height: 50px;
                        animation: spin 1s linear infinite;
                        margin-bottom: 15px;
                    "></div>
                    <p style="margin: 0;">正在获取数据,请稍候...</p>
                </div>
            </div>
            <style>
                @keyframes spin {
                    0% { transform: rotate(0deg); }
                    100% { transform: rotate(360deg); }
                }
            </style>
        `;
        document.body.appendChild(loading);
        return loading;
    }

    // 使用MutationObserver监听DOM变化
    const observer = new MutationObserver(function(mutations) {
        // 检查是否有新增节点包含.footerAction类
        const hasSecondClass = Array.from(mutations).some(mutation => {
            return Array.from(mutation.addedNodes).some(node => {
                return node.nodeType === 1 && (node.classList.contains('footerAction') ||
                                               node.querySelector('.footerAction') !== null);
            });
        });

        if (hasSecondClass || document.querySelector('.footerAction')) {
            createExportButton();
        }
    });

    // 开始观察整个document.body及其子节点的变化
    observer.observe(document.body, {
        childList: true,
        subtree: true
    });

    // 初始检查
    if (document.querySelector('.footerAction')) {
        createExportButton();
    }

    // 显示更新通知
    setTimeout(showUpdateNotification, 3000);
})();