// ==UserScript== // @name Everyday Profit Plus // @namespace http://tampermonkey.net/ // @version 2025.12.18.4 // @description 在原 Everyday Profit 基础上增强:保留原“总净资产”历史与标签;新增每日盈亏柱状图+均线、导出/导入备份、数据管理(编辑/删除/异常清理)、以及 MWITools 资产分项(装备/库存/订单/房子/技能)变化图(单按钮切换) // @author VictoryWinWinWin, PaperCat, SuXingX // @match https://www.milkywayidle.com/* // @match https://*.milkywayidlecn.com/* // @grant GM_addStyle // @license MIT // @downloadURL https://update.greasyfork.icu/scripts/559205/Everyday%20Profit%20Plus.user.js // @updateURL https://update.greasyfork.icu/scripts/559205/Everyday%20Profit%20Plus.meta.js // ==/UserScript== /* MIT License Copyright (c) 2025 VictoryWinWinWin, PaperCat, SuXingX Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ (function () { 'use strict'; /* ========================= Styles ========================= */ GM_addStyle(` #deltaNetworthChartModal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 900px; max-width: 94vw; background: #1e1e1e; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.6); z-index: 9999; display: none; flex-direction: column; color: #f5f5f5; border: 1px solid rgba(255,255,255,0.08); } #deltaNetworthChartModal.dragging { cursor: grabbing; } #deltaNetworthChartHeader { padding: 10px 15px; background: #333; color: white; font-weight: bold; display: flex; justify-content: space-between; align-items: center; cursor: move; user-select: none; border-top-left-radius: 8px; border-top-right-radius: 8px; } #deltaNetworthChartControls { padding: 10px; text-align: center; background: #0c141f; border-top: 1px solid rgba(255,255,255,0.05); border-bottom: 1px solid rgba(255,255,255,0.05); } #deltaNetworthChartControls button { background: #2b303a; color: #f0f0f0; border: 1px solid rgba(255,255,255,0.1); border-radius: 6px; transition: background 0.2s ease, transform 0.2s ease; min-width: 74px; margin: 5px; padding: 6px 12px; cursor: pointer; } #deltaNetworthChartControls button:hover { background: #3f4655; transform: translateY(-1px); } #deltaNetworthChartControls button.active { background: #00c6ff; color: #0b1522; box-shadow: 0 0 10px rgba(0,198,255,0.5); border-color: transparent; } #netWorthChartBody { padding: 15px; background: #0b1522; border-bottom-left-radius: 8px; border-bottom-right-radius: 8px; border: 1px solid rgba(255,255,255,0.05); } #netWorthChart { width: 100%; height: 340px; background: radial-gradient(circle at top, rgba(0,198,255,0.08), rgba(2,12,24,0.95)); border-radius: 6px; } .ep-delta-extra { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 6px; font-size: 14px; color: #cfd8e3; } .ep-delta-extra span { background: rgba(255, 255, 255, 0.08); border-radius: 4px; padding: 3px 8px; } .ep-metrics-grid { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 10px; padding: 10px 15px; background: #111b2b; border-top: 1px solid rgba(255,255,255,0.05); border-bottom: 1px solid rgba(255,255,255,0.05); } @media (max-width: 720px) { .ep-metrics-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } } @media (max-width: 420px) { .ep-metrics-grid { grid-template-columns: 1fr; } } .ep-metric-card { background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.08); border-radius: 8px; padding: 8px 10px; display: flex; flex-direction: column; gap: 4px; min-width: 0; } .ep-metric-card h4 { font-size: 12px; font-weight: normal; color: #9fb4d1; margin: 0; } .ep-metric-card strong { font-size: 18px; color: #f7fafc; word-break: break-word; } .ep-metric-card span { font-size: 12px; color: #7f8ca3; word-break: break-word; } .ep-inline-group { display: inline-flex; align-items: center; gap: 6px; flex-wrap: wrap; justify-content: center; } .ep-tag-toggle { display: inline-flex; align-items: center; gap: 4px; color: #dfe7f3; font-size: 13px; margin-left: 8px; } .ep-tag-manager, .ep-data-manager { padding: 10px 15px 10px; background: #0c141f; border-top: 1px solid rgba(255,255,255,0.05); border-bottom: 1px solid rgba(255,255,255,0.05); display: none; } .ep-tag-manager.active, .ep-data-manager.active { display: block; } .ep-tag-form { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-bottom: 8px; } .ep-tag-form select, .ep-tag-form input { flex: 1 1 160px; min-width: 120px; padding: 5px 8px; background: #0f1b2b; color: #e5f4ff; border: 1px solid rgba(255,255,255,0.12); border-radius: 4px; } .ep-tag-form button { padding: 6px 14px; background: #1f8ef1; border: none; border-radius: 4px; color: #fff; cursor: pointer; } .ep-tag-list { display: flex; flex-direction: column; gap: 6px; max-height: 160px; overflow-y: auto; } .ep-tag-item { display: flex; align-items: center; justify-content: space-between; background: rgba(255,255,255,0.05); border: 1px solid rgba(255,255,255,0.08); border-radius: 4px; padding: 4px 8px; font-size: 13px; color: #dfe7f3; gap: 10px; } .ep-tag-item strong { color: #ffd369; margin-right: 6px; } .ep-tag-actions { display: inline-flex; gap: 6px; flex: 0 0 auto; } .ep-tag-edit, .ep-tag-delete { background: transparent; border: none; cursor: pointer; font-size: 14px; color: #9cc2ff; } .ep-tag-delete { color: #ff6b6b; } .ep-tag-empty { color: #70819d; font-size: 13px; } .ep-data-toolbar { display: flex; flex-wrap: wrap; gap: 8px; align-items: center; justify-content: space-between; margin-bottom: 8px; } .ep-data-toolbar .left, .ep-data-toolbar .right { display: inline-flex; gap: 8px; flex-wrap: wrap; align-items: center; } .ep-data-toolbar button { padding: 6px 12px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.12); background: #2b303a; color: #f0f0f0; cursor: pointer; } .ep-data-toolbar button.danger { border-color: rgba(255,107,107,0.5); color: #ffb3b3; } .ep-data-toolbar button.primary { border-color: rgba(0,198,255,0.35); color: #c9f3ff; } .ep-data-toolbar small { color: #8aa0be; } .ep-data-table-wrap { max-height: 240px; overflow: auto; border: 1px solid rgba(255,255,255,0.08); border-radius: 8px; } .ep-data-table { width: 100%; border-collapse: collapse; font-size: 13px; } .ep-data-table th, .ep-data-table td { padding: 8px 10px; border-bottom: 1px solid rgba(255,255,255,0.06); vertical-align: middle; } .ep-data-table th { text-align: left; color: #9fb4d1; background: rgba(255,255,255,0.04); position: sticky; top: 0; z-index: 1; } .ep-data-table td { color: #dfe7f3; } .ep-data-table .mono { font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; } .ep-data-table .actions { display: inline-flex; gap: 8px; flex-wrap: wrap; } .ep-data-table .actions button { padding: 4px 10px; border-radius: 6px; border: 1px solid rgba(255,255,255,0.12); background: rgba(255,255,255,0.06); color: #e5f4ff; cursor: pointer; } .ep-data-table .actions button.danger { border-color: rgba(255,107,107,0.55); color: #ffd0d0; } .ep-note { margin-top: 8px; color: #8aa0be; font-size: 12px; text-align: left; line-height: 1.4; } /* EP+ Header logos (consistent badges) */ #epCrocLogo, #epCrocLogo span { font-size: 16px; line-height: 1; filter: drop-shadow(0 0 6px rgba(0, 198, 255, 0.35)); } #epPaperCatLogo { color: #e5f4ff; /* SVG stroke uses currentColor */ } /* --- EP+ Modal usability fixes: prevent over-tall modal & keep controls reachable --- */ #deltaNetworthChartModal { max-height: 92vh; overflow-y: auto; } #deltaNetworthChartHeader { position: sticky; top: 0; z-index: 10002; } #deltaNetworthChartControls { position: sticky; top: 44px; /* below header */ z-index: 10001; } /* Make managers independently scrollable to avoid extreme modal height */ .ep-tag-manager.active { max-height: 34vh; overflow: auto; } .ep-data-manager.active { max-height: 42vh; overflow: auto; } /* --- EP+ Manager layout: avoid tiny inner scroll areas when both managers are open --- */ .ep-tag-manager.active, .ep-data-manager.active { max-height: none !important; overflow: visible !important; } /* --- EP+ Precision hint banner (dismissible) --- */ #epPrecisionHint { margin-top: 8px; padding: 10px 12px; border-radius: 10px; border: 1px solid rgba(255, 255, 255, 0.14); background: rgba(0, 0, 0, 0.25); backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); color: rgba(255, 255, 255, 0.92); font-size: 13px; line-height: 1.45; } #epPrecisionHint code { padding: 1px 6px; border-radius: 6px; background: rgba(0, 198, 255, 0.12); border: 1px solid rgba(0, 198, 255, 0.22); color: rgba(255, 255, 255, 0.95); font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; font-size: 12px; } .epPrecisionHintActions { margin-top: 8px; display: flex; gap: 8px; flex-wrap: wrap; } .epPrecisionHintBtn { padding: 6px 10px; border-radius: 10px; border: 1px solid rgba(255, 255, 255, 0.18); background: rgba(255, 255, 255, 0.06); color: rgba(255, 255, 255, 0.92); cursor: pointer; font-size: 12px; } .epPrecisionHintBtn:hover { background: rgba(255, 255, 255, 0.10); } `); /* ========================= Constants / Utilities ========================= */ const STORAGE_KEYS = { totalData: 'kbd_calc_data', // 保持原 key,不改动 tags: 'kbd_calc_tags', // 保持原 key,不改动 tagPrefs: 'kbd_calc_tag_prefs', tagPanel: 'kbd_calc_tag_panel', dataPanel: 'kbd_calc_data_panel', lastUpdate: 'kbd_calc_last_update_at', breakdownData: 'kbd_calc_breakdown_data', // 新增:分项数据 }; const CHART_JS_SRC = [ 'https://cdn.jsdelivr.net/npm/chart.js@4.4.3/dist/chart.umd.min.js', 'https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.3/chart.umd.min.js', 'https://unpkg.com/chart.js@4.4.3/dist/chart.umd.min.js', ]; const PLUGIN_SOURCES = { zoom: [ 'https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom@2.0.1/dist/chartjs-plugin-zoom.min.js', 'https://unpkg.com/chartjs-plugin-zoom@2.0.1/dist/chartjs-plugin-zoom.min.js', ], crosshair: [ 'https://cdn.jsdelivr.net/npm/chartjs-plugin-crosshair@2.0.0/dist/chartjs-plugin-crosshair.min.js', 'https://unpkg.com/chartjs-plugin-crosshair@2.0.0/dist/chartjs-plugin-crosshair.min.js', ], }; const SCRIPT_IDS = { chart: 'everyday-profit-chartjs', zoom: 'everyday-profit-zoom', crosshair: 'everyday-profit-crosshair', }; const CHART_THEME = { lineColor: 'rgba(0, 198, 255, 0.9)', fillColor: 'rgba(0, 198, 255, 0.15)', pointColor: '#00c6ff', pointBorder: '#08111f', gridColor: 'rgba(255,255,255,0.08)', tickColor: '#d4d7dd', tooltipBg: 'rgba(8, 17, 31, 0.92)', tooltipColor: '#e5f4ff', profitMAColor: 'rgba(255, 211, 105, 0.95)', profitPos: 'rgba(0, 220, 140, 0.55)', profitNeg: 'rgba(255, 99, 132, 0.55)', breakdownEquip: 'rgba(0, 198, 255, 0.55)', breakdownInv: 'rgba(255, 211, 105, 0.55)', breakdownOrder: 'rgba(142, 202, 230, 0.55)', breakdownHouse: 'rgba(255, 99, 132, 0.55)', breakdownSkill: 'rgba(167, 139, 250, 0.55)', breakdownTotalLine: 'rgba(255,255,255,0.85)', }; const TAG_LABEL_PLUGIN_ID = 'epTagLabels'; const TAG_TEXT_MAX = 60; const safeJsonParse = (raw, fallback) => { try { return raw ? JSON.parse(raw) : fallback; } catch { return fallback; } }; const escapeHtml = (str = '') => str .replace(/&/g, '&') .replace(//g, '>') .replace(/"/g, '"') .replace(/'/g, '''); const sanitizeTagText = (text = '') => text.replace(/\s+/g, ' ').trim().slice(0, TAG_TEXT_MAX); function parseFormattedNumber(str) { if (!str) return 0; const match = String(str).match(/(-?[\d.,]+)\s*([kKmMbBtT]?)/); if (!match) return 0; let [, numericPart, unit = ''] = match; numericPart = numericPart.replace(/\s+/g, ''); if (!numericPart) return 0; const commaCount = (numericPart.match(/,/g) || []).length; const dotCount = (numericPart.match(/\./g) || []).length; if (commaCount && dotCount) { if (numericPart.lastIndexOf('.') > numericPart.lastIndexOf(',')) { numericPart = numericPart.replace(/,/g, ''); } else { numericPart = numericPart.replace(/\./g, ''); numericPart = numericPart.replace(/,/g, '.'); } } else if (commaCount) { if (commaCount === 1 && numericPart.split(',')[1]?.length <= 2) numericPart = numericPart.replace(',', '.'); else numericPart = numericPart.replace(/,/g, ''); } else if (dotCount > 1) { const parts = numericPart.split('.'); const decimal = parts.pop(); numericPart = parts.join('') + (decimal ? `.${decimal}` : ''); } const num = parseFloat(numericPart); if (isNaN(num)) return 0; const multiplierMap = { k: 1e3, m: 1e6, b: 1e9, t: 1e12 }; const multiplier = multiplierMap[(unit || '').toLowerCase()] || 1; return num * multiplier; } function formatLargeNumber(num) { const n = Number(num) || 0; const abs = Math.abs(n); if (abs >= 1e12) return (n / 1e12).toFixed(2) + 'T'; if (abs >= 1e9) return (n / 1e9).toFixed(2) + 'B'; if (abs >= 1e6) return (n / 1e6).toFixed(2) + 'M'; if (abs >= 1e3) return (n / 1e3).toFixed(2) + 'K'; return String(n); } const formatSignedLargeNumber = (num) => { const n = Number(num) || 0; return n > 0 ? `+${formatLargeNumber(n)}` : formatLargeNumber(n); }; const downloadTextFile = (filename, text, mime = 'application/json;charset=utf-8') => { const blob = new Blob([text], { type: mime }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); }; const getNowStamp = () => { const d = new Date(); const pad = (n) => String(n).padStart(2, '0'); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}_${pad(d.getHours())}${pad(d.getMinutes())}${pad(d.getSeconds())}`; }; const readRoleLastUpdateMap = () => safeJsonParse(localStorage.getItem(STORAGE_KEYS.lastUpdate), {}); const writeRoleLastUpdateMap = (map) => localStorage.setItem(STORAGE_KEYS.lastUpdate, JSON.stringify(map || {})); const setRoleLastUpdate = (roleId, iso = new Date().toISOString()) => { if (!roleId) return; const map = readRoleLastUpdateMap(); map[roleId] = iso; writeRoleLastUpdateMap(map); }; const getRoleLastUpdate = (roleId) => { if (!roleId) return null; const map = readRoleLastUpdateMap(); return map && map[roleId] ? map[roleId] : null; }; const formatIsoToLocalDateTime = (iso) => { if (!iso) return ''; const d = new Date(iso); if (!Number.isFinite(d.getTime())) return String(iso); const pad = (n) => String(n).padStart(2, '0'); return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`; }; /* ========================= Tag label plugin (keep) ========================= */ const drawRoundedRect = (ctx, x, y, width, height, radius = 6) => { const r = Math.min(radius, height / 2, width / 2); ctx.beginPath(); ctx.moveTo(x + r, y); ctx.lineTo(x + width - r, y); ctx.quadraticCurveTo(x + width, y, x + width, y + r); ctx.lineTo(x + width, y + height - r); ctx.quadraticCurveTo(x + width, y + height, x + width - r, y + height); ctx.lineTo(x + r, y + height); ctx.quadraticCurveTo(x, y + height, x, y + height - r); ctx.lineTo(x, y + r); ctx.quadraticCurveTo(x, y, x + r, y); ctx.closePath(); }; const tagLabelPlugin = { id: TAG_LABEL_PLUGIN_ID, defaults: { minGap: 45, stackSpacing: 6, boxPadding: { x: 6, y: 3 }, font: { size: 11, family: 'Segoe UI, "Microsoft YaHei", sans-serif' }, boxColor: 'rgba(255, 229, 153, 0.98)', borderColor: 'rgba(255, 196, 77, 0.9)', textColor: '#0b1522', lineColor: '#ffd369', pointColor: '#ff6b6b', }, afterDatasetsDraw(chart, args, opts) { if (opts?.enabled === false) return; const tags = opts?.tags; if (!Array.isArray(tags) || !tags.length) return; const dataset = chart.data.datasets?.[0]; if (!dataset) return; const xScale = chart.scales.x; const yScale = chart.scales.y; if (!xScale || !yScale) return; const area = chart.chartArea; const ctx = chart.ctx; const settings = { ...this.defaults, ...opts }; ctx.save(); ctx.font = `${settings.font.size}px ${settings.font.family}`; ctx.textBaseline = 'middle'; const items = tags .map((tag) => { const index = chart.data.labels.indexOf(tag.date); if (index === -1) return null; const value = dataset.data?.[index]; return { ...tag, index, value }; }) .filter(Boolean) .sort((a, b) => a.index - b.index); let lastX = -Infinity; let stackLevel = 0; items.forEach((tag) => { const x = xScale.getPixelForValue(tag.index); const baseY = yScale.getPixelForValue(tag.value); if (!Number.isFinite(x) || !Number.isFinite(baseY)) return; if (Math.abs(x - lastX) < settings.minGap) stackLevel += 1; else stackLevel = 0; lastX = x; const text = tag.text ?? ''; const textWidth = ctx.measureText(text).width; const boxWidth = textWidth + settings.boxPadding.x * 2; const boxHeight = settings.font.size + settings.boxPadding.y * 2; const stackOffset = stackLevel * (boxHeight + settings.stackSpacing + 6); let anchorAbove = true; let boxY = baseY - (boxHeight + 12 + stackOffset); if (boxY < area.top + 4) { anchorAbove = false; boxY = baseY + (12 + stackOffset); if (boxY + boxHeight > area.bottom - 4) { boxY = Math.max(area.top + 4, area.bottom - boxHeight - 4); } } let boxX = x - boxWidth / 2; if (boxX < area.left + 4) boxX = area.left + 4; else if (boxX + boxWidth > area.right - 4) boxX = area.right - boxWidth - 4; const pointerX = Math.min(Math.max(x, boxX + 4), boxX + boxWidth - 4); const pointerY = anchorAbove ? boxY + boxHeight : boxY; ctx.strokeStyle = settings.lineColor; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(pointerX, pointerY); ctx.lineTo(x, baseY); ctx.stroke(); ctx.fillStyle = settings.boxColor; ctx.strokeStyle = settings.borderColor; drawRoundedRect(ctx, boxX, boxY, boxWidth, boxHeight, 6); ctx.fill(); ctx.stroke(); ctx.fillStyle = settings.textColor; ctx.fillText(text, boxX + settings.boxPadding.x, boxY + boxHeight / 2); ctx.fillStyle = settings.pointColor; ctx.beginPath(); ctx.arc(x, baseY, 3, 0, Math.PI * 2); ctx.fill(); }); ctx.restore(); }, }; /* ========================= Script loader (Chart.js + plugins) ========================= */ const scriptPromises = {}; const loadScriptOnce = (id, srcOrList) => { if (scriptPromises[id]) return scriptPromises[id]; if (document.getElementById(id)) { scriptPromises[id] = Promise.resolve(); return scriptPromises[id]; } const sources = Array.isArray(srcOrList) ? srcOrList : [srcOrList]; const tryLoad = (index) => new Promise((resolve, reject) => { if (index >= sources.length) { reject(new Error(`脚本加载失败: ${sources.join(', ')}`)); return; } const script = document.createElement('script'); script.id = id; script.src = sources[index]; script.async = true; script.onload = resolve; script.onerror = () => { script.remove(); tryLoad(index + 1).then(resolve).catch(reject); }; document.head.appendChild(script); }); scriptPromises[id] = tryLoad(0); scriptPromises[id].catch(() => { delete scriptPromises[id]; }); return scriptPromises[id]; }; const hasPluginRegistered = (pluginId) => { if (typeof Chart === 'undefined') return false; try { if (typeof Chart.registry?.getPlugin === 'function') return !!Chart.registry.getPlugin(pluginId); } catch {} const pluginsArray = Chart.plugins?.plugins; if (Array.isArray(pluginsArray)) return pluginsArray.some((p) => p.id === pluginId); return false; }; const ensurePluginLoaded = async (pluginId, scriptId, src) => { if (hasPluginRegistered(pluginId)) return; await loadScriptOnce(scriptId, src); if (!hasPluginRegistered(pluginId)) throw new Error(`${pluginId} 插件注册失败`); }; let chartAssetsPromise = null; const ensureChartAssets = () => { if (!chartAssetsPromise) { chartAssetsPromise = (async () => { if (typeof Chart === 'undefined') await loadScriptOnce(SCRIPT_IDS.chart, CHART_JS_SRC); if (!hasPluginRegistered(TAG_LABEL_PLUGIN_ID)) Chart.register(tagLabelPlugin); await ensurePluginLoaded('zoom', SCRIPT_IDS.zoom, PLUGIN_SOURCES.zoom); await ensurePluginLoaded('crosshair', SCRIPT_IDS.crosshair, PLUGIN_SOURCES.crosshair); })().catch((err) => { chartAssetsPromise = null; throw err; }); } return chartAssetsPromise; }; const enqueueChartReady = (callback) => { ensureChartAssets() .then(() => { try { callback(); } catch (err) { console.error('渲染图表出错:', err); } }) .catch((err) => console.error('图表依赖加载失败:', err)); }; const cleanupScaleLimits = (scaleLike) => { if (!scaleLike) return; if (scaleLike.options) { delete scaleLike.options.min; delete scaleLike.options.max; } if ('min' in scaleLike) delete scaleLike.min; if ('max' in scaleLike) delete scaleLike.max; }; const resetChartZoom = (chartInstance) => { if (!chartInstance) return; try { if (typeof chartInstance.resetZoom === 'function') chartInstance.resetZoom(); } catch {} cleanupScaleLimits(chartInstance.options?.scales?.x); cleanupScaleLimits(chartInstance.options?.scales?.y); cleanupScaleLimits(chartInstance.scales?.x); cleanupScaleLimits(chartInstance.scales?.y); }; /* ========================= Role detection (compat) ========================= */ const normalizeRole = (s) => (s || '').replace(/\s+/g, ' ').trim(); const detectRoleId = () => { const candidates = [ document.querySelector('.CharacterName_name__1amXp span'), document.querySelector('[class*="CharacterName_name"] span'), document.querySelector('[data-testid="character-name"]'), ]; const text = normalizeRole(candidates.find(Boolean)?.textContent); if (text) return text; // fallback: if storage only has 1 role, use it (helps “读回旧数据”) const data = safeJsonParse(localStorage.getItem(STORAGE_KEYS.totalData), {}); const roles = Object.keys(data || {}); if (roles.length === 1) return roles[0]; return 'default'; }; /* ========================= MWITools breakdown reader ========================= */ const BREAKDOWN_DEFS = [ { key: 'equip', label: '装备', color: 'rgba(0, 198, 255, 0.55)', patterns: [ /装备价值\s*[::]\s*([-\d.,]+\s*[kKmMbBtT]?)/, /Equipment\s*Value\s*[::]\s*([-\d.,]+\s*[kKmMbBtT]?)/i, /Gear\s*Value\s*[::]\s*([-\d.,]+\s*[kKmMbBtT]?)/i, ]}, { key: 'inventory', label: '库存', color: 'rgba(255, 211, 105, 0.55)', patterns: [ /库存价值\s*[::]\s*([-\d.,]+\s*[kKmMbBtT]?)/, /Inventory\s*Value\s*[::]\s*([-\d.,]+\s*[kKmMbBtT]?)/i, ]}, { key: 'orders', label: '订单', color: 'rgba(142, 202, 230, 0.55)', patterns: [ /订单价值\s*[::]\s*([-\d.,]+\s*[kKmMbBtT]?)/, /Order(?:s)?\s*Value\s*[::]\s*([-\d.,]+\s*[kKmMbBtT]?)/i, ]}, { key: 'house', label: '房子', color: 'rgba(255, 99, 132, 0.55)', patterns: [ /房子价值\s*[::]\s*([-\d.,]+\s*[kKmMbBtT]?)/, /House\s*Value\s*[::]\s*([-\d.,]+\s*[kKmMbBtT]?)/i, /Home\s*Value\s*[::]\s*([-\d.,]+\s*[kKmMbBtT]?)/i, ]}, { key: 'skill', label: '技能', color: 'rgba(167, 139, 250, 0.55)', patterns: [ /技能价值\s*[::]\s*([-\d.,]+\s*[kKmMbBtT]?)/, /Skill\s*Value\s*[::]\s*([-\d.,]+\s*[kKmMbBtT]?)/i, ]}, ]; const readBreakdownTextBlock = () => { const parts = []; const el1 = document.getElementById('toggleNetWorth'); if (el1) parts.push(el1.innerText || el1.textContent || ''); const el2 = document.getElementById('currentAssets'); if (el2) parts.push(el2.innerText || el2.textContent || ''); const el3 = document.getElementById('nonCurrentAssets'); if (el3) parts.push(el3.innerText || el3.textContent || ''); // fallback: use parent container text if (parts.join('').trim().length < 10 && el1?.parentElement) { const t = el1.parentElement.innerText || el1.parentElement.textContent || ''; if (t) parts.push(t); } return parts.join('\n'); }; const extractFirstNumberByPatterns = (text, patterns) => { for (const re of patterns) { const m = text.match(re); if (m && m[1]) return parseFormattedNumber(m[1]); } return null; }; const readBreakdownFromMWITools = () => { const block = readBreakdownTextBlock(); if (!block || block.trim().length < 5) return null; const out = {}; let found = 0; for (const def of BREAKDOWN_DEFS) { const v = extractFirstNumberByPatterns(block, def.patterns); if (Number.isFinite(v) && v > 0) { out[def.key] = v; found += 1; } } if (found < 2) return null; out.currentTotal = (out.equip || 0) + (out.inventory || 0) + (out.orders || 0); out.nonCurrentTotal = (out.house || 0) + (out.skill || 0); return out; }; /* ========================= Stores ========================= */ class DailyDataStore { constructor(storageKey = STORAGE_KEYS.totalData, currentRole = 'default') { this.storageKey = storageKey; this.currentRole = currentRole; this.data = this.loadFromStorage(); } setRole(roleId) { this.currentRole = roleId; } getRoleData() { if (!this.data[this.currentRole]) this.data[this.currentRole] = {}; return this.data[this.currentRole]; } getTodayKey() { const now = new Date(); const utcPlus8 = new Date(now.getTime() + 8 * 3600000); return utcPlus8.toISOString().split('T')[0]; } getYesterdayKey() { const now = new Date(); const yesterday = new Date(now.getTime() - 24 * 3600000); const utcPlus8 = new Date(yesterday.getTime() + 8 * 3600000); return utcPlus8.toISOString().split('T')[0]; } loadFromStorage() { return safeJsonParse(localStorage.getItem(this.storageKey), {}); } saveToStorage() { localStorage.setItem(this.storageKey, JSON.stringify(this.data)); } setTodayValue(value) { const roleData = this.getRoleData(); const today = this.getTodayKey(); roleData[today] = value; this.saveToStorage(); } setDateValue(dateKey, value) { if (!dateKey) return false; const roleData = this.getRoleData(); roleData[dateKey] = value; this.saveToStorage(); return true; } removeDate(dateKey) { if (!dateKey) return false; const roleData = this.getRoleData(); if (Object.prototype.hasOwnProperty.call(roleData, dateKey)) { delete roleData[dateKey]; this.saveToStorage(); return true; } return false; } removeInvalidAndNonFinite() { const roleData = this.getRoleData(); let changed = false; Object.keys(roleData).forEach((k) => { const v = roleData[k]; if (!Number.isFinite(v) || v === null || v === undefined) { delete roleData[k]; changed = true; } }); if (changed) this.saveToStorage(); return changed; } getTodayDelta() { const roleData = this.getRoleData(); const todayKey = this.getTodayKey(); const yesterdayKey = this.getYesterdayKey(); const todayValue = roleData[todayKey] || 0; const yesterdayValue = roleData[yesterdayKey] || 0; return todayValue - yesterdayValue; } getHistoryEntriesSorted() { const roleData = this.getRoleData(); return Object.entries(roleData).sort(([a], [b]) => new Date(a) - new Date(b)); } getAllRoles() { return Object.keys(this.data); } } class BreakdownStore { constructor(storageKey = STORAGE_KEYS.breakdownData, currentRole = 'default') { this.storageKey = storageKey; this.currentRole = currentRole; this.data = this.loadFromStorage(); } setRole(roleId) { this.currentRole = roleId; } getRoleData() { if (!this.data[this.currentRole]) this.data[this.currentRole] = {}; return this.data[this.currentRole]; } loadFromStorage() { return safeJsonParse(localStorage.getItem(this.storageKey), {}); } saveToStorage() { localStorage.setItem(this.storageKey, JSON.stringify(this.data)); } setTodayValue(dateKey, breakdownObj) { if (!dateKey || !breakdownObj) return false; const roleData = this.getRoleData(); roleData[dateKey] = breakdownObj; this.saveToStorage(); return true; } removeDate(dateKey) { if (!dateKey) return false; const roleData = this.getRoleData(); if (Object.prototype.hasOwnProperty.call(roleData, dateKey)) { delete roleData[dateKey]; this.saveToStorage(); return true; } return false; } getHistoryEntriesSorted() { const roleData = this.getRoleData(); return Object.entries(roleData).sort(([a], [b]) => new Date(a) - new Date(b)); } } class TagStore { constructor(storageKey = STORAGE_KEYS.tags, currentRole = 'default') { this.storageKey = storageKey; this.currentRole = currentRole; this.data = this.loadFromStorage(); } setRole(roleId) { this.currentRole = roleId; } loadFromStorage() { return safeJsonParse(localStorage.getItem(this.storageKey), {}); } saveToStorage() { localStorage.setItem(this.storageKey, JSON.stringify(this.data)); } getRoleBucket() { if (!this.data[this.currentRole]) this.data[this.currentRole] = {}; return this.data[this.currentRole]; } listTags(validDates) { const bucket = this.getRoleBucket(); const result = []; const allowed = validDates ? new Set(validDates) : null; Object.entries(bucket).forEach(([date, arr]) => { if (allowed && !allowed.has(date)) return; (arr || []).forEach((tag) => result.push({ ...tag, date })); }); return result.sort((a, b) => new Date(a.date) - new Date(b.date)); } addTag(date, text) { if (!date || !text) return null; const bucket = this.getRoleBucket(); if (!Array.isArray(bucket[date])) bucket[date] = []; const tag = { id: `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`, text: sanitizeTagText(text), }; if (!tag.text) return null; bucket[date].push(tag); this.saveToStorage(); return tag; } removeTagById(tagId) { if (!tagId) return; const bucket = this.getRoleBucket(); let hasChange = false; Object.keys(bucket).forEach((date) => { const list = bucket[date] || []; const filtered = list.filter((tag) => tag.id !== tagId); if (filtered.length !== list.length) { hasChange = true; if (filtered.length) bucket[date] = filtered; else delete bucket[date]; } }); if (hasChange) this.saveToStorage(); } updateTagById(tagId, nextText) { if (!tagId) return false; const cleanText = sanitizeTagText(nextText); if (!cleanText) return false; const bucket = this.getRoleBucket(); let updated = false; Object.values(bucket).forEach((list) => { (list || []).forEach((tag) => { if (tag.id === tagId && tag.text !== cleanText) { tag.text = cleanText; updated = true; } }); }); if (updated) this.saveToStorage(); return updated; } removeDate(dateKey) { if (!dateKey) return false; const bucket = this.getRoleBucket(); if (Object.prototype.hasOwnProperty.call(bucket, dateKey)) { delete bucket[dateKey]; this.saveToStorage(); return true; } return false; } } const readPrefs = (key) => safeJsonParse(localStorage.getItem(key), {}); const writePrefs = (key, prefs) => localStorage.setItem(key, JSON.stringify(prefs)); const getRoleBoolPref = (key, roleId, defaultValue) => { const prefs = readPrefs(key); if (roleId && Object.prototype.hasOwnProperty.call(prefs, roleId)) return !!prefs[roleId]; return !!defaultValue; }; const setRoleBoolPref = (key, roleId, value) => { if (!roleId) return; const prefs = readPrefs(key); prefs[roleId] = !!value; writePrefs(key, prefs); }; /* ========================= Backup (export/import) ========================= */ const buildBackupObject = () => ({ __everyday_profit_backup__: true, schema: 2, exportedAt: new Date().toISOString(), payload: { [STORAGE_KEYS.totalData]: safeJsonParse(localStorage.getItem(STORAGE_KEYS.totalData), {}), [STORAGE_KEYS.tags]: safeJsonParse(localStorage.getItem(STORAGE_KEYS.tags), {}), [STORAGE_KEYS.tagPrefs]: safeJsonParse(localStorage.getItem(STORAGE_KEYS.tagPrefs), {}), [STORAGE_KEYS.tagPanel]: safeJsonParse(localStorage.getItem(STORAGE_KEYS.tagPanel), {}), [STORAGE_KEYS.dataPanel]: safeJsonParse(localStorage.getItem(STORAGE_KEYS.dataPanel), {}), [STORAGE_KEYS.breakdownData]: safeJsonParse(localStorage.getItem(STORAGE_KEYS.breakdownData), {}), }, }); const validateBackupObject = (obj) => { if (!obj || typeof obj !== 'object') return false; if (obj.__everyday_profit_backup__ !== true) return false; if (!obj.payload || typeof obj.payload !== 'object') return false; if (!obj.payload[STORAGE_KEYS.totalData] || typeof obj.payload[STORAGE_KEYS.totalData] !== 'object') return false; return true; }; const deepMergeRoleBuckets = (base, incoming, { overwrite = true } = {}) => { const out = { ...(base || {}) }; Object.keys(incoming || {}).forEach((roleId) => { if (!out[roleId] || typeof out[roleId] !== 'object') out[roleId] = {}; const baseRole = out[roleId] || {}; const incRole = incoming[roleId] || {}; Object.keys(incRole).forEach((k) => { if (overwrite || !Object.prototype.hasOwnProperty.call(baseRole, k)) baseRole[k] = incRole[k]; }); out[roleId] = baseRole; }); return out; }; const mergeTagsBuckets = (base, incoming, { overwrite = true } = {}) => { const out = { ...(base || {}) }; Object.keys(incoming || {}).forEach((roleId) => { if (!out[roleId] || typeof out[roleId] !== 'object') out[roleId] = {}; const baseRole = out[roleId] || {}; const incRole = incoming[roleId] || {}; Object.keys(incRole).forEach((date) => { const incList = Array.isArray(incRole[date]) ? incRole[date] : []; const baseList = Array.isArray(baseRole[date]) ? baseRole[date] : []; const existingIds = new Set(baseList.map(x => x?.id).filter(Boolean)); const normalized = incList.map((t) => { const text = sanitizeTagText(t?.text || ''); if (!text) return null; let id = String(t?.id || ''); if (!id || existingIds.has(id)) id = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`; existingIds.add(id); return { id, text }; }).filter(Boolean); if (overwrite) baseRole[date] = normalized; else baseRole[date] = baseList.concat(normalized); }); out[roleId] = baseRole; }); return out; }; const doExportBackup = () => { const obj = buildBackupObject(); const filename = `EverydayProfit_backup_${getNowStamp()}.json`; downloadTextFile(filename, JSON.stringify(obj, null, 2)); }; const doImportBackup = async (file) => { const text = await file.text(); const obj = safeJsonParse(text, null); if (!validateBackupObject(obj)) { alert('导入失败:备份文件格式不正确(或不是本插件导出的备份)。'); return; } const mode = prompt('导入方式:输入 1=覆盖(迁移推荐) / 2=合并(保留本地并覆盖同键)', '1'); if (mode === null) return; const isOverwrite = String(mode).trim() !== '2'; const incoming = obj.payload || {}; const currentTotal = safeJsonParse(localStorage.getItem(STORAGE_KEYS.totalData), {}); const currentTags = safeJsonParse(localStorage.getItem(STORAGE_KEYS.tags), {}); const currentPrefs = safeJsonParse(localStorage.getItem(STORAGE_KEYS.tagPrefs), {}); const currentTagPanel = safeJsonParse(localStorage.getItem(STORAGE_KEYS.tagPanel), {}); const currentDataPanel = safeJsonParse(localStorage.getItem(STORAGE_KEYS.dataPanel), {}); const currentBreakdown = safeJsonParse(localStorage.getItem(STORAGE_KEYS.breakdownData), {}); const nextTotal = isOverwrite ? (incoming[STORAGE_KEYS.totalData] || {}) : deepMergeRoleBuckets(currentTotal, incoming[STORAGE_KEYS.totalData] || {}, { overwrite: true }); const nextTags = isOverwrite ? (incoming[STORAGE_KEYS.tags] || {}) : mergeTagsBuckets(currentTags, incoming[STORAGE_KEYS.tags] || {}, { overwrite: false }); const nextPrefs = isOverwrite ? (incoming[STORAGE_KEYS.tagPrefs] || {}) : { ...currentPrefs, ...(incoming[STORAGE_KEYS.tagPrefs] || {}) }; const nextTagPanel = isOverwrite ? (incoming[STORAGE_KEYS.tagPanel] || {}) : { ...currentTagPanel, ...(incoming[STORAGE_KEYS.tagPanel] || {}) }; const nextDataPanel = isOverwrite ? (incoming[STORAGE_KEYS.dataPanel] || {}) : { ...currentDataPanel, ...(incoming[STORAGE_KEYS.dataPanel] || {}) }; const nextBreakdown = isOverwrite ? (incoming[STORAGE_KEYS.breakdownData] || {}) : deepMergeRoleBuckets(currentBreakdown, incoming[STORAGE_KEYS.breakdownData] || {}, { overwrite: true }); localStorage.setItem(STORAGE_KEYS.totalData, JSON.stringify(nextTotal)); localStorage.setItem(STORAGE_KEYS.tags, JSON.stringify(nextTags)); localStorage.setItem(STORAGE_KEYS.tagPrefs, JSON.stringify(nextPrefs)); localStorage.setItem(STORAGE_KEYS.tagPanel, JSON.stringify(nextTagPanel)); localStorage.setItem(STORAGE_KEYS.dataPanel, JSON.stringify(nextDataPanel)); localStorage.setItem(STORAGE_KEYS.breakdownData, JSON.stringify(nextBreakdown)); alert('导入成功:已写入本地存储。为确保 UI 同步,建议刷新页面。'); }; /* ========================= Tag UI ========================= */ const buildTagManagerSection = (dates, tags) => { if (!dates.length) return '
| 日期 | 总净资产 | 当日盈亏 | 操作 |
|---|
1e9,将对应那一行删除或注释后保存并刷新页面,
“今日盈亏”即可精确到 0.1M 级别。