SOURCE

// ==UserScript==
// @name         Abing 报表抓取 (v37.0 核心逻辑终极修正版)
// @namespace    http://tampermonkey.net/
// @version      37.0
// @description  修复报表数据引用层级错误(彻底解决数据0问题);查询面板补全手机号显示;保留严格URL与自动监控
// @author       Senior Engineer
// @match        *://*.abingxin.cn/*
// @grant        GM_xmlhttpRequest
// @grant        GM_registerMenuCommand
// @grant        GM_setClipboard
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        unsafeWindow
// @run-at       document-end
// ==/UserScript==

(function () {
    'use strict';

    const nativeIDB = unsafeWindow.indexedDB || window.indexedDB;
    const DB_NAME = 'AbingReportDB';
    const DB_VERSION = 3;

    if (!nativeIDB) { alert("不支持 IndexedDB"); return; }

    const toast = (msg, color='green') => {
        const d = document.createElement('div');
        d.style.cssText = `position:fixed;top:10px;right:10px;background:${color};color:white;padding:8px 15px;z-index:2147483647;border-radius:4px;box-shadow:0 2px 10px rgba(0,0,0,0.2);font-size:13px;`;
        d.innerText = msg;
        document.body.appendChild(d);
        setTimeout(() => d.remove(), 3000);
    };

    // ==========================================
    // 1. 数据库模块
    // ==========================================
    const DB = {
        db: null,
        init: function() {
            return new Promise((resolve, reject) => {
                if (this.db) return resolve(this.db);
                const req = nativeIDB.open(DB_NAME, DB_VERSION);
                req.onupgradeneeded = (e) => {
                    const db = e.target.result;
                    if (!db.objectStoreNames.contains('orders')) {
                        const store = db.createObjectStore('orders', { keyPath: 'orderId' });
                        store.createIndex('date', 'createTimeRaw', { unique: false });
                    }
                    if (!db.objectStoreNames.contains('dailyReports')) {
                        db.createObjectStore('dailyReports', { keyPath: 'date' });
                    }
                };
                req.onsuccess = (e) => { this.db = e.target.result; resolve(this.db); };
                req.onerror = (e) => reject("DB Error");
            });
        },
        saveOrders: function(list) {
            return new Promise((resolve, reject) => {
                if(!this.db) return reject("DB未连接");
                const tx = this.db.transaction(['orders'], 'readwrite');
                const store = tx.objectStore('orders');
                let count = 0;
                list.forEach(item => { if(item && item.orderId) { store.put(item); count++; } });
                tx.oncomplete = () => resolve(count);
                tx.onerror = (e) => reject(e.target.error);
            });
        },
        updateRemark: function(id, text) {
            return new Promise((resolve, reject) => {
                const tx = this.db.transaction(['orders'], 'readwrite');
                const store = tx.objectStore('orders');
                const req = store.get(id);
                req.onsuccess = () => {
                    const data = req.result;
                    if(data) { data.remark = text; store.put(data); resolve(true); }
                    else reject("未找到订单");
                };
            });
        },
        saveReportData: function(dataObj) {
            return new Promise((resolve, reject) => {
                const tx = this.db.transaction(['dailyReports'], 'readwrite');
                tx.objectStore('dailyReports').put(dataObj);
                tx.oncomplete = () => resolve();
                tx.onerror = (e) => reject(e.target.error);
            });
        },
        getAllOrders: function() {
            return new Promise((resolve, reject) => {
                const tx = this.db.transaction(['orders'], 'readonly');
                const req = tx.objectStore('orders').getAll();
                req.onsuccess = () => resolve(req.result || []);
                req.onerror = () => reject("查询失败");
            });
        },
        getAllReports: function() {
            return new Promise((resolve, reject) => {
                const tx = this.db.transaction(['dailyReports'], 'readonly');
                const req = tx.objectStore('dailyReports').getAll();
                req.onsuccess = () => {
                    const raw = req.result || [];
                    const clean = raw.map(r => r.data ? { date: r.date, ...r.data } : r);
                    resolve(clean);
                };
                req.onerror = () => reject("查询日报失败");
            });
        },
        reset: function() {
            return new Promise((resolve) => {
                if(this.db) this.db.close();
                setTimeout(() => { nativeIDB.deleteDatabase(DB_NAME).onsuccess = resolve; }, 100);
            });
        }
    };

    // ==========================================
    // 2. 配置与工具
    // ==========================================
    const DEFAULT_TECHS = ["候冰", "李天港", "华剑豪", "杨永辉", "李振", "李海亮", "秦阿梅", "赵海珍", "李慧萍", "刘莺歌", "何华贵"];

    const ConfigManager = {
        getTechList: () => GM_getValue('tech_whitelist', DEFAULT_TECHS),
        saveTechList: (list) => GM_setValue('tech_whitelist', list),
        getShopId: () => GM_getValue('shop_id', '12'),
        saveShopId: (id) => GM_setValue('shop_id', id)
    };

    const Utils = {
        cleanName: (str) => str ? str.replace(/\s+/g, '').replace(/[\((].*?[\))]/g, '') : "",
        cleanOrderTech: (str) => str ? str.replace(/共享|[12]|\s/g, '').replace(/[\((].*?[\))]/g, '') : "",
        parseMoney: (str) => {
            if (!str) return 0;
            const clean = str.replace(/[¥¥,\s]/g, '');
            const match = clean.match(/[-]?\d+(\.\d+)?/);
            return match ? parseFloat(match[0]) : 0;
        },
        parseIntVal: (str) => parseInt(str?.replace(/[,]/g, '') || 0, 10),
        parsePercent: (str) => parseFloat(str?.replace(/[%,\s]/g, '') || 0),
        getLogicalDate: () => {
            const d = new Date();
            if (d.getHours() < 3) d.setDate(d.getDate() - 1);
            return d;
        },
        getCurrentMonthRange: () => {
            const now = new Date();
            const start = new Date(now.getFullYear(), now.getMonth(), 1);
            const end = new Date(now.getFullYear(), now.getMonth() + 1, 0);
            const fmt = d => `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`;
            return { start: fmt(start), end: fmt(end) };
        }
    };

    const Network = {
        get: (url) => new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method: "GET", url,
                headers: { "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Upgrade-Insecure-Requests": "1" },
                onload: res => res.status === 200 ? resolve(res.responseText) : reject("HTTP " + res.status),
                onerror: err => reject("Net Error")
            });
        })
    };

    const TaskManager = {
        getTasks: () => {
            const d = Utils.getLogicalDate();
            const d1 = new Date(d.getFullYear(), d.getMonth(), 1);
            const d2 = new Date(d); d2.setDate(d.getDate() + 1);
            const ft = (t) => `${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,'0')}-${String(t.getDate()).padStart(2,'0')}+06%3A00%3A00`;
            const fd = (t) => `${t.getFullYear()}-${String(t.getMonth()+1).padStart(2,'0')}-${String(t.getDate()).padStart(2,'0')}`;
            const BASE = "https://abingxin.cn";
            const SHOP_ID = ConfigManager.getShopId();

            let orderListUrl = "https://abingxin.cn/abing-v3/entity?pageNo=1&pageSize=200&sort=desc&sortBy=id&en=Order&target=order%2ForderList&search_EQ_isShow=&search_EQ_shop.id=10&search_EQ_technician.id=&search_EQ_payStatus=1&search_EQ_status=&search_EQ_ordType=&search_LIKE_phoneNum=&search_EQ_member.openid=&search_EQ_sorts=&search_EQ_orderNum=&search_EQ_payAcc=&search_EQ_isdian=&search_EQ_orderWay=&search_GTE_createDt=__START__&search_LTE_createDt=__END__&search_GTE_hhmm=&search_LT_hhmm=&search_EQ_smdd=";
            orderListUrl = orderListUrl.replace("search_GTE_createDt=__START__", `search_GTE_createDt=${ft(d)}`);
            orderListUrl = orderListUrl.replace("search_LTE_createDt=__END__", `search_LTE_createDt=${ft(d2)}`);

            return [
                { id: 'sale', title: '营业统计', url: `${BASE}/abing-v3/report/saleReport?type=&shopId=${SHOP_ID}&begDt=${ft(d)}&endDt=${ft(d2)}`, parser: Parsers.sale },
                { id: 'work', title: '工时统计', url: `${BASE}/abing-v3/entity?pageNo=1&pageSize=2000&sort=desc&sortBy=dianzhongs&en=TechDayIncomeRep&search_EQ_shopId=${SHOP_ID}&search_EQ_begDt=${ft(d1)}&search_EQ_endDt=${ft(d2)}&search_EQ_techName=&search_EQ_ishb=&search_EQ_isshare=`, parser: Parsers.work },
                { id: 'comm', title: '技师提成', url: `${BASE}/abing-v3/report/orderReport2?sort=desc&sortBy=o.income_amount&shopid=${SHOP_ID}&begdt=${ft(d)}&enddt=${ft(d2)}&tecname=&techType=&btn_submit=`, parser: Parsers.comm },
                { id: 'orders', title: '订单预约列表', url: orderListUrl, parser: Parsers.orders },
                { id: 'fansm', title: '月转卡', url: `${BASE}/abing-v3/entity?pageNo=1&pageSize=100&sort=desc&sortBy=id&en=MemCardFenzRep&search_EQ_shopId=${SHOP_ID}&search_GTE_startTime=${fd(d1)}&search_LTE_startTime=${fd(d2)}`, parser: Parsers.gen },
                { id: 'fansd', title: '日转卡', url: `${BASE}/abing-v3/entity?pageNo=1&pageSize=100&sort=desc&sortBy=id&en=MemCardFenzRep&search_EQ_shopId=${SHOP_ID}&search_GTE_startTime=${fd(d)}&search_LTE_startTime=${fd(d2)}`, parser: Parsers.gen },
                { id: 'new_item', title: '新增订单项目统计', url: `${BASE}/abing-v3/entity?en=OrderItem&search_EQ_payStatus=1&search_GTE_payDt=${ft(d).replace(/\+/g, '%20')}&search_LTE_payDt=${ft(d2).replace(/\+/g, '%20')}&search_EQ_shopId=${SHOP_ID}&search_EQ_item.itemType.id=5`, parser: Parsers.gen }
            ];
        }
    };

    // ==========================================
    // 3. 解析器
    // ==========================================
    const Parsers = {
        gen: (html) => {
            const doc = new DOMParser().parseFromString(html, "text/html");
            const res = [];
            let tables = Array.from(doc.querySelectorAll('table'));
            if (!tables.length) return { data: [] };
            tables.sort((a, b) => b.rows.length - a.rows.length);
            const rows = Array.from(tables[0].rows);
            if(rows.length<1) return {data:[]};
            const headers = Array.from(rows[0].cells).map(c => c.innerText.trim());
            for(let i=1; i<rows.length; i++) {
                const obj = {};
                Array.from(rows[i].cells).forEach((c, idx) => { if(headers[idx]) obj[headers[idx]] = c.innerText.trim(); });
                if(Object.keys(obj).length>0) res.push(obj);
            }
            return { data: res };
        },
        sale: (html) => {
            const doc = new DOMParser().parseFromString(html, "text/html");
            const data = [];
            const parseTbl = (sel) => {
                const t = doc.querySelector(sel + ' table');
                if(!t) return;
                Array.from(t.querySelectorAll('tbody tr')).forEach(tr => {
                    const tds = tr.cells;
                    if(tds.length < 2) return;
                    const name = tds[0].innerText.trim();
                    const val1 = tds[1].innerText.trim();
                    const val2 = tds[2] ? tds[2].innerText.trim() : "";

                    let num = 0;
                    if(name.includes('营业人数') || name.includes('项目总数')) num = Utils.parseMoney(val1);
                    else if(val1.includes('/')) num = Utils.parseIntVal(val1.split('/')[0]);
                    else num = Utils.parseIntVal(val1);
                    data.push({ "项目": name, "数量": num, "金额": Utils.parseMoney(val2) });
                });
            };
            parseTbl('.doc1'); parseTbl('.doc2');
            return { data: data };
        },
        work: (html) => {
            const res = Parsers.gen(html);
            res.data.forEach(r => {
                const k = r['技师']?'技师':(r['姓名']?'姓名':'技师姓名');
                if(r[k]) r[k] = Utils.cleanName(r[k]);
                delete r['门店'];
            });
            return res;
        },
        comm: (html) => {
            const res = Parsers.gen(html);
            res.data.forEach(r => {
                const k = r['技师姓名']?'技师姓名':'技师';
                if(r[k]) r[k] = Utils.cleanName(r[k]);
                delete r['所属分店'];
            });
            return res;
        },
        orders: (html) => {
            const doc = new DOMParser().parseFromString(html, "text/html");
            const res = [];
            const rows = doc.querySelectorAll('#DataTables_Table_0 tbody tr');
            const BASE = "https://abingxin.cn";
            const currentYear = new Date().getFullYear();
            if(!rows || rows.length === 0) return { data: [] };
            rows.forEach(tr => {
                if(tr.cells.length < 7) return;
                const c = tr.cells;
                const phone = c[1].querySelector('.phoneNum')?.getAttribute('data-phone') || "";
                const idLink = c[2].querySelector('a');
                const oid = idLink ? idLink.innerText.trim() : "";
                let url = "";
                if(idLink) {
                    const m = idLink.getAttribute('onclick')?.match(/to_show\('[^']+',\s*'([^']+)'/);
                    if(m) url = BASE + m[1];
                }
                const techText = c[3].innerText;
                const techName = c[3].querySelector('a[onclick*="Technician"]')?.innerText.trim() || "";
                const isDian = techText.includes('点钟') ? "是" : "否";
                let category = "";
                c[3].querySelectorAll('p').forEach(p=>{ if(p.innerText.includes('类别:')) category = p.innerText.replace('类别:','').trim(); });
                const item = c[3].querySelector('a[data-txt]')?.getAttribute('data-txt') || "";
                const amt = parseFloat(c[4].innerText.match(/订单金额:([\d\.]+)/)?.[1] || 0);
                const timeTxt = c[5].innerText;
                let createT = timeTxt.match(/下单时间:([^\n]+)/)?.[1] || "";
                if(createT && !createT.startsWith('20')) createT = `${currentYear}-${createT.trim()}`;
                const servT = timeTxt.match(/服务时间:([^\n]+)/)?.[1] || "";
                const occT = timeTxt.match(/占用时间:([^\n]+)/)?.[1] || "";
                const statusTxt = c[6].innerText;
                const payS = statusTxt.includes('已支付') ? "已支付" : "未支付";
                let revS = "未评";
                c[6].querySelectorAll('p').forEach(p=>{ if(p.innerText.includes('评价状态')) revS = p.innerText.replace('评价状态:','').trim(); });

                res.push({
                    orderId: oid, phone: phone, url: url,
                    techName: Utils.cleanOrderTech(techName), itemName: item, category: category,
                    amount: amt, isDian: isDian, payStatus: payS, reviewStatus: revS,
                    createTimeRaw: createT.trim(), serviceTime: servT.trim(), occupyTime: occT.trim()
                });
            });
            return { data: res };
        },
        generic: (html) => Parsers.gen(html)
    };

    // ==========================================
    // 4. 核心逻辑 (修复:修正变量引用)
    // ==========================================
    const Core = {
        generateReportData: (finalData) => {
            const techs = ConfigManager.getTechList();
console.log('[2] finalData['技师提成']);

            // 修复:直接使用 finalData[...],不带 .data
            const comm = finalData['技师提成'] || [];
            const work = finalData['工时统计'] || [];
            const recMap = {};

            comm.forEach(r => {
                const n = r['技师姓名'] || r['技师'];
                if(n) recMap[n] = Utils.parseIntVal(r['接待人数']);
            });
            work.forEach(r => {
                const n = r['技师'] || r['姓名'];
                r['当天接待人数'] = (n && recMap[n]>=1) ? recMap[n] : 0;
            });

            const date = Utils.getLogicalDate();

            // 修复:直接使用 finalData['营业统计']
            const sale = finalData['营业统计'] || [];
            const getSale = (k) => {
                const f = sale.find(r => r['项目'] && r['项目'].includes(k));
                return f ? { c: f['数量'], a: f['金额'] } : { c:0, a:0 };
            };

            const numCust = getSale('营业人数').c;
            const amtRecharge = getSale('充卡').a;
            const numJingluo = getSale('经络').c;

            let okCount = 0;
            const targetWork = work.filter(r => techs.includes(r['技师']||r['姓名']));
            targetWork.forEach(r => { if(Utils.parsePercent(r['点钟率']) >= 30) okCount++; });
            const rate = techs.length ? ((okCount/techs.length)*100).toFixed(2) : "0.00";

            let fanDay = 0;
            (finalData['日转卡'] || []).forEach(r => fanDay += Utils.parseIntVal(r['转换数量']));

            let fanRateMonth = "0.00";
            const monthList = finalData['月转卡'] || [];
            if (monthList.length > 0) fanRateMonth = monthList[0]['充卡率'] || "0.00";

            let s1=0, s2=0;
            work.forEach(r => {
                if(r['当天接待人数'] >= 1) {
                    const shareStatus = r['是否共享'] || "";
                    if(shareStatus.includes('共享1子')) s1++;
                    if(shareStatus.includes('共享2子')) s2++;
                }
            });

            return {
                date: Utils.getLogicalDate().toISOString().split('T')[0],
                data: { numCust, rate, amtRecharge, fanDay, fanRateMonth, numJingluo, s1, s2 }
            };
        },
        formatReportText: (reportObj) => {
            const d = reportObj.data;
            const dateParts = reportObj.date.split('-');
            const dateStr = `${parseInt(dateParts[1])}月${parseInt(dateParts[2])}日`;
            return `祥盛街店${dateStr}
1:人数: ${d.numCust || 0}
2:截止今日点钟合格率${d.rate || "0.00"}%
3:充值 ${d.amtRecharge || 0}
4:今天新客转卡:${d.fanDay || 0}张,截止今天转卡率:${d.fanRateMonth || "0.00"}
5:经络:   ${d.numJingluo || 0}
共享1:(${d.s1 || 0})
共享2:(${d.s2 || 0} )
6:富贵包调理:0个
7:富贵包体验:0个
8:痛经调理:0个`;
        }
    };

    // ==========================================
    // 5. 自动监控 (22:30后 严谨URL)
    // ==========================================
    const AutoMonitor = {
        timer: null,
        start: () => {
            console.log("[Monitor] 启动自动监控...");
            if(AutoMonitor.timer) clearInterval(AutoMonitor.timer);
            AutoMonitor.timer = setInterval(() => {
                const now = new Date();
                if (now.getHours() === 22 && now.getMinutes() >= 30 || now.getHours() > 22) {
                    AutoMonitor.checkUnpaid();
                }
            }, 60000);
        },
        checkUnpaid: async () => {
            const SHOP_ID = ConfigManager.getShopId();
            const url = `https://abingxin.cn/abing-v3/entity?en=Order&target=order/orderList&search_EQ_payStatus=0&search_EQ_shop.id=${SHOP_ID}`;
            try {
                const html = await Network.get(url);
                const doc = new DOMParser().parseFromString(html, "text/html");
                const rows = doc.querySelectorAll('#DataTables_Table_0 tbody tr');
                let hasData = false;
                if(rows.length > 0) {
                     const firstCell = rows[0].cells[0].innerText;
                     if(!firstCell.includes("没有数据")) hasData = true;
                }
                if(!hasData) {
                    Main.run(true, 'orders');
                }
            } catch(e) { console.error("[Monitor] Error:", e); }
        }
    };
    setTimeout(AutoMonitor.start, 5000);

    // ==========================================
    // 6. UI 界面
    // ==========================================
    GM_addStyle(`
        #ab-p-box { position: fixed; bottom: 20px; right: 20px; width: 320px; background: #fff; border: 1px solid #999; box-shadow: 0 0 15px rgba(0,0,0,0.5); z-index: 2147483647; display: none; padding: 10px; font-size:12px; font-family:sans-serif; border-radius:5px;}
        #ab-p-bar { height: 8px; background: #eee; margin: 10px 0; border-radius:4px; overflow:hidden; }
        #ab-p-in { height: 100%; width: 0%; background: #28a745; transition: width 0.3s; }
        .ab-btn { padding: 5px 10px; cursor: pointer; border: 1px solid #ccc; background: #f8f8f8; margin-right: 5px; border-radius:3px; }
        .ab-mask { position: fixed; top:0; left:0; width:100%; height:100%; background: #f0f2f5; z-index: 2147483647; display: flex; flex-direction: column; font-family: sans-serif; }
        .ab-head { background: #fff; padding: 10px 20px; border-bottom: 1px solid #ddd; display: flex; justify-content: space-between; align-items: center; box-shadow:0 2px 5px rgba(0,0,0,0.05); }
        .ab-body { flex: 1; display: flex; overflow: hidden; padding: 15px; gap: 15px; }
        .ab-side { width: 260px; background: #fff; border: 1px solid #ddd; padding: 15px; overflow-y: auto; border-radius:5px; }
        .ab-main { flex: 1; background: #fff; border: 1px solid #ddd; padding: 0; display: flex; flex-direction: column; border-radius:5px; overflow:hidden; }
        .ab-row { margin-bottom: 15px; }
        .ab-tag { display: inline-block; padding: 3px 8px; border: 1px solid #d9d9d9; margin: 3px; cursor: pointer; font-size: 12px; border-radius:2px; background:#fafafa; }
        .ab-tag.on { background: #e6f7ff; border-color: #1890ff; color: #1890ff; }
        .ab-tag.off { background: #fff1f0; border-color: #ff4d4f; color: #ff4d4f; text-decoration: line-through; }
        table.ab-t { width: 100%; border-collapse: collapse; font-size: 12px; }
        table.ab-t th, table.ab-t td { border-bottom: 1px solid #f0f0f0; padding: 8px 10px; text-align: left; }
        table.ab-t th { background: #fafafa; cursor: pointer; position:sticky; top:0; z-index:1; border-bottom:2px solid #eee; }
        table.ab-t tr:hover { background: #e6f7ff; }
        .ab-modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); background: #fff; padding: 20px; border-radius: 8px; box-shadow: 0 5px 25px rgba(0,0,0,0.2); width: 500px; z-index: 2147483648; }
        .ab-backdrop { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); z-index: 2147483647; }
        input[type=text], input[type=date] { padding:5px; border:1px solid #ccc; border-radius:3px; width:100%; box-sizing:border-box; margin-top:5px; }
    `);

    const UI = {
        orders: [], reports: [], currentView: 'orders',
        filters: { start: "", end: "", kw: "", tags: { techs:{}, phones:{}, projects:{}, statuses:{}, dian:{}, review:{} } },
        sortStack: [{key:'createTimeRaw', dir:-1}],
        lastReport: "", lastData: null,

        openScraper: () => {
            let box = document.getElementById('ab-p-box');
            if(!box) {
                box = document.createElement('div');
                box.id = 'ab-p-box';
                box.innerHTML = `
                    <div style="font-weight:bold;margin-bottom:5px;display:flex;justify-content:space-between"><span>数据抓取中...</span><span id="ab-mini-set" style="cursor:pointer">⚙️</span></div>
                    <div id="ab-p-bar"><div id="ab-p-in"></div></div>
                    <div id="ab-p-msg" style="color:#666">初始化...</div>
                    <div id="ab-p-btn" style="display:none;margin-top:10px;text-align:right">
                        <button class="ab-btn" id="ab-copy-j">复制JSON</button>
                        <button class="ab-btn" id="ab-copy-r" style="background:#1890ff;color:white;border:none">复制报表</button>
                        <button class="ab-btn" onclick="this.parentElement.parentElement.style.display='none'">关闭</button>
                    </div>`;
                document.body.appendChild(box);
                document.getElementById('ab-copy-r').onclick = () => { GM_setClipboard(UI.lastReport); alert("已复制"); };
                document.getElementById('ab-copy-j').onclick = () => { GM_setClipboard(JSON.stringify(UI.lastData)); alert("已复制"); };
                document.getElementById('ab-mini-set').onclick = UI.showConfig;
            }
            box.style.display = 'block';
            document.getElementById('ab-p-btn').style.display = 'none';
            UI.progress(0, "开始...");
        },
        progress: (pct, msg) => {
            const bar = document.getElementById('ab-p-in');
            const txt = document.getElementById('ab-p-msg');
            if(bar && pct!=null) bar.style.width = pct + "%";
            if(txt) txt.innerText = msg;
        },
        finish: (report, data) => {
            UI.lastReport = report;
            UI.lastData = data;
            UI.progress(100, "完成!");
            GM_setClipboard(report);
            document.getElementById('ab-p-btn').style.display = 'block';
            const d = document.createElement('div');
            d.style.cssText = "position:fixed;top:10px;right:10px;background:green;color:white;padding:10px;z-index:9999999;border-radius:5px;";
            d.innerText = "抓取完成,报表已复制";
            document.body.appendChild(d);
            setTimeout(() => d.remove(), 3000);
        },

        openQuery: async () => {
            try {
                Main.run(true, 'orders');
                await DB.init();
                const all = await DB.getAllOrders();
                UI.orders = all;
                UI.reports = await DB.getAllReports();

                const range = Utils.getCurrentMonthRange();
                UI.filters.start = range.start;
                UI.filters.end = range.end;

                UI.renderQuery();
                UI.applyFilter();

                document.onkeydown = (e) => { if(e.key === 'Escape') { const mask = document.querySelector('.ab-mask'); if(mask) mask.remove(); } };
            } catch(e) { alert("打开失败: " + e); }
        },

        renderQuery: () => {
            const old = document.querySelector('.ab-mask');
            if(old) old.remove();
            const div = document.createElement('div');
            div.className = 'ab-mask';
            div.innerHTML = `
                <div class="ab-head">
                    <div style="font-size:18px;font-weight:bold">
                        <button class="ab-btn" id="ab-view-orders" style="background:#1890ff;color:white">订单查询</button>
                        <button class="ab-btn" id="ab-view-reports">日报历史</button>
                    </div>
                    <div><button class="ab-btn" id="ab-conf">⚙️ 配置</button><button class="ab-btn" id="ab-refresh">�� 抓取最新</button><button class="ab-btn" onclick="document.querySelector('.ab-mask').remove()" style="background:#ff4d4f;color:white;border:none">关闭(ESC)</button></div>
                </div>
                <div class="ab-body">
                    <div class="ab-side" id="ab-side-panel">
                        <div class="ab-row"><b>�� 日期</b><br><input type="date" id="d1" value="${UI.filters.start}"> 至 <input type="date" id="d2" value="${UI.filters.end}"></div>
                        <div class="ab-row"><b>�� 搜索</b><br><input type="text" id="kw" placeholder="技师/手机/订单号..."></div>
                        <div class="ab-row"><b>��️ 筛选(右键反选)</b><div id="ab-tags" style="margin-top:5px"></div></div>
                        <button class="ab-btn" id="ab-clear" style="width:100%">重置</button>
                        <div style="margin-top:20px;font-size:13px;line-height:1.6;color:#666">
                            总记录: <span id="ab-total" style="font-weight:bold;color:#333">0</span><br>
                            筛选后: <span id="ab-show" style="font-weight:bold;color:#1890ff">0</span><br>
                            金额: <span id="ab-amt" style="font-weight:bold;color:#f5222d">0</span>
                        </div>
                    </div>
                    <div class="ab-main">
                        <div style="flex:1;overflow:auto" id="ab-table-container"></div>
                    </div>
                </div>`;
            document.body.appendChild(div);

            const btnOrders = document.getElementById('ab-view-orders');
            const btnReports = document.getElementById('ab-view-reports');

            const switchView = (view) => {
                UI.currentView = view;
                if (view === 'orders') {
                    btnOrders.style.cssText = "background:#1890ff;color:white";
                    btnReports.style.cssText = "";
                    document.getElementById('ab-side-panel').style.display = 'block';
                    UI.renderOrderTable();
                } else {
                    btnReports.style.cssText = "background:#1890ff;color:white";
                    btnOrders.style.cssText = "";
                    document.getElementById('ab-side-panel').style.display = 'none';
                    UI.renderReportTable();
                }
            };

            btnOrders.onclick = () => switchView('orders');
            btnReports.onclick = () => switchView('reports');

            document.getElementById('d1').onchange = (e) => { UI.filters.start = e.target.value; UI.currentView==='orders'?UI.applyFilter():UI.renderReportList(); };
            document.getElementById('d2').onchange = (e) => { UI.filters.end = e.target.value; UI.currentView==='orders'?UI.applyFilter():UI.renderReportList(); };
            document.getElementById('kw').oninput = (e) => { UI.filters.kw = e.target.value; UI.applyFilter(); };
            document.getElementById('ab-clear').onclick = () => {
                UI.filters.tags = { techs:{}, phones:{}, projects:{}, statuses:{}, dian:{}, review:{} };
                UI.sortStack = [{key:'createTimeRaw', dir:-1}];
                UI.applyFilter();
            };
            document.getElementById('ab-refresh').onclick = () => {
                const btn = document.getElementById('ab-refresh');
                btn.innerText = "⏳ 更新中...";
                btn.disabled = true;
                Main.run(true, 'orders').then(() => {
                    btn.innerText = "�� 抓取最新";
                    btn.disabled = false;
                });
            };
            document.getElementById('ab-conf').onclick = UI.showConfig;

            UI.renderOrderTable();
        },

        renderOrderTable: () => {
            const container = document.getElementById('ab-table-container');
            // 【UI补全】增加手机号列
            container.innerHTML = `<table class="ab-t"><thead><tr><th k="createTimeRaw">时间</th><th k="orderId">订单号</th><th k="techName">技师</th><th k="isDian">点钟</th><th k="itemName">项目</th><th k="amount">金额</th><th k="payStatus">状态</th><th k="reviewStatus">评价</th><th k="phone">手机</th><th k="remark">备注</th></tr></thead><tbody id="ab-tbody"></tbody></table>`;

            container.querySelectorAll('th').forEach(th => th.onclick = () => {
                const k = th.getAttribute('k');
                const idx = UI.sortStack.findIndex(s => s.key === k);
                let newDir = -1;
                if (idx !== -1) {
                    newDir = UI.sortStack[idx].dir * -1;
                    UI.sortStack.splice(idx, 1);
                }
                UI.sortStack.push({ key: k, dir: newDir });
                UI.renderList();
            });
            UI.applyFilter();
        },

        renderReportTable: () => {
             const container = document.getElementById('ab-table-container');
             UI.reports.sort((a,b) => b.date.localeCompare(a.date));
             let rows = UI.reports.map(r => {
                 const text = Core.formatReportText({data:r, date:r.date});
                 const preview = text.replace(/\n/g, ' ').substring(0, 50) + "...";
                 return `<tr><td>${r.date}</td><td title="${text}">${preview}</td><td><button class="ab-btn" onclick="copyRep('${encodeURIComponent(text)}')">复制</button></td></tr>`;
             }).join('');
             container.innerHTML = `<table class="ab-t"><thead><tr><th>日期</th><th>预览</th><th>操作</th></tr></thead><tbody>${rows}</tbody></table>`;
             window.copyRep = (enc) => { GM_setClipboard(decodeURIComponent(enc)); alert("日报已复制"); };
        },

        applyFilter: () => {
            const f = UI.filters;
            const start = f.start + " 06:00:00";
            const d = new Date(f.end); d.setDate(d.getDate()+1);
            const end = d.toISOString().split('T')[0] + " 06:00:00";
            const kw = f.kw.toLowerCase();
            const check = (val, map) => {
                const keys = Object.keys(map);
                if(!keys.length) return true;
                const inc = keys.filter(k=>map[k]===1);
                const exc = keys.filter(k=>map[k]===-1);
                if(exc.includes(val)) return false;
                if(inc.length && !inc.includes(val)) return false;
                return true;
            };
            UI.viewData = UI.orders.filter(o => {
                if(o.createTimeRaw < start || o.createTimeRaw > end) return false;
                if(kw && !JSON.stringify(o).toLowerCase().includes(kw)) return false;
                return check(o.techName, f.tags.techs) &&
                       check(o.itemName, f.tags.projects) &&
                       check(o.payStatus, f.tags.statuses) &&
                       check(o.isDian, f.tags.dian) &&
                       check(o.reviewStatus, f.tags.review);
            });
            document.getElementById('ab-total').innerText = UI.orders.length;
            document.getElementById('ab-show').innerText = UI.viewData.length;
            document.getElementById('ab-amt').innerText = UI.viewData.reduce((a,b)=>a+(b.amount||0),0).toFixed(2);
            UI.renderTags();
            UI.renderList();
        },

        renderTags: () => {
            const div = document.getElementById('ab-tags');
            div.innerHTML = "";
            const addGroup = (map, label) => {
                Object.keys(map).forEach(k => {
                    const s = map[k];
                    const sp = document.createElement('span');
                    sp.className = `ab-tag ${s===1?'on':'off'}`;
                    sp.innerText = `${label}:${k}`;
                    sp.onclick = () => { if(s===1) map[k]=-1; else delete map[k]; UI.applyFilter(); };
                    sp.oncontextmenu = (e) => { e.preventDefault(); if(s===1) map[k]=-1; else map[k]=1; UI.applyFilter(); };
                    div.appendChild(sp);
                });
            };
            addGroup(UI.filters.tags.techs, '技师');
            addGroup(UI.filters.tags.dian, '点钟');
            addGroup(UI.filters.tags.projects, '项目');
            addGroup(UI.filters.tags.statuses, '状态');
            addGroup(UI.filters.tags.review, '评价');
        },

        renderList: () => {
            const tb = document.getElementById('ab-tbody');
            if(!tb) return;
            tb.innerHTML = "";
            UI.viewData.sort((a,b) => {
                for (let s of UI.sortStack) {
                    const va = a[s.key]||"", vb = b[s.key]||"";
                    if(va === vb) continue;
                    if (typeof va === 'number' && typeof vb === 'number') return s.dir * (va - vb);
                    return s.dir * String(va).localeCompare(String(vb));
                }
                return 0;
            });
            UI.viewData.slice(0, 300).forEach(o => {
                const tr = document.createElement('tr');
                const td = (t, clickType) => {
                    const el = document.createElement('td');
                    el.innerText = t || '-';
                    if(clickType) {
                        el.style.color = "#1890ff"; el.style.cursor = "pointer";
                        el.onclick = (e) => { e.stopPropagation(); UI.filters.tags[clickType][t] = 1; UI.applyFilter(); };
                    }
                    return el;
                };
                tr.appendChild(td(o.createTimeRaw));

                const idTd = document.createElement('td');
                idTd.innerHTML = `<a href="${o.url}" target="_blank" style="color:blue;text-decoration:underline">${o.orderId}</a>`;
                tr.appendChild(idTd);

                tr.appendChild(td(o.techName, 'techs'));
                tr.appendChild(td(o.isDian, 'dian'));
                tr.appendChild(td(o.itemName, 'projects'));
                tr.appendChild(td(o.amount));
                tr.appendChild(td(o.payStatus, 'statuses'));
                tr.appendChild(td(o.reviewStatus, 'review'));
                tr.appendChild(td(o.phone)); // 【UI补全】渲染手机号列

                const remTd = document.createElement('td');
                remTd.innerText = o.remark || '...';
                remTd.style.cursor = 'pointer';
                remTd.onclick = () => UI.editRemark(o);
                tr.appendChild(remTd);

                tb.appendChild(tr);
            });
        },

        editRemark: (o) => {
            const txt = prompt("编辑备注:", o.remark || "");
            if(txt !== null) {
                DB.updateRemark(o.orderId, txt).then(() => {
                    o.remark = txt;
                    UI.renderList();
                });
            }
        },

        showConfig: () => {
            const list = ConfigManager.getTechList();
            const shopId = ConfigManager.getShopId();
            const back = document.createElement('div'); back.className = 'ab-backdrop';
            const box = document.createElement('div'); box.className = 'ab-modal';
            box.innerHTML = `<h3>⚙️ 设置</h3>
                <div style="margin-bottom:10px"><b>店铺ID</b> (默认12)<br><input type="text" id="ab-shop-id" value="${shopId}" style="width:100%"></div>
                <div><b>技师白名单</b><br><textarea id="ab-conf-area" style="width:100%;height:150px;margin-top:5px">${list.join('\n')}</textarea></div>
                <div style="text-align:right;margin-top:10px">
                    <button class="ab-btn" id="ab-conf-save" style="background:#28a745;color:white;border:none">保存设置</button>
                    <button class="ab-btn" onclick="this.closest('.ab-backdrop').remove()">取消</button>
                </div>
                <div style="margin-top:20px;border-top:1px solid #eee;padding-top:10px">
                    <button class="ab-btn" id="ab-db-reset" style="background:red;color:white;width:100%">⚠️ 重置数据库</button>
                </div>`;
            back.appendChild(box); document.body.appendChild(back);

            document.getElementById('ab-conf-save').onclick = () => {
                const txt = document.getElementById('ab-conf-area').value;
                const sid = document.getElementById('ab-shop-id').value.trim();
                const arr = txt.split('\n').map(t=>t.trim()).filter(t=>t);
                ConfigManager.saveTechList(arr);
                ConfigManager.saveShopId(sid);
                alert("设置已保存");
                back.remove();
            };

            document.getElementById('ab-db-reset').onclick = async () => {
                if(confirm("确定清空所有数据?")) {
                    await DB.reset();
                    alert("已重置,请刷新页面");
                    location.reload();
                }
            };
        }
    };

    const Main = {
        run: async (isSilent, target='all') => {
            try {
                if(!isSilent) UI.openScraper();
                else UI.progress(0, "后台静默抓取...");
                await DB.init();
                let tasks = TaskManager.getTasks();

                if(target === 'orders') tasks = tasks.filter(t => t.id === 'orders');

                const finalData = {};
                for(let i=0; i<tasks.length; i++) {
                    const t = tasks[i];
                    if(!isSilent) UI.progress((i/tasks.length)*80, "抓取: " + t.title);
                    try {
                        const html = await Network.get(t.url);
                        const res = t.parser(html);
                        if(res.data) {
                            if(t.id === 'orders') {
                                const c = await DB.saveOrders(res.data);
                                if(!isSilent) UI.progress(null, `入库 ${c} 条`);
                                if(isSilent) {
                                    UI.orders = await DB.getAllOrders();
                                    if(UI.currentView === 'orders') UI.applyFilter();
                                    const d = document.createElement('div');
                                    d.style.cssText = "position:fixed;top:10px;right:10px;background:green;color:white;padding:10px;z-index:9999999;border-radius:5px;";
                                    d.innerText = `后台更新完成:${c} 条数据`;
                                    document.body.appendChild(d);
                                    setTimeout(() => d.remove(), 3000);
                                }
                            }
                            finalData[t.title] = res.data; // 修复点:直接赋值data数组
                        }
                    } catch(e) { console.error(e); }
                    await new Promise(r=>setTimeout(r, 600));
                }

                if(isSilent) return;

                if(finalData['营业统计']) {
                    const reportData = Core.generateReportData(finalData);
                    await DB.saveReportData(reportData);
                    const reportText = Core.formatReportText({data:reportData, date:reportData.date});
                    UI.finish(reportText, finalData);
                } else if(target !== 'all') {
                     UI.progress(100, "订单更新完成");
                     setTimeout(() => {
                         document.getElementById('ab-p-box').style.display = 'none';
                         toast("数据已同步");
                     }, 1000);
                }
            } catch(e) { alert("运行异常: " + e); }
        }
    };

    GM_registerMenuCommand("�� 开始抓取", () => Main.run(false, 'all'));
    GM_registerMenuCommand("�� 打开数据查询面板", async () => { try { await DB.init(); UI.openQuery(); } catch(e) { alert("打开面板失败:" + e); } });

})();
console 命令行工具 X clear

                    
>
console