您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
A standalone script that registers a Lead/Client search tool with the Master Tools window.
// ==UserScript== // @name Pixifi Master Tool - Search // @namespace http://tampermonkey.net/ // @version 1.0 // @description A standalone script that registers a Lead/Client search tool with the Master Tools window. // @match https://www.pixifi.com/admin/* // @license GPL // @grant none // ==/UserScript== (function () { 'use strict'; /** * We'll define our search tool here. We use a function so we can call it later. */ const mySearchTool = { name: '', domainRegex: /https:\/\/www\.pixifi\.com\/admin\/leads\//, // Only render on leads/* pages render(parentContainer) { const searchQueryInput = document.createElement('input'); Object.assign(searchQueryInput.style, { width: '200px', marginBottom: '5px', padding: '5px', border: '1px solid #ccc', borderRadius: '3px', display: 'block' }); searchQueryInput.type = 'text'; searchQueryInput.placeholder = 'Search Query'; const searchButton = document.createElement('button'); Object.assign(searchButton.style, { display: 'block', padding: '5px 10px', backgroundColor: '#007bff', color: '#fff', border: 'none', borderRadius: '3px', cursor: 'pointer', fontWeight: 'bold', textAlign: 'center' }); searchButton.textContent = 'Search'; searchButton.addEventListener('click', () => { const searchQuery = searchQueryInput.value.trim(); if (searchQuery) { searchLeadsAndClients(searchQuery); } else { alert('Please enter a Search Query.'); } }); parentContainer.appendChild(searchQueryInput); parentContainer.appendChild(searchButton); } }; /*************************************************************************/ /* HELPER #1: Show a modal that lists possible results */ /*************************************************************************/ function showSelectionModal(links, type) { const modal = document.createElement('div'); Object.assign(modal.style, { position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%, -50%)', backgroundColor: '#fff', padding: '20px', borderRadius: '10px', boxShadow: '0px 4px 6px rgba(0,0,0,0.3)', zIndex: '2000', fontFamily: 'Arial, sans-serif', maxWidth: '400px', overflowY: 'auto' }); // We'll insert a UL to list the leads/clients modal.innerHTML = ` <h3>Select a ${type}</h3> <ul id="modalListContainer" style="list-style: none; padding: 0; margin: 0;"> ${links .map( (link, index) => `<li style="margin-bottom: 10px;"> <button style="width: 100%; padding: 10px; text-align: left;" data-index="${index}"> ${link.innerText || link.getAttribute('href')} </button> </li>` ) .join('')} </ul> <button id="closeModal" style=" display: block; margin: 20px auto 0; padding: 10px 20px; background-color: #007bff; color: #fff; border: none; border-radius: 5px; cursor: pointer;"> Close </button> `; // If more than 8 items, set the UL to have a fixed max-height and auto-scroll if (links.length > 8) { const listContainer = modal.querySelector('#modalListContainer'); Object.assign(listContainer.style, { maxHeight: '480px', overflowY: 'auto' }); } // Link button click => open the URL in a new tab modal.querySelectorAll('button[data-index]').forEach(button => { button.addEventListener('click', e => { const index = e.target.getAttribute('data-index'); const link = links[index].getAttribute('href'); const absoluteLink = new URL(link, window.location.origin).href; window.open(absoluteLink, '_blank'); }); }); // Close button modal.querySelector('#closeModal').addEventListener('click', () => { document.body.removeChild(modal); }); document.body.appendChild(modal); } /*************************************************************************/ /* HELPER #2: Check if a string might be a phone number, ignoring punctuation */ /*************************************************************************/ function isPhoneNumberLike(query) { // Strip out parentheses, spaces, and dashes const digits = query.replace(/[\(\)\s\-]/g, ''); // A simple check: if it’s 10 or 11 digits, call it a “phone number” return /^\d{10,11}$/.test(digits); } /*************************************************************************/ /* HELPER #3: Generate all standard phone number variants from digits */ /*************************************************************************/ function getPhoneNumberVariants(rawQuery) { // 1) Strip to just digits const digits = rawQuery.replace(/\D/g, ''); // Because we might have 10 or 11 digits, handle that // For example: 10-digit => 1234567890 // or 11-digit => 11234567890 // If 11-digit, we assume the first digit might be a leading country code (1). // You may or may not want that logic. This is just an example. // In this example, we’ll assume we want to handle 10-digit only: // If the user typed 11 digits, but the leading digit is "1," we’ll strip it to 10 for the variants below. let phone10 = digits; if (digits.length === 11 && digits.startsWith('1')) { phone10 = digits.substring(1); } // If it’s not exactly 10 at this point, just return [digits] or handle differently if (phone10.length !== 10) { return [digits]; } // phone10 is now something like "1234567890" // Generate the possible variations. Examples: // 1. (123) 456-7890 // 2. (123) 4567890 // 3. (123)4567890 // 4. 1234567890 // 5. 123 456-7890 // 6. 123 4567890 // 7. 123 456 7890 // ... // You can generate as many as you need: const area = phone10.substring(0, 3); // 123 const prefix = phone10.substring(3, 6); // 456 const line = phone10.substring(6); // 7890 return [ `(${area}) ${prefix}-${line}`, `(${area}) ${prefix}${line}`, `(${area})${prefix}${line}`, `${area}${prefix}${line}`, `${area} ${prefix}-${line}`, `${area} ${prefix}${line}`, `${area} ${prefix} ${line}`, // ...add any other permutations you want ]; } /*************************************************************************/ /* HELPER #4: Make a POST call to lead-search or client-search */ /*************************************************************************/ // We'll extract “getLeads” and “getClients” into separate functions // that simply return the array of DOM <a> elements (or an empty array). async function getLeads(searchQuery) { try { const response = await fetch("https://www.pixifi.com/admin/fn/leads/getLeads/", { headers: { "accept": "*/*", "accept-language": "en-US,en;q=0.9", "content-type": "application/x-www-form-urlencoded; charset=UTF-8", "x-requested-with": "XMLHttpRequest" }, referrer: "https://www.pixifi.com/admin/leads/", referrerPolicy: "strict-origin-when-cross-origin", body: new URLSearchParams({ clientID: "12295", page: 1, section: "id", searchQuery, dir: "D", viewFilter: "all" }).toString(), method: "POST", mode: "cors", credentials: "include" }); if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } const text = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(text, "text/html"); const leadLinks = [...doc.querySelectorAll('a[href^="/admin/leads/"]')] .filter(link => { const txt = link.textContent .replace(/\u00A0/g, ' ') .replace(/\s+/g, ' ') .trim(); return txt.length > 0; }) .filter((link, index, self) => { const href = link.getAttribute('href'); return ( index === self.findIndex(otherLink => otherLink.getAttribute('href') === href) ); }); return leadLinks; } catch (error) { console.error('getLeads Error:', error); // Return an empty array so that the caller can handle “nothing found” return []; } } async function getClients(searchQuery) { try { const response = await fetch("https://www.pixifi.com/admin/fn/clients/getClientListing/", { headers: { "accept": "*/*", "accept-language": "en-US,en;q=0.9", "content-type": "application/x-www-form-urlencoded; charset=UTF-8", "x-requested-with": "XMLHttpRequest" }, referrer: "https://www.pixifi.com/admin/clients/", referrerPolicy: "strict-origin-when-cross-origin", body: new URLSearchParams({ clientID: "12295", page: 1, searchQuery, section: "name", dir: "A", archived: "unarchived", card: "all" }).toString(), method: "POST", mode: "cors", credentials: "include" }); if (!response.ok) { throw new Error(`HTTP error! Status: ${response.status}`); } const text = await response.text(); const parser = new DOMParser(); const doc = parser.parseFromString(text, "text/html"); const clientLinks = [...doc.querySelectorAll('a[href^="/admin/clients/"]')] .filter(link => { const txt = link.textContent .replace(/\u00A0/g, ' ') .replace(/\s+/g, ' ') .trim(); return txt.length > 0; }) .filter(link => !link.getAttribute('href').startsWith('/admin/clients/delete/')) .filter((link, index, self) => { const href = link.getAttribute('href'); return ( index === self.findIndex(otherLink => otherLink.getAttribute('href') === href) ); }); return clientLinks; } catch (error) { console.error('getClients Error:', error); return []; } } /*************************************************************************/ /* MAIN SEARCH FLOW: Try leads, then clients, then phone expansions */ /*************************************************************************/ async function searchLeadsAndClients(searchQuery) { console.log(`Searching leads with query: ${searchQuery}`); const leadLinks = await getLeads(searchQuery); if (leadLinks.length > 0) { showSelectionModal(leadLinks, 'Lead'); return; } console.warn('No leads found, now searching clients...'); const clientLinks = await getClients(searchQuery); if (clientLinks.length > 0) { showSelectionModal(clientLinks, 'Client'); return; } console.warn('No clients found. Checking if the query is phone-like...'); if (isPhoneNumberLike(searchQuery)) { // Generate phone number variants console.log('Query looks like a phone number. Searching all variants in parallel...'); const phoneVariants = getPhoneNumberVariants(searchQuery); // Run leads/clients calls in parallel for each variant // We’ll gather all leads, all clients into a single array. const allFetches = phoneVariants.map(async variant => { const theseLeads = await getLeads(variant); const theseClients = await getClients(variant); return { variant, leads: theseLeads, clients: theseClients }; }); const results = await Promise.all(allFetches); // Flatten out all the leads and all the clients from each variant let combinedLeads = []; let combinedClients = []; results.forEach(r => { if (r.leads.length > 0) { combinedLeads = combinedLeads.concat(r.leads); } if (r.clients.length > 0) { combinedClients = combinedClients.concat(r.clients); } }); // Deduplicate by href const uniqueLeads = deduplicateByHref(combinedLeads); const uniqueClients = deduplicateByHref(combinedClients); // Show leads if any if (uniqueLeads.length > 0) { showSelectionModal(uniqueLeads, 'Lead (Phone Variants)'); } // Show clients if any if (uniqueClients.length > 0) { showSelectionModal(uniqueClients, 'Client (Phone Variants)'); } // If still nothing if (uniqueLeads.length === 0 && uniqueClients.length === 0) { alert('No leads or clients found (even after phone expansions).'); } } else { alert('No leads or clients found. Please refine your search.'); } } function deduplicateByHref(links) { const seen = new Set(); return links.filter(link => { const href = link.getAttribute('href'); if (seen.has(href)) { return false; } seen.add(href); return true; }); } /*************************************************************************/ /* Attempt to register our tool with the MasterTools if it exists */ /*************************************************************************/ const MAX_ATTEMPTS = 10; let attempts = 0; function tryRegisterSearchTool() { if (window.MasterTools && typeof window.MasterTools.registerTool === 'function') { window.MasterTools.registerTool(mySearchTool); } else if (attempts < MAX_ATTEMPTS) { attempts++; setTimeout(tryRegisterSearchTool, 500); } else { console.warn('Master Tools not found. The Search Tool will not be registered.'); } } tryRegisterSearchTool(); })();