import React, { useState, useEffect, useMemo, useCallback, useRef } from 'react'; const Icons = { Tire: () => , Sync: () => , Save: () => , Settings: () => , Link: () => , Play: () => , Stop: () => , Search: () => }; export default function App() { const [activeTab, setActiveTab] = useState('products'); const [products, setProducts] = useState([]); const [loading, setLoading] = useState(false); const [bulkLoading, setBulkLoading] = useState(false); const [progress, setProgress] = useState(0); const [logs, setLogs] = useState([]); const [showConfig, setShowConfig] = useState(false); const [searchTerm, setSearchTerm] = useState(''); const [strategy, setStrategy] = useState({ type: 'undercut', amount: 10000 }); const abortRef = useRef(false); const [config, setConfig] = useState(() => { const saved = localStorage.getItem('azki_config'); return saved ? JSON.parse(saved) : { url: 'https://azkitire.com', ck: 'ck_0ba7cf15f93aa51a610568a3ff2b1d6d909239dd', cs: 'cs_7c36c140f806894f9e165670273ace2a1fba12a7', scraper: 'http://139.162.172.61:7860/api/fetch', proxy: 'https://azkitre-torob.m-tavazoa.workers.dev', requestMode: 'direct' }; }); const addLog = useCallback((msg, type = 'info') => { const time = new Date().toLocaleTimeString('fa-IR'); setLogs(prev => [{ msg, type, time }, ...prev].slice(0, 150)); }, []); const sendRequest = async (targetUrl, options = {}, isWoo = true) => { const { requestMode, proxy, ck, cs } = config; let finalUrl = targetUrl; const headers = { 'Content-Type': 'application/json', ...options.headers }; if (isWoo) headers['Authorization'] = 'Basic ' + btoa(`${ck}:${cs}`); if (requestMode === 'proxy' && proxy) { finalUrl = `${proxy.replace(/\/$/, '')}/?url=${encodeURIComponent(targetUrl)}`; } try { const fetchOptions = { ...options, headers, body: options.method !== 'GET' ? options.body : undefined }; const response = await fetch(finalUrl, fetchOptions); if (!response.ok) { const errText = await response.text(); let msg = response.statusText; try { const json = JSON.parse(errText); msg = json.message || json.error || msg; } catch(e) {} throw new Error(msg || `Status ${response.status}`); } return await response.json(); } catch (e) { throw e; } }; const loadAll = async () => { setLoading(true); addLog(`در حال فراخوانی لیست محصولات...`, 'info'); try { let allFetched = []; let page = 1; let keepGoing = true; while (keepGoing) { const data = await sendRequest(`${config.url.replace(/\/$/, "")}/wp-json/wc/v3/products?per_page=100&page=${page}&status=publish`); if (data && data.length > 0) { allFetched = [...allFetched, ...data]; if (data.length < 100) keepGoing = false; else page++; } else keepGoing = false; if (page > 35) break; } const mapped = allFetched.map(p => { // بازیابی هوشمند قیمت و تامین کننده از تمام کلیدهای احتمالی const torobLinkMeta = p.meta_data.find(m => m.key === '_torob_link')?.value || ''; const priceMeta = p.meta_data.find(m => m.key === '_torob_last_price')?.value || ''; // چک کردن چندین کلید متای احتمالی برای نام فروشنده const sellerMeta = p.meta_data.find(m => m.key === 'torob_seller_name')?.value || p.meta_data.find(m => m.key === '_torob_supplier')?.value || ''; const sellerAttr = p.attributes.find(a => a.name === 'تامینکننده')?.options[0] || ''; return { id: p.id, name: p.name, image: p.images[0]?.src || '', stock: p.stock_status, price: p.regular_price || '0', torobLink: torobLinkMeta, originalTorobLink: torobLinkMeta, torobPrice: priceMeta, torobSupplier: sellerMeta || sellerAttr, tireCount: p.attributes.find(a => a.name === 'تعداد حلقه')?.options[0] || '', allAttributes: p.attributes || [], syncStatus: 'idle' }; }); setProducts(mapped); addLog(`موفقیت: مجموعاً ${mapped.length} محصول بارگذاری شد.`, 'success'); } catch (e) { addLog(`خطا در لود محصولات: ${e.message}`, 'error'); } finally { setLoading(false); } }; const saveToSite = async (p, isManual = false) => { const cleanPrice = String(p.price).replace(/[^\d]/g, ''); // ۱. آمادهسازی متا دیتا (فقط اگر مقدار داشته باشند ارسال شوند تا دیتای قبلی پاک نشود) let metaData = []; if (p.torobLink) metaData.push({ key: '_torob_link', value: p.torobLink }); if (p.torobPrice) metaData.push({ key: '_torob_last_price', value: String(p.torobPrice).replace(/[^\d]/g, '') }); if (p.torobSupplier) metaData.push({ key: 'torob_seller_name', value: p.torobSupplier }); // ۲. مدیریت ویژگیها let currentAttrs = p.allAttributes.map(attr => ({ id: attr.id, name: attr.name, position: attr.position, visible: attr.visible, variation: attr.variation, options: attr.options })); if (p.torobSupplier && p.torobSupplier.trim() !== '') { const suppIdx = currentAttrs.findIndex(a => a.name === 'تامینکننده'); const suppAttr = { name: 'تامینکننده', visible: false, variation: false, options: [p.torobSupplier] }; if (suppIdx > -1) currentAttrs[suppIdx] = suppAttr; else currentAttrs.push(suppAttr); } if (isManual && p.tireCount && p.tireCount.trim() !== '') { const tireIdx = currentAttrs.findIndex(a => a.name === 'تعداد حلقه'); const tireAttr = { name: 'تعداد حلقه', visible: true, variation: false, options: [p.tireCount] }; if (tireIdx > -1) currentAttrs[tireIdx] = tireAttr; else currentAttrs.push(tireAttr); } const payload = { meta_data: metaData, attributes: currentAttrs }; if (cleanPrice && cleanPrice !== '0' && cleanPrice !== '') { payload.regular_price = cleanPrice; } try { await sendRequest(`${config.url.replace(/\/$/, "")}/wp-json/wc/v3/products/${p.id}`, { method: 'PUT', body: JSON.stringify(payload) }); setProducts(prev => prev.map(x => x.id === p.id ? { ...x, originalTorobLink: p.torobLink } : x)); return true; } catch (e) { addLog(`خطای ووکامرس برای ${p.id}: ${e.message}`, 'error'); return false; } }; const syncAndApply = async (p) => { if (!p.torobLink || p.torobLink.trim() === "") { addLog(`${p.name}: لینک ترب ندارد.`, 'error'); return { status: 'no-link' }; } setProducts(prev => prev.map(x => x.id === p.id ? { ...x, syncStatus: 'loading' } : x)); addLog(`استعلام قیمت: ${p.name}...`, 'info'); try { const data = await sendRequest(config.scraper, { method: 'POST', body: JSON.stringify({ url: p.torobLink }) }, false); if (!data.success) { if (data.message && (data.message.includes('490') || data.message.includes('500') || data.message.includes('timeout'))) { return { status: 'blocked', message: data.message }; } throw new Error(data.message || "خطا در اسکرپ"); } const tPrice = parseInt(String(data.current_price).replace(/[^\d]/g, '')) || 0; let newPrice = 0; if (tPrice > 0) { newPrice = strategy.type === 'undercut' ? tPrice - strategy.amount : tPrice; newPrice = newPrice > 0 ? newPrice : 0; } const updated = { ...p, torobPrice: tPrice === 0 ? 'ناموجود' : String(data.current_price).replace(/\B(?=(\d{3})+(?!\d))/g, ","), torobSupplier: data.supplier, price: newPrice === 0 ? p.price : String(newPrice), syncStatus: 'success' }; setProducts(prev => prev.map(x => x.id === p.id ? updated : x)); const isSaved = await saveToSite(updated, false); if (isSaved) { addLog(`${p.name}: بروزرسانی موفق.`, 'success'); return { status: 'success' }; } else throw new Error("خطا در ثبت روی سایت"); } catch (e) { addLog(`${p.name}: عملیات ناموفق - ${e.message}`, 'error'); setProducts(prev => prev.map(x => x.id === p.id ? { ...x, syncStatus: 'error' } : x)); return { status: 'fail' }; } }; const startBulkSync = async () => { abortRef.current = false; const targets = products.filter(p => p.torobLink && p.torobLink.trim() !== ""); if (targets.length === 0) return addLog("محصولی با لینک یافت نشد.", "error"); setBulkLoading(true); setProgress(0); addLog(`اتوماسیون آغاز شد...`, 'info'); for (let i = 0; i < targets.length; i++) { if (abortRef.current) break; const result = await syncAndApply(targets[i]); if (result.status === 'blocked') { addLog(`بلاک/خطای موقت از ترب. ۲ دقیقه استراحت...`, "error"); setProducts(prev => prev.map(x => x.id === targets[i].id ? { ...x, syncStatus: 'idle' } : x)); await new Promise(r => setTimeout(r, 120000)); i--; // تلاش مجدد continue; } setProgress(Math.round(((i + 1) / targets.length) * 100)); const waitTime = Math.floor(Math.random() * 5000) + 7500; await new Promise(r => setTimeout(r, waitTime)); } setBulkLoading(false); addLog(`پایان اتوماسیون همگانی قیمت.`, 'success'); }; const bulkSaveLinks = async () => { abortRef.current = false; const targets = products.filter(p => p.torobLink !== p.originalTorobLink); if (targets.length === 0) return addLog("تغییر لینکی برای ثبت یافت نشد.", "info"); setBulkLoading(true); setProgress(0); addLog(`شروع ثبت لینکهای جدید (${targets.length} مورد)...`, 'info'); for (let i = 0; i < targets.length; i++) { if (abortRef.current) break; const p = targets[i]; const success = await saveToSite(p, false); if (success) addLog(`${p.name}: لینک ثبت شد.`, 'success'); setProgress(Math.round(((i + 1) / targets.length) * 100)); await new Promise(r => setTimeout(r, 600)); } setBulkLoading(false); addLog(`پایان ثبت همگانی لینکها.`, 'success'); }; const filtered = useMemo(() => products.filter(p => p.name.toLowerCase().includes(searchTerm.toLowerCase())), [products, searchTerm] ); return (
Automated Scraper Engine
| کد | محصول و لینک ترب | تعداد حلقه | قیمت سایت | قیمت ترب | تأمینکننده | عملیات |
|---|---|---|---|---|---|---|
| {p.id} |
{p.name}
setProducts(prev => prev.map(x => x.id === p.id ? { ...x, torobLink: e.target.value } : x))} className="w-full bg-transparent text-[9px] outline-none font-mono text-indigo-400 border-b border-transparent focus:border-indigo-200" dir="ltr" />
|
setProducts(prev => prev.map(x => x.id === p.id ? { ...x, price: e.target.value } : x))} className="w-28 text-center bg-white border border-slate-200 rounded-xl py-2 outline-none focus:border-indigo-400 shadow-inner" /> |
{p.torobPrice || '---'}
|
{p.torobSupplier || '---'}
|
|