diff --git a/.gitignore b/.gitignore
index 8412632..68a9926 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,3 @@
push.sh
-fetch_midas.js
+build_data.py
.DS_Store
diff --git a/data.json b/data.json
index 187a4c8..e37d399 100644
--- a/data.json
+++ b/data.json
@@ -1,42 +1,1589 @@
{
- "updated_at": "2026-09-10T17:14:00+08:00",
- "source": "data_report/list,按日期+订单",
- "account": { "id": 11535, "name": "C00000196_TECDO HONG KONG LIMITED" },
- "range": { "start": "2026-09-03", "end": "2026-09-09" },
- "campaigns": [
- {
- "id": "3202606240007",
- "name": "[Pending]PendingC00000196_Tecdo_Agc_ArionPlay_APK_PH_CPI_MIX_20260624",
- "app": "ArionPlay",
- "start": "2026-06-24",
- "bid": 0.65,
- "daily_budget": 150,
- "lifetime_spend": 3770
- },
- {
- "id": "3202608110004",
- "name": "C00000196_Tecdo_Agc_PTGaming_APK_PH_CPI_MIX_20260811",
- "app": "PTGaming",
- "start": "2026-08-11",
- "bid": 0.65,
- "daily_budget": 50,
- "lifetime_spend": 5.2
- }
- ],
- "daily": [
- { "dt": "2026-09-03", "campaign_id": "3202606240007", "spend": 40.95, "installs": 63, "clicks": 28436, "ecpi": 0.65 },
- { "dt": "2026-09-03", "campaign_id": "3202608110004", "spend": 0, "installs": 0, "clicks": 6123, "ecpi": 0 },
- { "dt": "2026-09-04", "campaign_id": "3202606240007", "spend": 46.8, "installs": 72, "clicks": 30193, "ecpi": 0.65 },
- { "dt": "2026-09-04", "campaign_id": "3202608110004", "spend": 0, "installs": 0, "clicks": 8263, "ecpi": 0 },
- { "dt": "2026-09-05", "campaign_id": "3202606240007", "spend": 45.5, "installs": 70, "clicks": 29587, "ecpi": 0.65 },
- { "dt": "2026-09-05", "campaign_id": "3202608110004", "spend": 0, "installs": 0, "clicks": 6176, "ecpi": 0 },
- { "dt": "2026-09-06", "campaign_id": "3202606240007", "spend": 46.8, "installs": 72, "clicks": 30822, "ecpi": 0.65 },
- { "dt": "2026-09-06", "campaign_id": "3202608110004", "spend": 0, "installs": 0, "clicks": 7574, "ecpi": 0 },
- { "dt": "2026-09-07", "campaign_id": "3202606240007", "spend": 42.25, "installs": 65, "clicks": 31475, "ecpi": 0.65 },
- { "dt": "2026-09-07", "campaign_id": "3202608110004", "spend": 0, "installs": 0, "clicks": 8234, "ecpi": 0 },
- { "dt": "2026-09-08", "campaign_id": "3202606240007", "spend": 40.3, "installs": 62, "clicks": 28810, "ecpi": 0.65 },
- { "dt": "2026-09-08", "campaign_id": "3202608110004", "spend": 0, "installs": 0, "clicks": 3481, "ecpi": 0 },
- { "dt": "2026-09-09", "campaign_id": "3202606240007", "spend": 42.25, "installs": 65, "clicks": 27030, "ecpi": 0.65 },
- { "dt": "2026-09-09", "campaign_id": "3202608110004", "spend": 0, "installs": 0, "clicks": 4208, "ecpi": 0 }
- ]
+ "updated_at": "2026-09-10T17:24+08:00",
+ "source": "tecdo_数据.xlsx(分天)+ 钛动预付款.xlsx",
+ "range": {
+ "start": "2026-06-17",
+ "end": "2026-09-08"
+ },
+ "daily": [
+ {
+ "dt": "2026-06-17",
+ "pkg": "com.ic.sv777_81",
+ "installs": 3,
+ "reg": 2,
+ "ftd": 0,
+ "spend": 1.2
+ },
+ {
+ "dt": "2026-06-17",
+ "pkg": "com.shiluoshazhou.uasdfjasf",
+ "installs": 0,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0
+ },
+ {
+ "dt": "2026-06-18",
+ "pkg": "com.ic.sv777_81",
+ "installs": 0,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0
+ },
+ {
+ "dt": "2026-06-18",
+ "pkg": "com.shiluoshazhou.uasdfjasf",
+ "installs": 2,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0.8
+ },
+ {
+ "dt": "2026-06-19",
+ "pkg": "com.ic.sv777_81",
+ "installs": 8,
+ "reg": 8,
+ "ftd": 0,
+ "spend": 3.2
+ },
+ {
+ "dt": "2026-06-19",
+ "pkg": "com.shiluoshazhou.uasdfjasf",
+ "installs": 1,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0.4
+ },
+ {
+ "dt": "2026-06-20",
+ "pkg": "com.ic.sv777_81",
+ "installs": 9,
+ "reg": 6,
+ "ftd": 0,
+ "spend": 3.6
+ },
+ {
+ "dt": "2026-06-20",
+ "pkg": "com.shiluoshazhou.uasdfjasf",
+ "installs": 3,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 1.2
+ },
+ {
+ "dt": "2026-06-21",
+ "pkg": "com.ic.sv777_81",
+ "installs": 5,
+ "reg": 5,
+ "ftd": 1,
+ "spend": 2
+ },
+ {
+ "dt": "2026-06-21",
+ "pkg": "com.shiluoshazhou.uasdfjasf",
+ "installs": 3,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 1.2
+ },
+ {
+ "dt": "2026-06-22",
+ "pkg": "com.ic.sv777_81",
+ "installs": 0,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0
+ },
+ {
+ "dt": "2026-06-22",
+ "pkg": "com.shiluoshazhou.uasdfjasf",
+ "installs": 2,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0.8
+ },
+ {
+ "dt": "2026-06-23",
+ "pkg": "com.ic.sv777_81",
+ "installs": 1,
+ "reg": 1,
+ "ftd": 0,
+ "spend": 0.4
+ },
+ {
+ "dt": "2026-06-23",
+ "pkg": "com.shiluoshazhou.uasdfjasf",
+ "installs": 0,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0
+ },
+ {
+ "dt": "2026-06-24",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 0,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0
+ },
+ {
+ "dt": "2026-06-25",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 0,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0
+ },
+ {
+ "dt": "2026-06-28",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 8,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 5.2
+ },
+ {
+ "dt": "2026-06-29",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 16,
+ "reg": 0,
+ "ftd": 2,
+ "spend": 10.4
+ },
+ {
+ "dt": "2026-06-30",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 86,
+ "reg": 4,
+ "ftd": 17,
+ "spend": 55.9
+ },
+ {
+ "dt": "2026-07-01",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 86,
+ "reg": 3,
+ "ftd": 13,
+ "spend": 55.9
+ },
+ {
+ "dt": "2026-07-02",
+ "pkg": "com.f111.kk222",
+ "installs": 45,
+ "reg": 3,
+ "ftd": 0,
+ "spend": 27
+ },
+ {
+ "dt": "2026-07-02",
+ "pkg": "com.ic.ek77_85",
+ "installs": 33,
+ "reg": 10,
+ "ftd": 0,
+ "spend": 16.5
+ },
+ {
+ "dt": "2026-07-02",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 96,
+ "reg": 7,
+ "ftd": 15,
+ "spend": 62.4
+ },
+ {
+ "dt": "2026-07-03",
+ "pkg": "com.f111.kk222",
+ "installs": 115,
+ "reg": 18,
+ "ftd": 2,
+ "spend": 69
+ },
+ {
+ "dt": "2026-07-03",
+ "pkg": "com.ic.ek77_85",
+ "installs": 91,
+ "reg": 35,
+ "ftd": 4,
+ "spend": 45.5
+ },
+ {
+ "dt": "2026-07-03",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 79,
+ "reg": 1,
+ "ftd": 10,
+ "spend": 51.35
+ },
+ {
+ "dt": "2026-07-04",
+ "pkg": "com.f111.kk222",
+ "installs": 111,
+ "reg": 21,
+ "ftd": 2,
+ "spend": 66.6
+ },
+ {
+ "dt": "2026-07-04",
+ "pkg": "com.ic.ek77_85",
+ "installs": 60,
+ "reg": 18,
+ "ftd": 5,
+ "spend": 30
+ },
+ {
+ "dt": "2026-07-04",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 80,
+ "reg": 2,
+ "ftd": 9,
+ "spend": 52
+ },
+ {
+ "dt": "2026-07-05",
+ "pkg": "com.f111.kk222",
+ "installs": 110,
+ "reg": 11,
+ "ftd": 3,
+ "spend": 66
+ },
+ {
+ "dt": "2026-07-05",
+ "pkg": "com.ic.ek77_85",
+ "installs": 53,
+ "reg": 20,
+ "ftd": 2,
+ "spend": 26.5
+ },
+ {
+ "dt": "2026-07-05",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 66,
+ "reg": 2,
+ "ftd": 3,
+ "spend": 42.9
+ },
+ {
+ "dt": "2026-07-06",
+ "pkg": "com.f111.kk222",
+ "installs": 60,
+ "reg": 12,
+ "ftd": 1,
+ "spend": 36
+ },
+ {
+ "dt": "2026-07-06",
+ "pkg": "com.ic.ek77_85",
+ "installs": 94,
+ "reg": 37,
+ "ftd": 6,
+ "spend": 47
+ },
+ {
+ "dt": "2026-07-06",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 62,
+ "reg": 3,
+ "ftd": 6,
+ "spend": 40.3
+ },
+ {
+ "dt": "2026-07-07",
+ "pkg": "com.ic.ek77_85",
+ "installs": 161,
+ "reg": 85,
+ "ftd": 9,
+ "spend": 80.5
+ },
+ {
+ "dt": "2026-07-07",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 71,
+ "reg": 2,
+ "ftd": 10,
+ "spend": 46.15
+ },
+ {
+ "dt": "2026-07-08",
+ "pkg": "com.ic.ek77_85",
+ "installs": 148,
+ "reg": 80,
+ "ftd": 16,
+ "spend": 74
+ },
+ {
+ "dt": "2026-07-08",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 69,
+ "reg": 1,
+ "ftd": 7,
+ "spend": 44.85
+ },
+ {
+ "dt": "2026-07-09",
+ "pkg": "com.ic.ek77_85",
+ "installs": 138,
+ "reg": 75,
+ "ftd": 10,
+ "spend": 69
+ },
+ {
+ "dt": "2026-07-09",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 77,
+ "reg": 5,
+ "ftd": 10,
+ "spend": 50.05
+ },
+ {
+ "dt": "2026-07-10",
+ "pkg": "com.ic.ek77_85",
+ "installs": 135,
+ "reg": 78,
+ "ftd": 11,
+ "spend": 67.5
+ },
+ {
+ "dt": "2026-07-10",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 87,
+ "reg": 2,
+ "ftd": 8,
+ "spend": 56.55
+ },
+ {
+ "dt": "2026-07-11",
+ "pkg": "com.ic.ek77_85",
+ "installs": 152,
+ "reg": 84,
+ "ftd": 13,
+ "spend": 76
+ },
+ {
+ "dt": "2026-07-11",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 86,
+ "reg": 5,
+ "ftd": 12,
+ "spend": 55.9
+ },
+ {
+ "dt": "2026-07-12",
+ "pkg": "com.ic.ek77_85",
+ "installs": 153,
+ "reg": 90,
+ "ftd": 14,
+ "spend": 76.5
+ },
+ {
+ "dt": "2026-07-12",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 73,
+ "reg": 2,
+ "ftd": 4,
+ "spend": 47.45
+ },
+ {
+ "dt": "2026-07-13",
+ "pkg": "com.f111.kk444",
+ "installs": 4,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 2.4
+ },
+ {
+ "dt": "2026-07-13",
+ "pkg": "com.ic.ek77_85",
+ "installs": 5,
+ "reg": 6,
+ "ftd": 2,
+ "spend": 2.5
+ },
+ {
+ "dt": "2026-07-13",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 60,
+ "reg": 1,
+ "ftd": 2,
+ "spend": 39
+ },
+ {
+ "dt": "2026-07-14",
+ "pkg": "com.f111.kk444",
+ "installs": 129,
+ "reg": 0,
+ "ftd": 2,
+ "spend": 77.4
+ },
+ {
+ "dt": "2026-07-14",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 60,
+ "reg": 0,
+ "ftd": 5,
+ "spend": 39
+ },
+ {
+ "dt": "2026-07-15",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 69,
+ "reg": 2,
+ "ftd": 6,
+ "spend": 44.85
+ },
+ {
+ "dt": "2026-07-16",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 85,
+ "reg": 5,
+ "ftd": 16,
+ "spend": 55.25
+ },
+ {
+ "dt": "2026-07-17",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 51,
+ "reg": 5,
+ "ftd": 13,
+ "spend": 33.15
+ },
+ {
+ "dt": "2026-07-18",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 65,
+ "reg": 6,
+ "ftd": 22,
+ "spend": 42.25
+ },
+ {
+ "dt": "2026-07-19",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 61,
+ "reg": 7,
+ "ftd": 19,
+ "spend": 39.65
+ },
+ {
+ "dt": "2026-07-20",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 68,
+ "reg": 3,
+ "ftd": 20,
+ "spend": 44.2
+ },
+ {
+ "dt": "2026-07-21",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 76,
+ "reg": 10,
+ "ftd": 18,
+ "spend": 49.4
+ },
+ {
+ "dt": "2026-07-22",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 76,
+ "reg": 17,
+ "ftd": 27,
+ "spend": 49.4
+ },
+ {
+ "dt": "2026-07-23",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 55,
+ "reg": 6,
+ "ftd": 18,
+ "spend": 35.75
+ },
+ {
+ "dt": "2026-07-24",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 63,
+ "reg": 7,
+ "ftd": 16,
+ "spend": 40.95
+ },
+ {
+ "dt": "2026-07-25",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 79,
+ "reg": 14,
+ "ftd": 28,
+ "spend": 51.35
+ },
+ {
+ "dt": "2026-07-26",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 70,
+ "reg": 12,
+ "ftd": 31,
+ "spend": 45.5
+ },
+ {
+ "dt": "2026-07-27",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 54,
+ "reg": 7,
+ "ftd": 18,
+ "spend": 35.1
+ },
+ {
+ "dt": "2026-07-28",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 46,
+ "reg": 7,
+ "ftd": 17,
+ "spend": 29.9
+ },
+ {
+ "dt": "2026-07-29",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 64,
+ "reg": 8,
+ "ftd": 25,
+ "spend": 41.6
+ },
+ {
+ "dt": "2026-07-30",
+ "pkg": "com.ic.ek77_85",
+ "installs": 16,
+ "reg": 6,
+ "ftd": 0,
+ "spend": 8
+ },
+ {
+ "dt": "2026-07-30",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 82,
+ "reg": 9,
+ "ftd": 22,
+ "spend": 53.3
+ },
+ {
+ "dt": "2026-07-31",
+ "pkg": "com.ic.ek77_85",
+ "installs": 1,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0.5
+ },
+ {
+ "dt": "2026-07-31",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 109,
+ "reg": 8,
+ "ftd": 28,
+ "spend": 70.85
+ },
+ {
+ "dt": "2026-08-01",
+ "pkg": "com.ic.ek77_85",
+ "installs": 1,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0.5
+ },
+ {
+ "dt": "2026-08-01",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 98,
+ "reg": 9,
+ "ftd": 25,
+ "spend": 63.7
+ },
+ {
+ "dt": "2026-08-02",
+ "pkg": "com.ic.ek77_85",
+ "installs": 4,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 2
+ },
+ {
+ "dt": "2026-08-02",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 73,
+ "reg": 12,
+ "ftd": 30,
+ "spend": 47.45
+ },
+ {
+ "dt": "2026-08-03",
+ "pkg": "com.ic.ek77_85",
+ "installs": 3,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 1.5
+ },
+ {
+ "dt": "2026-08-03",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 113,
+ "reg": 13,
+ "ftd": 36,
+ "spend": 73.45
+ },
+ {
+ "dt": "2026-08-04",
+ "pkg": "com.ic.ek77_85",
+ "installs": 9,
+ "reg": 3,
+ "ftd": 0,
+ "spend": 4.5
+ },
+ {
+ "dt": "2026-08-04",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 82,
+ "reg": 5,
+ "ftd": 26,
+ "spend": 53.3
+ },
+ {
+ "dt": "2026-08-05",
+ "pkg": "com.ic.ek77_85",
+ "installs": 44,
+ "reg": 26,
+ "ftd": 4,
+ "spend": 22
+ },
+ {
+ "dt": "2026-08-05",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 116,
+ "reg": 8,
+ "ftd": 32,
+ "spend": 75.4
+ },
+ {
+ "dt": "2026-08-06",
+ "pkg": "com.ic.ek77_85",
+ "installs": 20,
+ "reg": 10,
+ "ftd": 0,
+ "spend": 10
+ },
+ {
+ "dt": "2026-08-06",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 134,
+ "reg": 13,
+ "ftd": 37,
+ "spend": 87.1
+ },
+ {
+ "dt": "2026-08-07",
+ "pkg": "com.ic.ek77_85",
+ "installs": 42,
+ "reg": 22,
+ "ftd": 2,
+ "spend": 21
+ },
+ {
+ "dt": "2026-08-07",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 129,
+ "reg": 21,
+ "ftd": 36,
+ "spend": 83.85
+ },
+ {
+ "dt": "2026-08-08",
+ "pkg": "com.ic.ek77_85",
+ "installs": 22,
+ "reg": 12,
+ "ftd": 1,
+ "spend": 11
+ },
+ {
+ "dt": "2026-08-08",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 121,
+ "reg": 11,
+ "ftd": 28,
+ "spend": 78.65
+ },
+ {
+ "dt": "2026-08-09",
+ "pkg": "com.ic.ek77_85",
+ "installs": 42,
+ "reg": 23,
+ "ftd": 0,
+ "spend": 21
+ },
+ {
+ "dt": "2026-08-09",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 110,
+ "reg": 13,
+ "ftd": 27,
+ "spend": 71.5
+ },
+ {
+ "dt": "2026-08-10",
+ "pkg": "com.ic.ek77_85",
+ "installs": 23,
+ "reg": 12,
+ "ftd": 2,
+ "spend": 11.5
+ },
+ {
+ "dt": "2026-08-10",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 64,
+ "reg": 8,
+ "ftd": 19,
+ "spend": 41.6
+ },
+ {
+ "dt": "2026-08-10",
+ "pkg": "com.yogame.com",
+ "installs": 0,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0
+ },
+ {
+ "dt": "2026-08-11",
+ "pkg": "com.ic.ek77_85",
+ "installs": 38,
+ "reg": 23,
+ "ftd": 1,
+ "spend": 19
+ },
+ {
+ "dt": "2026-08-11",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 68,
+ "reg": 10,
+ "ftd": 13,
+ "spend": 44.2
+ },
+ {
+ "dt": "2026-08-11",
+ "pkg": "com.yogame.com",
+ "installs": 0,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0
+ },
+ {
+ "dt": "2026-08-12",
+ "pkg": "com.ic.ek77_85",
+ "installs": 38,
+ "reg": 27,
+ "ftd": 4,
+ "spend": 19
+ },
+ {
+ "dt": "2026-08-12",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 72,
+ "reg": 10,
+ "ftd": 16,
+ "spend": 46.8
+ },
+ {
+ "dt": "2026-08-12",
+ "pkg": "com.yogame.com",
+ "installs": 0,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0
+ },
+ {
+ "dt": "2026-08-13",
+ "pkg": "com.ic.ek77_85",
+ "installs": 49,
+ "reg": 36,
+ "ftd": 4,
+ "spend": 24.5
+ },
+ {
+ "dt": "2026-08-13",
+ "pkg": "com.playmate.playzone",
+ "installs": 0,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0
+ },
+ {
+ "dt": "2026-08-13",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 83,
+ "reg": 9,
+ "ftd": 21,
+ "spend": 53.95
+ },
+ {
+ "dt": "2026-08-13",
+ "pkg": "com.yogame.com",
+ "installs": 27,
+ "reg": 7,
+ "ftd": 1,
+ "spend": 13.5
+ },
+ {
+ "dt": "2026-08-14",
+ "pkg": "com.ic.ek77_85",
+ "installs": 73,
+ "reg": 51,
+ "ftd": 5,
+ "spend": 36.5
+ },
+ {
+ "dt": "2026-08-14",
+ "pkg": "com.playmate.playzone",
+ "installs": 2,
+ "reg": 2,
+ "ftd": 0,
+ "spend": 1.3
+ },
+ {
+ "dt": "2026-08-14",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 58,
+ "reg": 9,
+ "ftd": 15,
+ "spend": 37.7
+ },
+ {
+ "dt": "2026-08-14",
+ "pkg": "com.yogame.com",
+ "installs": 41,
+ "reg": 12,
+ "ftd": 3,
+ "spend": 20.5
+ },
+ {
+ "dt": "2026-08-15",
+ "pkg": "com.ic.ek77_85",
+ "installs": 60,
+ "reg": 48,
+ "ftd": 1,
+ "spend": 30
+ },
+ {
+ "dt": "2026-08-15",
+ "pkg": "com.playmate.playzone",
+ "installs": 1,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0.65
+ },
+ {
+ "dt": "2026-08-15",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 59,
+ "reg": 13,
+ "ftd": 15,
+ "spend": 38.35
+ },
+ {
+ "dt": "2026-08-15",
+ "pkg": "com.yogame.com",
+ "installs": 18,
+ "reg": 5,
+ "ftd": 3,
+ "spend": 9
+ },
+ {
+ "dt": "2026-08-16",
+ "pkg": "com.ic.ek77_85",
+ "installs": 83,
+ "reg": 56,
+ "ftd": 6,
+ "spend": 41.5
+ },
+ {
+ "dt": "2026-08-16",
+ "pkg": "com.playmate.playzone",
+ "installs": 1,
+ "reg": 1,
+ "ftd": 1,
+ "spend": 0.65
+ },
+ {
+ "dt": "2026-08-16",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 56,
+ "reg": 6,
+ "ftd": 17,
+ "spend": 36.4
+ },
+ {
+ "dt": "2026-08-16",
+ "pkg": "com.yogame.com",
+ "installs": 75,
+ "reg": 21,
+ "ftd": 1,
+ "spend": 37.5
+ },
+ {
+ "dt": "2026-08-17",
+ "pkg": "com.ic.ek77_85",
+ "installs": 74,
+ "reg": 50,
+ "ftd": 7,
+ "spend": 37
+ },
+ {
+ "dt": "2026-08-17",
+ "pkg": "com.playmate.playzone",
+ "installs": 0,
+ "reg": 0,
+ "ftd": 1,
+ "spend": 0
+ },
+ {
+ "dt": "2026-08-17",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 93,
+ "reg": 12,
+ "ftd": 25,
+ "spend": 60.45
+ },
+ {
+ "dt": "2026-08-17",
+ "pkg": "com.yogame.com",
+ "installs": 97,
+ "reg": 28,
+ "ftd": 3,
+ "spend": 48.5
+ },
+ {
+ "dt": "2026-08-18",
+ "pkg": "com.ic.ek77_85",
+ "installs": 90,
+ "reg": 54,
+ "ftd": 3,
+ "spend": 45
+ },
+ {
+ "dt": "2026-08-18",
+ "pkg": "com.playmate.playzone",
+ "installs": 1,
+ "reg": 1,
+ "ftd": 0,
+ "spend": 0.65
+ },
+ {
+ "dt": "2026-08-18",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 110,
+ "reg": 7,
+ "ftd": 21,
+ "spend": 71.5
+ },
+ {
+ "dt": "2026-08-18",
+ "pkg": "com.yogame.com",
+ "installs": 95,
+ "reg": 26,
+ "ftd": 7,
+ "spend": 47.5
+ },
+ {
+ "dt": "2026-08-19",
+ "pkg": "com.ic.ek77_85",
+ "installs": 72,
+ "reg": 45,
+ "ftd": 4,
+ "spend": 36
+ },
+ {
+ "dt": "2026-08-19",
+ "pkg": "com.playmate.playzone",
+ "installs": 1,
+ "reg": 1,
+ "ftd": 1,
+ "spend": 0.65
+ },
+ {
+ "dt": "2026-08-19",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 96,
+ "reg": 16,
+ "ftd": 28,
+ "spend": 62.4
+ },
+ {
+ "dt": "2026-08-19",
+ "pkg": "com.yogame.com",
+ "installs": 96,
+ "reg": 34,
+ "ftd": 5,
+ "spend": 48
+ },
+ {
+ "dt": "2026-08-20",
+ "pkg": "com.ic.ek77_85",
+ "installs": 44,
+ "reg": 25,
+ "ftd": 4,
+ "spend": 22
+ },
+ {
+ "dt": "2026-08-20",
+ "pkg": "com.playmate.playzone",
+ "installs": 0,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0
+ },
+ {
+ "dt": "2026-08-20",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 111,
+ "reg": 9,
+ "ftd": 31,
+ "spend": 72.15
+ },
+ {
+ "dt": "2026-08-20",
+ "pkg": "com.yogame.com",
+ "installs": 100,
+ "reg": 31,
+ "ftd": 9,
+ "spend": 50
+ },
+ {
+ "dt": "2026-08-21",
+ "pkg": "com.ic.ek77_85",
+ "installs": 102,
+ "reg": 71,
+ "ftd": 7,
+ "spend": 51
+ },
+ {
+ "dt": "2026-08-21",
+ "pkg": "com.playmate.playzone",
+ "installs": 0,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0
+ },
+ {
+ "dt": "2026-08-21",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 110,
+ "reg": 11,
+ "ftd": 35,
+ "spend": 71.5
+ },
+ {
+ "dt": "2026-08-21",
+ "pkg": "com.yogame.com",
+ "installs": 192,
+ "reg": 51,
+ "ftd": 12,
+ "spend": 96
+ },
+ {
+ "dt": "2026-08-22",
+ "pkg": "com.ic.ek77_85",
+ "installs": 123,
+ "reg": 92,
+ "ftd": 12,
+ "spend": 61.5
+ },
+ {
+ "dt": "2026-08-22",
+ "pkg": "com.playmate.playzone",
+ "installs": 1,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0.65
+ },
+ {
+ "dt": "2026-08-22",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 131,
+ "reg": 19,
+ "ftd": 23,
+ "spend": 85.15
+ },
+ {
+ "dt": "2026-08-22",
+ "pkg": "com.yogame.com",
+ "installs": 195,
+ "reg": 74,
+ "ftd": 15,
+ "spend": 97.5
+ },
+ {
+ "dt": "2026-08-23",
+ "pkg": "com.ic.ek77_85",
+ "installs": 116,
+ "reg": 83,
+ "ftd": 9,
+ "spend": 58
+ },
+ {
+ "dt": "2026-08-23",
+ "pkg": "com.playmate.playzone",
+ "installs": 1,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0.65
+ },
+ {
+ "dt": "2026-08-23",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 124,
+ "reg": 13,
+ "ftd": 34,
+ "spend": 80.6
+ },
+ {
+ "dt": "2026-08-23",
+ "pkg": "com.yogame.com",
+ "installs": 197,
+ "reg": 52,
+ "ftd": 14,
+ "spend": 98.5
+ },
+ {
+ "dt": "2026-08-24",
+ "pkg": "com.ic.ek77_85",
+ "installs": 146,
+ "reg": 111,
+ "ftd": 11,
+ "spend": 73
+ },
+ {
+ "dt": "2026-08-24",
+ "pkg": "com.playmate.playzone",
+ "installs": 0,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0
+ },
+ {
+ "dt": "2026-08-24",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 94,
+ "reg": 7,
+ "ftd": 28,
+ "spend": 61.1
+ },
+ {
+ "dt": "2026-08-24",
+ "pkg": "com.yogame.com",
+ "installs": 184,
+ "reg": 47,
+ "ftd": 7,
+ "spend": 92
+ },
+ {
+ "dt": "2026-08-25",
+ "pkg": "com.ic.ek77_85",
+ "installs": 89,
+ "reg": 58,
+ "ftd": 5,
+ "spend": 44.5
+ },
+ {
+ "dt": "2026-08-25",
+ "pkg": "com.playmate.playzone",
+ "installs": 0,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0
+ },
+ {
+ "dt": "2026-08-25",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 107,
+ "reg": 14,
+ "ftd": 36,
+ "spend": 69.55
+ },
+ {
+ "dt": "2026-08-25",
+ "pkg": "com.yogame.com",
+ "installs": 214,
+ "reg": 57,
+ "ftd": 6,
+ "spend": 107
+ },
+ {
+ "dt": "2026-08-26",
+ "pkg": "com.ic.ek77_85",
+ "installs": 96,
+ "reg": 63,
+ "ftd": 7,
+ "spend": 48
+ },
+ {
+ "dt": "2026-08-26",
+ "pkg": "com.playmate.playzone",
+ "installs": 0,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0
+ },
+ {
+ "dt": "2026-08-26",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 96,
+ "reg": 8,
+ "ftd": 32,
+ "spend": 62.4
+ },
+ {
+ "dt": "2026-08-26",
+ "pkg": "com.yogame.com",
+ "installs": 35,
+ "reg": 12,
+ "ftd": 5,
+ "spend": 17.5
+ },
+ {
+ "dt": "2026-08-27",
+ "pkg": "com.ic.ek77_85",
+ "installs": 71,
+ "reg": 45,
+ "ftd": 6,
+ "spend": 35.5
+ },
+ {
+ "dt": "2026-08-27",
+ "pkg": "com.playmate.playzone",
+ "installs": 0,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0
+ },
+ {
+ "dt": "2026-08-27",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 71,
+ "reg": 6,
+ "ftd": 18,
+ "spend": 46.15
+ },
+ {
+ "dt": "2026-08-28",
+ "pkg": "com.ic.ek77_85",
+ "installs": 97,
+ "reg": 59,
+ "ftd": 10,
+ "spend": 48.5
+ },
+ {
+ "dt": "2026-08-28",
+ "pkg": "com.playmate.playzone",
+ "installs": 0,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0
+ },
+ {
+ "dt": "2026-08-28",
+ "pkg": "com.rkgg.com2",
+ "installs": 3,
+ "reg": 1,
+ "ftd": 0,
+ "spend": 2.4
+ },
+ {
+ "dt": "2026-08-28",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 58,
+ "reg": 11,
+ "ftd": 20,
+ "spend": 37.7
+ },
+ {
+ "dt": "2026-08-29",
+ "pkg": "com.ic.ek77_85",
+ "installs": 102,
+ "reg": 71,
+ "ftd": 12,
+ "spend": 51
+ },
+ {
+ "dt": "2026-08-29",
+ "pkg": "com.playmate.playzone",
+ "installs": 0,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0
+ },
+ {
+ "dt": "2026-08-29",
+ "pkg": "com.rkgg.com2",
+ "installs": 1,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0.8
+ },
+ {
+ "dt": "2026-08-29",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 82,
+ "reg": 7,
+ "ftd": 20,
+ "spend": 53.3
+ },
+ {
+ "dt": "2026-08-30",
+ "pkg": "com.ic.ek77_85",
+ "installs": 152,
+ "reg": 102,
+ "ftd": 13,
+ "spend": 76
+ },
+ {
+ "dt": "2026-08-30",
+ "pkg": "com.playmate.playzone",
+ "installs": 0,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0
+ },
+ {
+ "dt": "2026-08-30",
+ "pkg": "com.rkgg.com2",
+ "installs": 0,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0
+ },
+ {
+ "dt": "2026-08-30",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 63,
+ "reg": 4,
+ "ftd": 13,
+ "spend": 40.95
+ },
+ {
+ "dt": "2026-08-31",
+ "pkg": "com.ic.ek77_85",
+ "installs": 125,
+ "reg": 89,
+ "ftd": 13,
+ "spend": 62.5
+ },
+ {
+ "dt": "2026-08-31",
+ "pkg": "com.playmate.playzone",
+ "installs": 0,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0
+ },
+ {
+ "dt": "2026-08-31",
+ "pkg": "com.rkgg.com2",
+ "installs": 0,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0
+ },
+ {
+ "dt": "2026-08-31",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 61,
+ "reg": 6,
+ "ftd": 14,
+ "spend": 39.65
+ },
+ {
+ "dt": "2026-09-01",
+ "pkg": "com.ic.ek77_85",
+ "installs": 110,
+ "reg": 66,
+ "ftd": 7,
+ "spend": 55
+ },
+ {
+ "dt": "2026-09-01",
+ "pkg": "com.playmate.playzone",
+ "installs": 0,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0
+ },
+ {
+ "dt": "2026-09-01",
+ "pkg": "com.rkgg.com2",
+ "installs": 0,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0
+ },
+ {
+ "dt": "2026-09-01",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 68,
+ "reg": 10,
+ "ftd": 4,
+ "spend": 44.2
+ },
+ {
+ "dt": "2026-09-02",
+ "pkg": "com.ic.ek77_85",
+ "installs": 120,
+ "reg": 65,
+ "ftd": 11,
+ "spend": 60
+ },
+ {
+ "dt": "2026-09-02",
+ "pkg": "com.playmate.playzone",
+ "installs": 0,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0
+ },
+ {
+ "dt": "2026-09-02",
+ "pkg": "com.rkgg.com2",
+ "installs": 0,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0
+ },
+ {
+ "dt": "2026-09-02",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 62,
+ "reg": 4,
+ "ftd": 20,
+ "spend": 40.3
+ },
+ {
+ "dt": "2026-09-03",
+ "pkg": "com.ic.ek77_85",
+ "installs": 19,
+ "reg": 18,
+ "ftd": 8,
+ "spend": 9.5
+ },
+ {
+ "dt": "2026-09-03",
+ "pkg": "com.playmate.playzone",
+ "installs": 0,
+ "reg": 1,
+ "ftd": 1,
+ "spend": 0
+ },
+ {
+ "dt": "2026-09-03",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 63,
+ "reg": 9,
+ "ftd": 18,
+ "spend": 40.95
+ },
+ {
+ "dt": "2026-09-04",
+ "pkg": "com.playmate.playzone",
+ "installs": 0,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0
+ },
+ {
+ "dt": "2026-09-04",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 72,
+ "reg": 5,
+ "ftd": 13,
+ "spend": 46.8
+ },
+ {
+ "dt": "2026-09-05",
+ "pkg": "com.playmate.playzone",
+ "installs": 0,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0
+ },
+ {
+ "dt": "2026-09-05",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 70,
+ "reg": 3,
+ "ftd": 16,
+ "spend": 45.5
+ },
+ {
+ "dt": "2026-09-06",
+ "pkg": "com.playmate.playzone",
+ "installs": 0,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0
+ },
+ {
+ "dt": "2026-09-06",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 72,
+ "reg": 7,
+ "ftd": 17,
+ "spend": 46.8
+ },
+ {
+ "dt": "2026-09-07",
+ "pkg": "com.playmate.playzone",
+ "installs": 0,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0
+ },
+ {
+ "dt": "2026-09-07",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 65,
+ "reg": 7,
+ "ftd": 16,
+ "spend": 42.25
+ },
+ {
+ "dt": "2026-09-08",
+ "pkg": "com.playmate.playzone",
+ "installs": 0,
+ "reg": 0,
+ "ftd": 0,
+ "spend": 0
+ },
+ {
+ "dt": "2026-09-08",
+ "pkg": "com.winaxis.arionplayandroidapp",
+ "installs": 62,
+ "reg": 6,
+ "ftd": 24,
+ "spend": 40.3
+ }
+ ],
+ "recharges": [
+ {
+ "date": "2026-06-12",
+ "paid": 1000,
+ "amount": 975
+ },
+ {
+ "date": "2026-07-14",
+ "paid": 1000,
+ "amount": 975
+ },
+ {
+ "date": "2026-07-22",
+ "paid": 3000,
+ "amount": 2975
+ },
+ {
+ "date": "2026-08-13",
+ "paid": 3000,
+ "amount": 2975
+ },
+ {
+ "date": "2026-08-27",
+ "paid": 3000,
+ "amount": 2975
+ }
+ ]
}
diff --git a/index.html b/index.html
index f4dd5ef..ce15256 100644
--- a/index.html
+++ b/index.html
@@ -10,21 +10,22 @@
color-scheme: light;
--page: #f9f9f7; --surface: #fcfcfb; --ink: #0b0b0b; --ink-2: #52514e; --muted: #898781;
--grid: #e1e0d9; --axis: #c3c2b7; --border: rgba(11,11,11,.10);
- --s1: #2a78d6; --s2: #eb6834; --warn: #fab219; --crit: #d03b3b;
+ --s1: #2a78d6; --s2: #eb6834; --s3: #1baf7a; --s4: #eda100; --s5: #e87ba4; --other: #a3a29b;
+ --warn: #fab219; --crit: #d03b3b;
}
@media (prefers-color-scheme: dark) {
:root:where(:not([data-theme="light"])) {
color-scheme: dark;
--page: #0d0d0d; --surface: #1a1a19; --ink: #ffffff; --ink-2: #c3c2b7; --muted: #898781;
--grid: #2c2c2a; --axis: #383835; --border: rgba(255,255,255,.10);
- --s1: #3987e5; --s2: #d95926;
+ --s1: #3987e5; --s2: #d95926; --s3: #199e70; --s4: #c98500; --s5: #d55181; --other: #6b6a65;
}
}
:root[data-theme="dark"] {
color-scheme: dark;
--page: #0d0d0d; --surface: #1a1a19; --ink: #ffffff; --ink-2: #c3c2b7; --muted: #898781;
--grid: #2c2c2a; --axis: #383835; --border: rgba(255,255,255,.10);
- --s1: #3987e5; --s2: #d95926;
+ --s1: #3987e5; --s2: #d95926; --s3: #199e70; --s4: #c98500; --s5: #d55181; --other: #6b6a65;
}
* { box-sizing: border-box; }
body { margin: 0; background: var(--page); color: var(--ink); font: 14px/1.5 system-ui, -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; }
@@ -39,7 +40,13 @@ h1 { font-size: 22px; font-weight: 600; margin: 0 0 4px; }
.tile-sub { color: var(--muted); font-size: 12px; margin-top: 4px; }
.card { padding: 16px 18px; margin-bottom: 16px; }
.card h2 { font-size: 15px; font-weight: 600; margin: 0; }
+.card-head { display: flex; flex-wrap: wrap; align-items: flex-start; justify-content: space-between; gap: 8px 16px; }
.desc { color: var(--ink-2); font-size: 12px; margin: 2px 0 12px; }
+.seg { display: inline-flex; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
+.seg button { font: inherit; font-size: 12px; border: 0; background: transparent; color: var(--ink-2); padding: 4px 10px; cursor: pointer; }
+.seg button + button { border-left: 1px solid var(--border); }
+.seg button[aria-pressed="true"] { background: var(--ink); color: var(--surface); }
+select { font: inherit; font-size: 12px; color: var(--ink); background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 4px 8px; max-width: 100%; }
.legend { display: flex; flex-wrap: wrap; gap: 4px 18px; font-size: 12px; color: var(--ink-2); margin-bottom: 4px; }
.sw { display: inline-block; width: 10px; height: 10px; border-radius: 3px; margin-right: 6px; vertical-align: -1px; flex: none; }
.chart { position: relative; }
@@ -52,20 +59,19 @@ h1 { font-size: 22px; font-weight: 600; margin: 0 0 4px; }
.line { fill: none; stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.dot { stroke: var(--surface); stroke-width: 2; }
.cross { stroke: var(--axis); stroke-width: 1; }
-.tip { position: absolute; z-index: 2; pointer-events: none; min-width: 170px; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; box-shadow: 0 6px 20px rgba(0,0,0,.14); padding: 8px 10px; font-size: 12px; }
+.tip { position: absolute; z-index: 2; pointer-events: none; min-width: 180px; max-width: 340px; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; box-shadow: 0 6px 20px rgba(0,0,0,.14); padding: 8px 10px; font-size: 12px; }
.tip-title { font-weight: 600; margin-bottom: 4px; }
.tip-row { display: flex; align-items: center; gap: 6px; }
-.tip-name { flex: 1; color: var(--ink-2); }
+.tip-name { flex: 1; color: var(--ink-2); overflow-wrap: anywhere; }
.tip-val { font-weight: 600; font-variant-numeric: tabular-nums; }
.tip-sub { color: var(--muted); padding-left: 16px; margin-bottom: 2px; }
.tip-total { border-top: 1px solid var(--grid); margin-top: 4px; padding-top: 4px; }
-.empty { color: var(--ink-2); border: 1px dashed var(--axis); border-radius: 8px; padding: 18px; font-size: 13px; }
.table-wrap { overflow-x: auto; }
table { border-collapse: collapse; width: 100%; font-size: 13px; white-space: nowrap; }
th { text-align: left; color: var(--muted); font-weight: 500; font-size: 12px; padding: 6px 10px; border-bottom: 1px solid var(--axis); }
td { padding: 7px 10px; border-bottom: 1px solid var(--grid); }
th.num, td.num { text-align: right; font-variant-numeric: tabular-nums; }
-td.muted { color: var(--muted); }
+tr.sub td { font-weight: 600; border-top: 1px solid var(--axis); }
tfoot td { font-weight: 600; border-bottom: none; }
.badge { display: inline-flex; align-items: center; gap: 5px; color: var(--ink); }
.badge-icon { display: inline-grid; place-items: center; width: 15px; height: 15px; border-radius: 50%; font-size: 10px; font-weight: 700; color: #0b0b0b; }
@@ -78,7 +84,7 @@ footer { color: var(--muted); font-size: 12px; margin-top: 8px; }
- Tecdo 客户消耗与余额
+ Tecdo(钛动)消耗与余额
加载中…
@@ -86,45 +92,71 @@ footer { color: var(--muted); font-size: 12px; margin-top: 8px; }
- 近 7 天每日消耗
- 两个订单按天堆叠,悬停查看每个订单的消耗、安装和点击
+
+
+
每日消耗
+
按包堆叠,悬停查看当天每个包的消耗、注册和首充
+
+
+
+
+
+
+
余额走势
- 每天日终余额 = 截至当天的累计充值 − 累计消耗
- 还没有充值记录。把充值日期和金额发给 Claude,或者编辑仓库里的 manual.json,余额和走势会自动算出来。
+ 每天日终余额 = 截至当天的累计到账 − 累计消耗,圆点是到账日
- 每日明细
- 按日期倒序,每个订单一行
+ 分包汇总
+ 全部历史,按消耗从高到低;成本 = 消耗 ÷ 首充
-
- | 日期 | 订单 | App | pkg name | 消耗 | 安装 | 点击 | eCPI |
+
- 订单信息
+ 分月汇总
+ 每月先列合计,再列当月有数据的包
-
- | 订单 | 订单名 | App | pkg name | 开始日期 | 出价 | 日预算 | 累计消耗 |
+
-
+
+
+
充值记录
+ 余额按到账金额计算
@@ -141,7 +173,11 @@ const h = (tag, cls, text) => { const e = document.createElement(tag); if (cls)
const r2 = v => Math.round(v * 100) / 100;
const usd = (v, dp = 2) => (v < 0 ? '−$' : '$') + Math.abs(v).toLocaleString('en-US', { minimumFractionDigits: dp, maximumFractionDigits: dp });
const int = v => Number(v || 0).toLocaleString('en-US');
+const costText = (spend, ftd) => ftd ? usd(spend / ftd) : '—';
const shortDate = dt => dt.slice(5).replace('-', '/');
+const addDays = (s, n) => { const t = new Date(s + 'T00:00:00Z'); t.setUTCDate(t.getUTCDate() + n); return t.toISOString().slice(0, 10); };
+const dayRange = (a, b) => { const out = []; for (let s = a; s <= b; s = addDays(s, 1)) out.push(s); return out; };
+const SLOTS = 5; // 颜色槽位数,排不进的包合并成「其他」
function niceStep(raw) {
const p = 10 ** Math.floor(Math.log10(raw)), n = raw / p;
@@ -157,50 +193,80 @@ function scaleY(min, max, count = 4) {
}
const roundTop = (x, y, w, hgt, r) =>
`M${x},${y + hgt}V${y + r}Q${x},${y} ${x + r},${y}H${x + w - r}Q${x + w},${y} ${x + w},${y + r}V${y + hgt}Z`;
+const labelStep = (n, pw) => Math.max(1, Math.ceil(n / Math.max(1, Math.floor(pw / 52))));
function showTip(wrap, x, y, content) {
const t = wrap.querySelector('.tip');
t.replaceChildren(content);
t.hidden = false;
const left = Math.min(Math.max(x - t.offsetWidth / 2, 4), wrap.clientWidth - t.offsetWidth - 4);
- t.style.left = left + 'px';
+ t.style.left = Math.max(0, left) + 'px';
t.style.top = Math.max(4, y - t.offsetHeight - 14) + 'px';
}
const hideTip = wrap => { wrap.querySelector('.tip').hidden = true; };
const swatch = color => { const s = h('i', 'sw'); s.style.background = color; return s; };
-
-function derive(d, m) {
- const pkgs = m.pkg_names || {};
- const camps = d.campaigns.map((c, i) => ({ ...c, color: `var(--s${i + 1})`, pkg: pkgs[c.id] || '' }));
- const dates = [...new Set(d.daily.map(r => r.dt))].sort();
- const cell = {};
- d.daily.forEach(r => { (cell[r.dt] ??= {})[r.campaign_id] = r; });
- const get = (dt, id) => (cell[dt] || {})[id];
- const dayTotal = dt => r2(camps.reduce((s, c) => s + (get(dt, c.id)?.spend || 0), 0));
- const rangeSpend = r2(d.daily.reduce((s, r) => s + r.spend, 0));
- const rangeInstalls = d.daily.reduce((s, r) => s + r.installs, 0);
- const rangeClicks = d.daily.reduce((s, r) => s + r.clicks, 0);
- const lifetime = r2(camps.reduce((s, c) => s + c.lifetime_spend, 0));
- const recharges = (m.recharges || []).filter(x => x.date && Number(x.amount)).sort((a, b) => a.date.localeCompare(b.date));
- const rechargeTotal = r2(recharges.reduce((s, x) => s + Number(x.amount), 0));
- let cum = lifetime - rangeSpend;
- const balanceSeries = dates.map(dt => {
- cum += dayTotal(dt);
- const rc = recharges.filter(x => x.date <= dt).reduce((s, x) => s + Number(x.amount), 0);
- return { dt, v: r2(rc - cum), spend: dayTotal(dt), recharge: r2(recharges.filter(x => x.date === dt).reduce((s, x) => s + Number(x.amount), 0)) };
- });
- return {
- camps, dates, get, dayTotal, rangeSpend, rangeInstalls, rangeClicks, lifetime, recharges, rechargeTotal,
- hasRecharge: recharges.length > 0, balance: r2(rechargeTotal - lifetime), balanceSeries,
- avgDaily: dates.length ? rangeSpend / dates.length : 0,
- };
-}
-
function badge(kind, icon, text) {
const b = h('span', 'badge ' + kind);
b.append(h('span', 'badge-icon', icon), document.createTextNode(text));
return b;
}
+function tr(cells) {
+ const row = h('tr');
+ cells.forEach(c => {
+ const td = h('td', c.cls || '');
+ if (c.color) td.append(swatch(c.color));
+ td.append(document.createTextNode(c.text));
+ row.append(td);
+ });
+ return row;
+}
+const sumRows = rows => rows.reduce((a, r) => ({ installs: a.installs + r.installs, reg: a.reg + r.reg, ftd: a.ftd + r.ftd, spend: r2(a.spend + r.spend) }), { installs: 0, reg: 0, ftd: 0, spend: 0 });
+const metricCells = t => [
+ { text: int(t.installs), cls: 'num' }, { text: int(t.reg), cls: 'num' }, { text: int(t.ftd), cls: 'num' },
+ { text: usd(t.spend), cls: 'num' }, { text: costText(t.spend, t.ftd), cls: 'num' },
+];
+
+function derive(d) {
+ const end = d.range.end;
+ const groups = {};
+ d.daily.forEach(r => (groups[r.pkg] ??= []).push(r));
+ const pkgs = Object.entries(groups).map(([pkg, rows]) => ({
+ pkg,
+ first: rows.reduce((m, r) => r.dt < m ? r.dt : m, rows[0].dt),
+ last: rows.reduce((m, r) => r.dt > m ? r.dt : m, rows[0].dt),
+ ...sumRows(rows),
+ })).sort((a, b) => b.spend - a.spend || a.pkg.localeCompare(b.pkg));
+
+ // 颜色跟着包走:按全部历史消耗排名分配,切换时间范围不重新上色
+ const series = pkgs.slice(0, SLOTS).map((p, i) => ({ label: p.pkg, color: `var(--s${i + 1})`, members: [p.pkg] }));
+ if (pkgs.length > SLOTS) series.push({ label: `其他 ${pkgs.length - SLOTS} 个包`, color: 'var(--other)', members: pkgs.slice(SLOTS).map(p => p.pkg) });
+ const seriesOf = {};
+ series.forEach((s, i) => s.members.forEach(p => { seriesOf[p] = i; }));
+ const colorOf = pkg => series[seriesOf[pkg]].color;
+ pkgs.forEach(p => { p.color = colorOf(p.pkg); });
+
+ const byDate = {};
+ d.daily.forEach(r => (byDate[r.dt] ??= []).push(r));
+ const rowsOn = dt => byDate[dt] || [];
+
+ const total = sumRows(d.daily);
+ const recharges = [...d.recharges].sort((a, b) => a.date.localeCompare(b.date));
+ const received = r2(recharges.reduce((s, x) => s + x.amount, 0));
+ const paid = r2(recharges.reduce((s, x) => s + x.paid, 0));
+ const w7 = sumRows(dayRange(addDays(end, -6), end).flatMap(rowsOn));
+
+ const bStart = [d.range.start, recharges[0] && recharges[0].date].filter(Boolean).sort()[0];
+ let cumSpend = 0, cumIn = 0;
+ const balanceSeries = dayRange(bStart, end).map(dt => {
+ const spend = r2(rowsOn(dt).reduce((s, r) => s + r.spend, 0));
+ const inflow = r2(recharges.filter(x => x.date === dt).reduce((s, x) => s + x.amount, 0));
+ cumSpend += spend;
+ cumIn += inflow;
+ return { dt, v: r2(cumIn - cumSpend), spend, inflow };
+ });
+
+ return { end, pkgs, series, seriesOf, colorOf, rowsOn, total, recharges, received, paid, balance: r2(received - total.spend), w7, avg7: w7.spend / 7, balanceSeries };
+}
function renderTiles(S, d) {
const box = $('#tiles');
@@ -211,58 +277,77 @@ function renderTiles(S, d) {
if (sub) { const s = h('div', 'tile-sub'); s.append(sub); t.append(s); }
box.append(t);
};
- const firstStart = S.camps.map(c => c.start).sort()[0];
- add('近 7 天消耗', usd(S.rangeSpend), `日均 ${usd(S.avgDaily)}`);
- add('近 7 天安装', int(S.rangeInstalls), S.rangeInstalls ? `eCPI ${usd(S.rangeSpend / S.rangeInstalls)}` : '暂无安装');
- add('累计消耗', usd(S.lifetime), `${firstStart} 起,截至 ${d.range.end}`);
- add('累计充值', S.hasRecharge ? usd(S.rechargeTotal) : '待补', S.hasRecharge ? `共 ${S.recharges.length} 笔` : '还没有充值记录');
- if (!S.hasRecharge) {
- add('当前余额', '待补', '累计充值 − 累计消耗');
- } else {
- const days = S.avgDaily > 0 ? S.balance / S.avgDaily : Infinity;
- let sub;
- if (S.balance <= 0) sub = badge('crit', '✕', '余额已用完');
- else if (days < 7) sub = badge('warn', '!', `按日均约可用 ${Math.floor(days)} 天`);
- else sub = isFinite(days) ? `按日均约可用 ${Math.floor(days)} 天` : '近 7 天没有消耗';
- add('当前余额', usd(S.balance), sub);
- }
+ const days = S.avg7 > 0 ? S.balance / S.avg7 : Infinity;
+ let sub;
+ if (S.balance <= 0) sub = badge('crit', '✕', '余额已用完');
+ else if (days < 7) sub = badge('warn', '!', `按近 7 天日均约可用 ${Math.floor(days)} 天`);
+ else sub = isFinite(days) ? `按近 7 天日均约可用 ${Math.floor(days)} 天` : '近 7 天没有消耗';
+ add('当前余额', usd(S.balance), sub);
+ add('近 7 天消耗', usd(S.w7.spend), `${shortDate(addDays(S.end, -6))}–${shortDate(S.end)},日均 ${usd(S.avg7)}`);
+ add('近 7 天首充', int(S.w7.ftd), `成本 ${costText(S.w7.spend, S.w7.ftd)} · 注册 ${int(S.w7.reg)}`);
+ add('累计消耗', usd(S.total.spend), `${d.range.start} 起 · 成本 ${costText(S.total.spend, S.total.ftd)}`);
+ add('累计到账', usd(S.received), `打款 ${usd(S.paid, 0)} · 共 ${S.recharges.length} 笔`);
}
-function drawSpend(S) {
+let RANGE = '7';
+
+function drawSpend(S, d) {
const wrap = $('#spend-chart');
wrap.querySelector('svg')?.remove();
hideTip(wrap);
- const W = Math.max(300, wrap.clientWidth), H = 260, m = { l: 52, r: 12, t: 24, b: 30 };
+ const start = RANGE === 'all' ? d.range.start : addDays(S.end, -(Number(RANGE) - 1));
+ const dates = dayRange(start < d.range.start ? d.range.start : start, S.end);
+ const stacks = dates.map(dt => {
+ const acc = S.series.map(() => 0);
+ S.rowsOn(dt).forEach(r => { acc[S.seriesOf[r.pkg]] += r.spend; });
+ return acc.map(r2);
+ });
+ const totals = stacks.map(s => r2(s.reduce((a, b) => a + b, 0)));
+
+ $('#spend-legend').replaceChildren(...S.series
+ .filter((s, i) => stacks.some(st => st[i] > 0))
+ .map(s => {
+ const e = h('span');
+ e.append(swatch(s.color), document.createTextNode(s.label));
+ if (s.members.length > 1) e.title = s.members.join('\n');
+ return e;
+ }));
+
+ const W = Math.max(300, wrap.clientWidth), H = 260, m = { l: 56, r: 14, t: 24, b: 30 };
const pw = W - m.l - m.r, ph = H - m.t - m.b;
- const totals = S.dates.map(S.dayTotal);
const sc = scaleY(0, Math.max(...totals, 1));
const y = v => m.t + ph - (v - sc.lo) / (sc.hi - sc.lo) * ph;
- const band = pw / Math.max(1, S.dates.length), bw = Math.min(24, band * 0.55);
- const svg = el('svg', { viewBox: `0 0 ${W} ${H}`, width: W, height: H, role: 'img', 'aria-label': '近 7 天每日消耗,按订单堆叠' });
+ const n = dates.length, band = pw / n, bw = Math.max(2, Math.min(24, band * 0.6));
+ const clampX = x => Math.min(Math.max(x, m.l + 18), W - m.r - 18);
+ const svg = el('svg', { viewBox: `0 0 ${W} ${H}`, width: W, height: H, role: 'img', 'aria-label': '每日消耗,按包堆叠' });
wrap.prepend(svg);
const hover = el('rect', { class: 'hover-band', y: m.t, height: ph, width: band, visibility: 'hidden' }, svg);
sc.ticks.forEach(t => {
el('line', { x1: m.l, x2: W - m.r, y1: y(t), y2: y(t), class: t === 0 ? 'axis' : 'grid' }, svg);
el('text', { x: m.l - 8, y: y(t) + 4, 'text-anchor': 'end', class: 'tick' }, svg).textContent = usd(t, sc.dp);
});
- const maxI = totals.indexOf(Math.max(...totals)), lastI = totals.length - 1;
- S.dates.forEach((dt, i) => {
+ const step = labelStep(n, pw);
+ const maxI = totals.indexOf(Math.max(...totals)), lastI = n - 1;
+ dates.forEach((dt, i) => {
const cx = m.l + band * (i + 0.5);
- const segs = S.camps.map(c => ({ c, v: S.get(dt, c.id)?.spend || 0 })).filter(s => s.v > 0);
+ const segs = stacks[i].map((v, s) => ({ v, color: S.series[s].color })).filter(s => s.v > 0);
let base = 0;
- segs.forEach((s, j) => {
- const y0 = y(base), y1 = y(base + s.v), gap = j > 0 ? 2 : 0;
- const hgt = Math.max(0, y0 - y1 - gap);
- const r = j === segs.length - 1 ? Math.min(4, hgt) : 0;
- el('path', { d: roundTop(cx - bw / 2, y1, bw, hgt, r), fill: s.c.color }, svg);
- base += s.v;
+ segs.forEach((sg, j) => {
+ const y0 = y(base), y1 = y(base + sg.v), gap = j > 0 ? 2 : 0;
+ const hgt = y0 - y1 - gap;
+ base += sg.v;
+ if (hgt <= 0) return;
+ const rr = j === segs.length - 1 ? Math.min(4, hgt, bw / 2) : 0;
+ el('path', { d: roundTop(cx - bw / 2, y1, bw, hgt, rr), fill: sg.color }, svg);
});
- el('text', { x: cx, y: H - m.b + 18, 'text-anchor': 'middle', class: 'tick' }, svg).textContent = shortDate(dt);
- if ((i === maxI || i === lastI) && totals[i] > 0) {
- el('text', { x: cx, y: y(totals[i]) - 7, 'text-anchor': 'middle', class: 'val' }, svg).textContent = usd(totals[i]);
+ if ((lastI - i) % step === 0) {
+ el('text', { x: clampX(cx), y: H - m.b + 18, 'text-anchor': 'middle', class: 'tick' }, svg).textContent = shortDate(dt);
+ }
+ if (totals[i] > 0 && (i === lastI || (i === maxI && lastI - maxI >= 3 * step))) {
+ el('text', { x: clampX(cx), y: y(totals[i]) - 7, 'text-anchor': 'middle', class: 'val' }, svg).textContent = usd(totals[i]);
}
});
- S.dates.forEach((dt, i) => {
+ dates.forEach((dt, i) => {
const cx = m.l + band * (i + 0.5);
const hit = el('rect', { x: cx - band / 2, y: m.t, width: band, height: ph, fill: 'transparent' }, svg);
hit.addEventListener('pointerenter', () => {
@@ -277,16 +362,18 @@ function drawSpend(S) {
function spendTip(S, dt, total) {
const f = document.createDocumentFragment();
f.append(h('div', 'tip-title', dt));
- S.camps.forEach(c => {
- const r = S.get(dt, c.id);
+ const rows = S.rowsOn(dt).filter(r => r.spend > 0 || r.ftd > 0 || r.reg > 0).sort((a, b) => b.spend - a.spend);
+ if (!rows.length) f.append(h('div', 'tip-sub', '当天没有消耗'));
+ rows.forEach(r => {
const row = h('div', 'tip-row');
- row.append(swatch(c.color), h('span', 'tip-name', c.app || c.id), h('span', 'tip-val', usd(r?.spend || 0)));
- f.append(row);
- if (r) f.append(h('div', 'tip-sub', `安装 ${int(r.installs)} · 点击 ${int(r.clicks)}`));
+ row.append(swatch(S.colorOf(r.pkg)), h('span', 'tip-name', r.pkg), h('span', 'tip-val', usd(r.spend)));
+ f.append(row, h('div', 'tip-sub', `安装 ${int(r.installs)} · 注册 ${int(r.reg)} · 首充 ${int(r.ftd)}`));
});
- const tot = h('div', 'tip-row tip-total');
- tot.append(h('span', 'tip-name', '合计'), h('span', 'tip-val', usd(total)));
- f.append(tot);
+ if (rows.length > 1) {
+ const tot = h('div', 'tip-row tip-total');
+ tot.append(h('span', 'tip-name', '合计'), h('span', 'tip-val', usd(total)));
+ f.append(tot);
+ }
return f;
}
@@ -294,25 +381,29 @@ function drawBalance(S) {
const wrap = $('#balance-chart');
wrap.querySelector('svg')?.remove();
hideTip(wrap);
- $('#balance-empty').hidden = S.hasRecharge;
- wrap.hidden = !S.hasRecharge;
- if (!S.hasRecharge) return;
- const W = Math.max(300, wrap.clientWidth), H = 220, m = { l: 64, r: 16, t: 28, b: 30 };
+ const pts = S.balanceSeries, n = pts.length, li = n - 1;
+ if (!n) return;
+ const W = Math.max(300, wrap.clientWidth), H = 240, m = { l: 64, r: 16, t: 28, b: 30 };
const pw = W - m.l - m.r, ph = H - m.t - m.b;
- const pts = S.balanceSeries, vals = pts.map(p => p.v), li = pts.length - 1;
+ const vals = pts.map(p => p.v);
const sc = scaleY(Math.min(0, ...vals), Math.max(0, ...vals));
const y = v => m.t + ph - (v - sc.lo) / (sc.hi - sc.lo) * ph;
- const stepX = pts.length > 1 ? pw / (pts.length - 1) : 0;
- const x = i => pts.length > 1 ? m.l + i * stepX : m.l + pw / 2;
+ const stepX = n > 1 ? pw / (n - 1) : 0;
+ const x = i => n > 1 ? m.l + i * stepX : m.l + pw / 2;
+ const clampX = v => Math.min(Math.max(v, m.l + 18), W - m.r - 18);
const svg = el('svg', { viewBox: `0 0 ${W} ${H}`, width: W, height: H, role: 'img', 'aria-label': '每日日终余额走势' });
wrap.prepend(svg);
sc.ticks.forEach(t => {
el('line', { x1: m.l, x2: W - m.r, y1: y(t), y2: y(t), class: t === 0 ? 'axis' : 'grid' }, svg);
el('text', { x: m.l - 8, y: y(t) + 4, 'text-anchor': 'end', class: 'tick' }, svg).textContent = usd(t, sc.dp);
});
- pts.forEach((p, i) => { el('text', { x: x(i), y: H - m.b + 18, 'text-anchor': 'middle', class: 'tick' }, svg).textContent = shortDate(p.dt); });
+ const step = labelStep(n, pw);
+ pts.forEach((p, i) => {
+ if ((li - i) % step === 0) el('text', { x: clampX(x(i)), y: H - m.b + 18, 'text-anchor': 'middle', class: 'tick' }, svg).textContent = shortDate(p.dt);
+ });
const cross = el('line', { y1: m.t, y2: m.t + ph, class: 'cross', visibility: 'hidden' }, svg);
el('path', { d: 'M' + pts.map((p, i) => `${x(i)},${y(p.v)}`).join('L'), class: 'line', stroke: 'var(--s1)' }, svg);
+ pts.forEach((p, i) => { if (p.inflow) el('circle', { cx: x(i), cy: y(p.v), r: 4, class: 'dot', fill: 'var(--s1)' }, svg); });
el('circle', { cx: x(li), cy: y(pts[li].v), r: 4, class: 'dot', fill: 'var(--s1)' }, svg);
el('text', { x: x(li), y: y(pts[li].v) - 12, 'text-anchor': 'end', class: 'val' }, svg).textContent = usd(pts[li].v);
const hov = el('circle', { r: 5, class: 'dot', fill: 'var(--s1)', visibility: 'hidden' }, svg);
@@ -329,83 +420,85 @@ function drawBalance(S) {
const row = (name, val) => { const r = h('div', 'tip-row'); r.append(h('span', 'tip-name', name), h('span', 'tip-val', val)); f.append(r); };
row('日终余额', usd(p.v));
row('当天消耗', usd(p.spend));
- if (p.recharge) row('当天充值', usd(p.recharge));
+ if (p.inflow) row('当天到账', usd(p.inflow));
showTip(wrap, x(i), y(p.v), f);
});
hit.addEventListener('pointerleave', () => { cross.setAttribute('visibility', 'hidden'); hov.setAttribute('visibility', 'hidden'); hideTip(wrap); });
}
-function tr(cells) {
- const row = h('tr');
- cells.forEach(c => {
- const td = h('td', c.cls || '');
- if (c.color) td.append(swatch(c.color));
- td.append(document.createTextNode(c.text));
- row.append(td);
+function renderTables(S, d) {
+ $('#pkgs tbody').replaceChildren(...S.pkgs.map(p => tr([{ text: p.pkg, color: p.color }, { text: `${p.first} ~ ${p.last}` }, ...metricCells(p)])));
+ $('#pkgs tfoot').replaceChildren(tr([{ text: '合计' }, { text: `${d.range.start} ~ ${d.range.end}` }, ...metricCells(S.total)]));
+
+ const months = {};
+ d.daily.forEach(r => { ((months[r.dt.slice(0, 7)] ??= {})[r.pkg] ??= []).push(r); });
+ const mbody = $('#months tbody');
+ mbody.replaceChildren();
+ Object.keys(months).sort().reverse().forEach(mo => {
+ const sub = tr([{ text: mo }, { text: '当月合计' }, ...metricCells(sumRows(Object.values(months[mo]).flat()))]);
+ sub.className = 'sub';
+ mbody.append(sub);
+ Object.entries(months[mo])
+ .map(([pkg, rows]) => ({ pkg, ...sumRows(rows) }))
+ .filter(e => e.installs || e.reg || e.ftd || e.spend)
+ .sort((a, b) => b.spend - a.spend)
+ .forEach(e => mbody.append(tr([{ text: '' }, { text: e.pkg, color: S.colorOf(e.pkg) }, ...metricCells(e)])));
});
- return row;
-}
-function renderTables(S) {
- const pkgCell = c => c.pkg ? { text: c.pkg } : { text: '待补', cls: 'muted' };
- const daily = $('#daily tbody');
- daily.replaceChildren();
- [...S.dates].reverse().forEach(dt => S.camps.forEach(c => {
- const r = S.get(dt, c.id);
- if (!r) return;
- daily.append(tr([
- { text: dt }, { text: c.id, color: c.color }, { text: c.app || '—' }, pkgCell(c),
- { text: usd(r.spend), cls: 'num' }, { text: int(r.installs), cls: 'num' }, { text: int(r.clicks), cls: 'num' },
- { text: r.installs ? usd(r.spend / r.installs) : '—', cls: 'num' },
- ]));
- }));
- $('#daily tfoot').replaceChildren(tr([
- { text: '合计' }, { text: '' }, { text: '' }, { text: '' },
- { text: usd(S.rangeSpend), cls: 'num' }, { text: int(S.rangeInstalls), cls: 'num' }, { text: int(S.rangeClicks), cls: 'num' },
- { text: S.rangeInstalls ? usd(S.rangeSpend / S.rangeInstalls) : '—', cls: 'num' },
- ]));
+ const sel = $('#pkg-filter');
+ sel.replaceChildren(new Option('全部包', ''), ...S.pkgs.map(p => new Option(p.pkg, p.pkg)));
+ sel.onchange = () => renderDaily(S, d);
+ renderDaily(S, d);
- const camps = $('#camps tbody');
- camps.replaceChildren(...S.camps.map(c => tr([
- { text: c.id, color: c.color }, { text: c.name }, { text: c.app || '—' }, pkgCell(c), { text: c.start },
- { text: usd(c.bid), cls: 'num' }, { text: usd(c.daily_budget, 0), cls: 'num' }, { text: usd(c.lifetime_spend), cls: 'num' },
- ])));
-
- $('#recharge-card').hidden = !S.hasRecharge;
$('#recharges tbody').replaceChildren(...S.recharges.map(x => tr([
- { text: x.date }, { text: usd(Number(x.amount)), cls: 'num' }, { text: x.note || '' },
+ { text: x.date }, { text: usd(x.paid), cls: 'num' }, { text: usd(x.amount), cls: 'num' }, { text: usd(r2(x.paid - x.amount)), cls: 'num' },
])));
- $('#recharges tfoot').replaceChildren(tr([{ text: '合计' }, { text: usd(S.rechargeTotal), cls: 'num' }, { text: '' }]));
+ $('#recharges tfoot').replaceChildren(tr([
+ { text: '合计' }, { text: usd(S.paid), cls: 'num' }, { text: usd(S.received), cls: 'num' }, { text: usd(r2(S.paid - S.received)), cls: 'num' },
+ ]));
}
-let S;
-const drawCharts = () => { if (S) { drawSpend(S); drawBalance(S); } };
+function renderDaily(S, d) {
+ const f = $('#pkg-filter').value;
+ const rows = d.daily.filter(r => !f || r.pkg === f).sort((a, b) => b.dt.localeCompare(a.dt) || b.spend - a.spend);
+ $('#daily tbody').replaceChildren(...rows.map(r => tr([{ text: r.dt }, { text: r.pkg, color: S.colorOf(r.pkg) }, ...metricCells(r)])));
+ $('#daily tfoot').replaceChildren(tr([{ text: '合计' }, { text: f || '全部包' }, ...metricCells(sumRows(rows))]));
+}
-Promise.all([
- fetch('data.json', { cache: 'no-store' }).then(r => { if (!r.ok) throw new Error('data.json ' + r.status); return r.json(); }),
- fetch('manual.json', { cache: 'no-store' }).then(r => r.ok ? r.json() : {}).catch(() => ({})),
-]).then(([d, m]) => {
- S = derive(d, m);
- const updated = new Date(d.updated_at).toLocaleString('zh-CN', { hour12: false });
- $('#meta').replaceChildren(
- h('span', '', d.account ? d.account.name : 'Tecdo'),
- h('span', '', `订单 ${S.camps.map(c => c.id).join('、')}`),
- h('span', '', `数据区间 ${d.range.start} ~ ${d.range.end}`),
- h('span', '', `更新于 ${updated}`),
- );
- const legend = $('#spend-legend');
- legend.replaceChildren(...S.camps.map(c => { const s = h('span'); s.append(swatch(c.color), document.createTextNode(`${c.app || '订单'} · ${c.id}`)); return s; }));
- renderTiles(S, d);
- renderTables(S);
- drawCharts();
- $('#foot').textContent = `数据来源:MIDAS 综合报表(${d.source || ''})。每天 11:00 自动刷新,金额单位美元。充值记录和 pkg name 手工维护在 manual.json。`;
-}).catch(e => {
- $('#meta').textContent = '';
- const err = $('#err');
- err.hidden = false;
- err.textContent = '数据加载失败:' + e.message;
+let S, DATA;
+const drawCharts = () => { if (S) { drawSpend(S, DATA); drawBalance(S); } };
+
+$('#range').addEventListener('click', e => {
+ const b = e.target.closest('button');
+ if (!b) return;
+ RANGE = b.dataset.range;
+ [...$('#range').children].forEach(x => x.setAttribute('aria-pressed', String(x === b)));
+ if (S) drawSpend(S, DATA);
});
+fetch('data.json', { cache: 'no-store' })
+ .then(r => { if (!r.ok) throw new Error('data.json ' + r.status); return r.json(); })
+ .then(d => {
+ DATA = d;
+ S = derive(d);
+ const updated = new Date(d.updated_at).toLocaleString('zh-CN', { hour12: false });
+ $('#meta').replaceChildren(
+ h('span', '', `全账户 ${S.pkgs.length} 个包`),
+ h('span', '', `数据区间 ${d.range.start} ~ ${d.range.end}`),
+ h('span', '', `表格更新于 ${updated}`),
+ );
+ renderTiles(S, d);
+ renderTables(S, d);
+ drawCharts();
+ $('#foot').textContent = `数据来源:${d.source}。消耗按表格口径,成本 = 消耗 ÷ 首充,余额按到账金额计算。每天 11:00 检查表格,有更新就自动刷新。`;
+ })
+ .catch(e => {
+ $('#meta').textContent = '';
+ const err = $('#err');
+ err.hidden = false;
+ err.textContent = '数据加载失败:' + e.message;
+ });
+
let resizeTimer, lastWidth = 0;
function onResize() {
const w = document.querySelector('main').clientWidth;
diff --git a/manual.json b/manual.json
deleted file mode 100644
index 07307bb..0000000
--- a/manual.json
+++ /dev/null
@@ -1,8 +0,0 @@
-{
- "_说明": "手工维护的数据,自动刷新不会改动这个文件。pkg_names:两个订单的包名。recharges:每笔充值一行,date 用 YYYY-MM-DD,amount 单位美元,note 可空。",
- "pkg_names": {
- "3202606240007": "",
- "3202608110004": ""
- },
- "recharges": []
-}