// ==UserScript== // @name 二手车摆渡人§精污棒一键估值助手 // @namespace http://tampermonkey.net/ // @version 1.35 // @description 从268V车辆估价详情页提取车型/城市/注册日期,自动填写信息并估值 // @author 268v-che300 // @match https://dp.268v.com/* // @match https://www.che300.com/pinggu* // @grant GM_setValue // @grant GM_getValue // @grant GM_openInTab // @run-at document-idle // ==/UserScript== (function () { 'use strict'; const STORAGE_KEY = 'che300_helper_v2'; const STORAGE_TS = 'che300_helper_v2_ts'; const CHE300_URL = 'https://www.che300.com/pinggu?rt=1'; const IS_268V = /(^|\.)268v\.com$/.test(location.host); const IS_CHE300 = /(^|\.)che300\.com$/.test(location.host); const $ = s => document.querySelector(s); const sleep = ms => new Promise(r => setTimeout(r, ms)); const TAG = '[268→che300]'; if (IS_268V) { function getFieldValue(fieldName) { const spans = document.querySelectorAll('span'); for (const sp of spans) { if ((sp.textContent || '').trim() !== fieldName) continue; const sib = sp.nextElementSibling; if (sib && sib.tagName === 'B') { const v = (sib.textContent || '').trim(); if (v) return v; } } return ''; } function extractInfo() { const info = { brand: '', series: '', model: '', year: '', month: '', city: '', }; const carNameText = getFieldValue('车辆名称'); const modelText = getFieldValue('车辆型号'); if (carNameText) { const parts = carNameText.split(/\s+/); info.brand = parts[0] || ''; info.series = parts.slice(1).join(' '); } if (modelText) { info.model = modelText; if (!info.series) { const s = modelText.replace(/^\d{4}款\s*/, '').trim(); const m = s.match(/^(\S+)\s+(.+)$/); if (m) info.series = m[1]; } } const summary = getFieldValue('车辆概况'); if (summary) { const m = summary.match(/(\d{4})[-/.](\d{1,2})[-/.](\d{1,2})/); if (m) { info.year = m[1]; info.month = m[2]; } } const loc = getFieldValue('车辆所在地'); if (loc) info.city = loc.replace(/(省|市|自治区|特别行政区)$/, '').trim(); return info; } function injectButton() { if (document.getElementById('che300-btn')) return; const titleEl = document.querySelector('.subTitle.mt-24'); if (!titleEl || !(titleEl.textContent || '').includes('车辆估价')) return; const btn = document.createElement('button'); btn.id = 'che300-btn'; btn.textContent = '一键估值 → 车300'; Object.assign(btn.style, { position: 'fixed', top: '30px', right: '230px', zIndex: '99999', padding: '8px 16px', fontSize: '12px', fontWeight: '500', color: '#fff', background: 'linear-gradient(135deg,#ff6a00,#ff3d00)', border: 'none', borderRadius: '4px', boxShadow: '0 2px 8px rgba(255,106,0,.35)', cursor: 'pointer', }); btn.addEventListener('click', () => { const info = extractInfo(); GM_setValue(STORAGE_KEY, info); GM_setValue(STORAGE_TS, Date.now()); GM_openInTab(CHE300_URL, { active: true, insert: true }); }); document.body.appendChild(btn); } let lastInject = 0; const tryInject = () => { const now = Date.now(); if (now - lastInject < 800) return; lastInject = now; injectButton(); }; const obs = new MutationObserver(tryInject); obs.observe(document.body, { childList: true, subtree: true }); setTimeout(tryInject, 1000); setTimeout(tryInject, 3000); } if (IS_CHE300) { function waitForEl(sel, timeout) { timeout = timeout || 15000; return new Promise((resolve, reject) => { const start = Date.now(); const check = () => { const el = $(sel); if (el) return resolve(el); if (Date.now() - start > timeout) return reject(new Error('timeout: ' + sel)); setTimeout(check, 150); }; check(); }); } function normStr(s) { return (s || '').replace(/\s+/g, ' ').trim(); } function cleanBrand(s) { return normStr(s).replace(/(汽车|集团|股份|汽车销售|汽车有限公司)$/, '').trim(); } function cleanModel(s) { return normStr(s).replace(/版$/, '').replace(/\s+/g, ' ').trim(); } function findMatch(ps, text, exact) { const t = normStr(text); for (const p of ps) { const pt = normStr(p.textContent); if (exact ? pt === t : pt.includes(t)) return p; } return null; } function findBrand(ps, text) { const t = cleanBrand(text); let best = null, bestScore = 0; for (const p of ps) { const pt = normStr(p.textContent); const pc = cleanBrand(pt); if (pt === text || pc === t) return p; if (pc.includes(t) || t.includes(pc)) { const score = Math.min(pc.length, t.length) / Math.max(pc.length, t.length); if (score > bestScore) { bestScore = score; best = p; } } } return best; } function findModel(ps, text) { const t = cleanModel(text); let best = null, bestScore = 0; for (const p of ps) { const pt = normStr(p.textContent); const pc = cleanModel(pt); if (pt === text || pc === t) return p; if (pc.includes(t) || t.includes(pc)) { const score = Math.min(pc.length, t.length) / Math.max(pc.length, t.length); if (score > bestScore) { bestScore = score; best = p; } } } return best; } async function selectModel(brand, series, model) { const b = normStr(brand); const s = normStr(series); const m = normStr(model); $('#select1').click(); await sleep(400); const brandUl = $('.ucarselecttype_pinpaibottom_ul.brand'); if (!brandUl) throw new Error('品牌列表未出现'); const brandP = findBrand(brandUl.querySelectorAll('p'), b); if (!brandP) throw new Error('品牌未匹配: ' + b); brandP.click(); await sleep(500); const seriesUl = $('.ucarselecttype_pinpaibottom_ul.series'); if (!seriesUl) throw new Error('车系列表未出现'); const seriesP = findMatch(seriesUl.querySelectorAll('p'), s, false); if (!seriesP) throw new Error('车系未匹配: ' + s); seriesP.click(); await sleep(500); const modelUl = $('.ucarselecttype_pinpaibottom_ul.simple'); if (!modelUl) throw new Error('车型列表未出现'); const mps = modelUl.querySelectorAll('p'); let mp = findModel(mps, m); if (!mp) { const m2 = m.replace(/^\d{4}款\s*/, ''); mp = findModel(mps, m2); } if (!mp) throw new Error('车型未匹配: ' + m); mp.click(); } async function selectYearMonth(year, month) { $('#select4').click(); await sleep(400); const yUl = $('.ucarselecttype_pinpaibottom_ul.years'); if (!yUl || !yUl.querySelectorAll('p').length) throw new Error('年份列表为空(可能未选车型)'); const yp = findMatch(yUl.querySelectorAll('p'), year + '年', true); if (!yp) throw new Error('年份未匹配: ' + year + '年'); yp.click(); await sleep(400); const m = String(month).padStart(2, '0') + '月'; const mUl = $('.ucarselecttype_pinpaibottom_ul.months'); if (!mUl) throw new Error('月份列表未出现'); const mp = findMatch(mUl.querySelectorAll('p'), m, true); if (!mp) throw new Error('月份未匹配: ' + m); mp.click(); await sleep(300); } const CITY2PROV = { 北京: '北京', 上海: '上海', 天津: '天津', 重庆: '重庆', 石家庄: '河北', 唐山: '河北', 秦皇岛: '河北', 邯郸: '河北', 邢台: '河北', 保定: '河北', 张家口: '河北', 承德: '河北', 沧州: '河北', 廊坊: '河北', 衡水: '河北', 太原: '山西', 大同: '山西', 阳泉: '山西', 长治: '山西', 晋城: '山西', 朔州: '山西', 晋中: '山西', 运城: '山西', 忻州: '山西', 临汾: '山西', 吕梁: '山西', 呼和浩特: '内蒙古', 包头: '内蒙古', 乌海: '内蒙古', 赤峰: '内蒙古', 通辽: '内蒙古', 鄂尔多斯: '内蒙古', 呼伦贝尔: '内蒙古', 巴彦淖尔: '内蒙古', 乌兰察布: '内蒙古', 兴安盟: '内蒙古', 锡林郭勒盟: '内蒙古', 阿拉善盟: '内蒙古', 沈阳: '辽宁', 大连: '辽宁', 鞍山: '辽宁', 抚顺: '辽宁', 本溪: '辽宁', 丹东: '辽宁', 锦州: '辽宁', 营口: '辽宁', 阜新: '辽宁', 辽阳: '辽宁', 盘锦: '辽宁', 铁岭: '辽宁', 朝阳: '辽宁', 葫芦岛: '辽宁', 长春: '吉林', 吉林市: '吉林', 四平: '吉林', 辽源: '吉林', 通化: '吉林', 白山: '吉林', 松原: '吉林', 白城: '吉林', 延边: '吉林', 哈尔滨: '黑龙江', 齐齐哈尔: '黑龙江', 鸡西: '黑龙江', 鹤岗: '黑龙江', 双鸭山: '黑龙江', 大庆: '黑龙江', 伊春: '黑龙江', 佳木斯: '黑龙江', 七台河: '黑龙江', 牡丹江: '黑龙江', 黑河: '黑龙江', 绥化: '黑龙江', 大兴安岭: '黑龙江', 南京: '江苏', 无锡: '江苏', 徐州: '江苏', 常州: '江苏', 苏州: '江苏', 南通: '江苏', 连云港: '江苏', 淮安: '江苏', 盐城: '江苏', 扬州: '江苏', 镇江: '江苏', 泰州: '江苏', 宿迁: '江苏', 杭州: '浙江', 宁波: '浙江', 温州: '浙江', 嘉兴: '浙江', 湖州: '浙江', 绍兴: '浙江', 金华: '浙江', 衢州: '浙江', 舟山: '浙江', 台州: '浙江', 丽水: '浙江', 合肥: '安徽', 芜湖: '安徽', 蚌埠: '安徽', 淮南: '安徽', 马鞍山: '安徽', 淮北: '安徽', 铜陵: '安徽', 安庆: '安徽', 黄山: '安徽', 滁州: '安徽', 阜阳: '安徽', 宿州: '安徽', 六安: '安徽', 亳州: '安徽', 池州: '安徽', 宣城: '安徽', 福州: '福建', 厦门: '福建', 莆田: '福建', 三明: '福建', 泉州: '福建', 漳州: '福建', 南平: '福建', 龙岩: '福建', 宁德: '福建', 南昌: '江西', 景德镇: '江西', 萍乡: '江西', 九江: '江西', 新余: '江西', 鹰潭: '江西', 赣州: '江西', 吉安: '江西', 宜春: '江西', 抚州: '江西', 上饶: '江西', 济南: '山东', 青岛: '山东', 淄博: '山东', 枣庄: '山东', 东营: '山东', 烟台: '山东', 潍坊: '山东', 济宁: '山东', 泰安: '山东', 威海: '山东', 日照: '山东', 临沂: '山东', 德州: '山东', 聊城: '山东', 滨州: '山东', 菏泽: '山东', 郑州: '河南', 开封: '河南', 洛阳: '河南', 平顶山: '河南', 安阳: '河南', 鹤壁: '河南', 新乡: '河南', 焦作: '河南', 濮阳: '河南', 许昌: '河南', 漯河: '河南', 三门峡: '河南', 南阳: '河南', 商丘: '河南', 信阳: '河南', 周口: '河南', 驻马店: '河南', 济源: '河南', 武汉: '湖北', 黄石: '湖北', 十堰: '湖北', 宜昌: '湖北', 襄阳: '湖北', 鄂州: '湖北', 荆门: '湖北', 孝感: '湖北', 荆州: '湖北', 黄冈: '湖北', 咸宁: '湖北', 随州: '湖北', 恩施: '湖北', 长沙: '湖南', 株洲: '湖南', 湘潭: '湖南', 衡阳: '湖南', 邵阳: '湖南', 岳阳: '湖南', 常德: '湖南', 张家界: '湖南', 益阳: '湖南', 郴州: '湖南', 永州: '湖南', 怀化: '湖南', 娄底: '湖南', 湘西: '湖南', 广州: '广东', 韶关: '广东', 深圳: '广东', 珠海: '广东', 汕头: '广东', 佛山: '广东', 江门: '广东', 湛江: '广东', 茂名: '广东', 肇庆: '广东', 惠州: '广东', 梅州: '广东', 汕尾: '广东', 河源: '广东', 阳江: '广东', 清远: '广东', 东莞: '广东', 中山: '广东', 潮州: '广东', 揭阳: '广东', 云浮: '广东', 南宁: '广西', 柳州: '广西', 桂林: '广西', 梧州: '广西', 北海: '广西', 防城港: '广西', 钦州: '广西', 贵港: '广西', 玉林: '广西', 百色: '广西', 贺州: '广西', 河池: '广西', 来宾: '广西', 崇左: '广西', 海口: '海南', 三亚: '海南', 三沙: '海南', 儋州: '海南', 成都: '四川', 自贡: '四川', 攀枝花: '四川', 泸州: '四川', 德阳: '四川', 绵阳: '四川', 广元: '四川', 遂宁: '四川', 内江: '四川', 乐山: '四川', 南充: '四川', 眉山: '四川', 宜宾: '四川', 广安: '四川', 达州: '四川', 雅安: '四川', 巴中: '四川', 资阳: '四川', 阿坝: '四川', 甘孜: '四川', 凉山: '四川', 贵阳: '贵州', 六盘水: '贵州', 遵义: '贵州', 安顺: '贵州', 毕节: '贵州', 铜仁: '贵州', 黔西南: '贵州', 黔东南: '贵州', 黔南: '贵州', 昆明: '云南', 曲靖: '云南', 玉溪: '云南', 保山: '云南', 昭通: '云南', 丽江: '云南', 普洱: '云南', 临沧: '云南', 楚雄: '云南', 红河: '云南', 文山: '云南', 西双版纳: '云南', 大理: '云南', 德宏: '云南', 怒江: '云南', 迪庆: '云南', 拉萨: '西藏', 日喀则: '西藏', 昌都: '西藏', 林芝: '西藏', 山南: '西藏', 那曲: '西藏', 阿里: '西藏', 西安: '陕西', 铜川: '陕西', 宝鸡: '陕西', 咸阳: '陕西', 渭南: '陕西', 延安: '陕西', 汉中: '陕西', 榆林: '陕西', 安康: '陕西', 商洛: '陕西', 兰州: '甘肃', 嘉峪关: '甘肃', 金昌: '甘肃', 白银: '甘肃', 天水: '甘肃', 武威: '甘肃', 张掖: '甘肃', 平凉: '甘肃', 酒泉: '甘肃', 庆阳: '甘肃', 定西: '甘肃', 陇南: '甘肃', 临夏: '甘肃', 甘南: '甘肃', 西宁: '青海', 海东: '青海', 海北: '青海', 黄南: '青海', 海南州: '青海', 果洛: '青海', 玉树: '青海', 海西: '青海', 银川: '宁夏', 石嘴山: '宁夏', 吴忠: '宁夏', 固原: '宁夏', 中卫: '宁夏', 乌鲁木齐: '新疆', 克拉玛依: '新疆', 吐鲁番: '新疆', 哈密: '新疆', 昌吉: '新疆', 博尔塔拉: '新疆', 巴音郭楞: '新疆', 阿克苏: '新疆', 克孜勒苏: '新疆', 喀什: '新疆', 和田: '新疆', 伊犁: '新疆', 塔城: '新疆', 阿勒泰: '新疆', 石河子: '新疆', 阿拉尔: '新疆', 图木舒克: '新疆', 五家渠: '新疆', 北屯: '新疆', 铁门关: '新疆', 双河: '新疆', 可克达拉: '新疆', }; async function selectRegion(cityName) { if (!cityName) return; const city = normStr(cityName); const province = CITY2PROV[city]; if (!province) throw new Error('未找到城市映射: ' + city); $('#select5').click(); await sleep(400); const provUl = $('.ucarselecttype_pinpaibottom_ul.select_province'); if (!provUl) throw new Error('省份列表未出现'); const pp = findMatch(provUl.querySelectorAll('p'), province, true); if (!pp) throw new Error('省份未匹配: ' + province); pp.click(); await sleep(400); const cityUl = $('.ucarselecttype_pinpaibottom_ul.select_city'); if (!cityUl) throw new Error('城市列表未出现'); const cp = findMatch(cityUl.querySelectorAll('p'), city, true); if (!cp) throw new Error('城市未匹配: ' + city); cp.click(); await sleep(300); } function showTip(text) { const old = document.getElementById('che300-tip'); if (old) old.remove(); const tip = document.createElement('div'); tip.id = 'che300-tip'; tip.textContent = text; Object.assign(tip.style, { position: 'fixed', top: '50%', left: '50%', transform: 'translate(-50%,-50%)', zIndex: '99998', padding: '16px 28px', background: 'rgba(0,0,0,.78)', color: '#fff', fontSize: '15px', borderRadius: '8px', boxShadow: '0 6px 20px rgba(0,0,0,.3)', cursor: 'pointer', userSelect: 'none', maxWidth: '80%', textAlign: 'center', lineHeight: '1.6', }); const mask = document.createElement('div'); mask.id = 'che300-tip-mask'; Object.assign(mask.style, { position: 'fixed', inset: '0', zIndex: '99997', background: 'transparent', cursor: 'pointer', }); const close = () => { mask.remove(); tip.remove(); }; mask.addEventListener('click', close); tip.addEventListener('click', close); document.body.appendChild(mask); document.body.appendChild(tip); setTimeout(close, 3500); } async function main() { try { const ts = GM_getValue(STORAGE_TS, 0); const info = GM_getValue(STORAGE_KEY, null); if (!ts || Date.now() - ts > 10 * 60 * 1000 || !info) return; await waitForEl('#select1'); await waitForEl('#select4'); await waitForEl('#select5'); await sleep(500); if (info.city) { try { await selectRegion(info.city); } catch (e) { console.warn(TAG, '地区失败:', e.message); } await sleep(200); } let modelOk = false; if (info.brand && info.series && info.model) { try { await selectModel(info.brand, info.series, info.model); modelOk = true; } catch (e) { console.warn(TAG, '车型失败:', e.message); showTip('车型未匹配到,请手动选择'); } await sleep(200); } if (modelOk && info.year) { try { await selectYearMonth(info.year, info.month || '01'); } catch (e) { console.warn(TAG, '年份失败:', e.message); } } } catch (err) { console.error(TAG, '主流程失败:', err); } } setTimeout(main, 1500); } })();