Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 98417ff8c6 | |||
| b7957e58c0 | |||
| 5ae87fc11e | |||
| f4043f2abf | |||
| 472912560c | |||
| 9afb213b50 | |||
| 1a6e590a42 | |||
| cef5ea0140 | |||
| b1923c3133 | |||
| ecbce54e70 | |||
| 654aca4c40 | |||
| a393b5c0e3 | |||
| 9134ea9255 |
Generated
+874
-1
@@ -11,6 +11,7 @@
|
||||
"@ant-design/cssinjs": "^1.24.0",
|
||||
"@ant-design/icons": "^5.5.2",
|
||||
"@ant-design/nextjs-registry": "^1.0.2",
|
||||
"@ant-design/plots": "^2.6.8",
|
||||
"@ant-design/v5-patch-for-react-19": "^1.0.3",
|
||||
"antd": "^5.22.0",
|
||||
"axios": "^1.7.7",
|
||||
@@ -26,6 +27,19 @@
|
||||
"typescript": "^5.6.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/charts-util": {
|
||||
"version": "0.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/@ant-design/charts-util/-/charts-util-0.0.3.tgz",
|
||||
"integrity": "sha512-x1H7UT6t4dXAyGRoHqlOnEsEqBSTANFGTZEAMI0CWYhYUpp13n0o9grl9oPtoL6FEQMjUBTY+zGJKlHkz8smMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.4",
|
||||
"react-dom": ">=16.8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/colors": {
|
||||
"version": "7.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/@ant-design/colors/-/colors-7.2.1.tgz",
|
||||
@@ -120,6 +134,24 @@
|
||||
"react-dom": ">=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/plots": {
|
||||
"version": "2.6.8",
|
||||
"resolved": "https://registry.npmmirror.com/@ant-design/plots/-/plots-2.6.8.tgz",
|
||||
"integrity": "sha512-QsunUs2d5rbq/1BwVhga/siA5H50OaG23YopMYwPD4sPsza6NQzPQ8FM3elNIsD/BIk298tihqX1cJ/MmvVJbQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@ant-design/charts-util": "0.0.3",
|
||||
"@antv/event-emitter": "^0.1.3",
|
||||
"@antv/g": "^6.1.7",
|
||||
"@antv/g2": "^5.2.7",
|
||||
"@antv/g2-extension-plot": "^0.2.1",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.4",
|
||||
"react-dom": ">=16.8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@ant-design/react-slick": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/@ant-design/react-slick/-/react-slick-1.1.2.tgz",
|
||||
@@ -150,6 +182,251 @@
|
||||
"react-dom": ">=19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/component": {
|
||||
"version": "2.1.11",
|
||||
"resolved": "https://registry.npmmirror.com/@antv/component/-/component-2.1.11.tgz",
|
||||
"integrity": "sha512-dTdz8VAd3rpjOaGEZTluz82mtzrP4XCtNlNQyrxY7VNRNcjtvpTLDn57bUL2lRu1T+iklKvgbE2llMriWkq9vQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@antv/g": "^6.1.11",
|
||||
"@antv/scale": "^0.4.16",
|
||||
"@antv/util": "^3.3.10",
|
||||
"svg-path-parser": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/component/node_modules/@antv/scale": {
|
||||
"version": "0.4.16",
|
||||
"resolved": "https://registry.npmmirror.com/@antv/scale/-/scale-0.4.16.tgz",
|
||||
"integrity": "sha512-5wg/zB5kXHxpTV5OYwJD3ja6R8yTiqIOkjOhmpEJiowkzRlbEC/BOyMvNUq5fqFIHnMCE9woO7+c3zxEQCKPjw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@antv/util": "^3.3.7",
|
||||
"color-string": "^1.5.5",
|
||||
"fecha": "^4.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/coord": {
|
||||
"version": "0.4.7",
|
||||
"resolved": "https://registry.npmmirror.com/@antv/coord/-/coord-0.4.7.tgz",
|
||||
"integrity": "sha512-UTbrMLhwJUkKzqJx5KFnSRpU3BqrdLORJbwUbHK2zHSCT3q3bjcFA//ZYLVfIlwqFDXp/hzfMyRtp0c77A9ZVA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@antv/scale": "^0.4.12",
|
||||
"@antv/util": "^2.0.13",
|
||||
"gl-matrix": "^3.4.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/coord/node_modules/@antv/scale": {
|
||||
"version": "0.4.16",
|
||||
"resolved": "https://registry.npmmirror.com/@antv/scale/-/scale-0.4.16.tgz",
|
||||
"integrity": "sha512-5wg/zB5kXHxpTV5OYwJD3ja6R8yTiqIOkjOhmpEJiowkzRlbEC/BOyMvNUq5fqFIHnMCE9woO7+c3zxEQCKPjw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@antv/util": "^3.3.7",
|
||||
"color-string": "^1.5.5",
|
||||
"fecha": "^4.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/coord/node_modules/@antv/scale/node_modules/@antv/util": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmmirror.com/@antv/util/-/util-3.3.11.tgz",
|
||||
"integrity": "sha512-FII08DFM4ABh2q5rPYdr0hMtKXRgeZazvXaFYCs7J7uTcWDHUhczab2qOCJLNDugoj8jFag1djb7wS9ehaRYBg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"gl-matrix": "^3.3.0",
|
||||
"tslib": "^2.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/coord/node_modules/@antv/util": {
|
||||
"version": "2.0.17",
|
||||
"resolved": "https://registry.npmmirror.com/@antv/util/-/util-2.0.17.tgz",
|
||||
"integrity": "sha512-o6I9hi5CIUvLGDhth0RxNSFDRwXeywmt6ExR4+RmVAzIi48ps6HUy+svxOCayvrPBN37uE6TAc2KDofRo0nK9Q==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.8",
|
||||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/event-emitter": {
|
||||
"version": "0.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/@antv/event-emitter/-/event-emitter-0.1.3.tgz",
|
||||
"integrity": "sha512-4ddpsiHN9Pd4UIlWuKVK1C4IiZIdbwQvy9i7DUSI3xNJ89FPUFt8lxDYj8GzzfdllV0NkJTRxnG+FvLk0llidg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@antv/expr": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/@antv/expr/-/expr-1.0.2.tgz",
|
||||
"integrity": "sha512-vrfdmPHkTuiS5voVutKl2l06w1ihBh9A8SFdQPEE+2KMVpkymzGOF1eWpfkbGZ7tiFE15GodVdhhHomD/hdIwg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@antv/g": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/@antv/g/-/g-6.3.1.tgz",
|
||||
"integrity": "sha512-WYEKqy86LHB2PzTmrZXrIsIe+3Epeds2f68zceQ+BJtRoGki7Sy4IhlC8LrUMztgfT1t3d/0L745NWZwITroKA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@antv/g-lite": "2.7.0",
|
||||
"@antv/util": "^3.3.5",
|
||||
"@babel/runtime": "^7.25.6",
|
||||
"gl-matrix": "^3.4.3",
|
||||
"html2canvas": "^1.4.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/g-canvas": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/@antv/g-canvas/-/g-canvas-2.2.0.tgz",
|
||||
"integrity": "sha512-h7zVBBo2aO64DuGKvq9sG+yTU3sCUb9DALCVm7nz8qGPs8hhLuFOkKPEzUDNfNYZGJUGzY8UDtJ3QRGRFcvEQg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@antv/g-lite": "2.7.0",
|
||||
"@antv/g-math": "3.1.0",
|
||||
"@antv/util": "^3.3.5",
|
||||
"@babel/runtime": "^7.25.6",
|
||||
"gl-matrix": "^3.4.3",
|
||||
"tslib": "^2.5.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/g-lite": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmmirror.com/@antv/g-lite/-/g-lite-2.7.0.tgz",
|
||||
"integrity": "sha512-uSzgHYa5bwR5L2Au7/5tsOhFmXKZKLPBH90+Q9bP9teVs5VT4kOAi0isPSpDI8uhdDC2/VrfTWu5K9HhWI6FWw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@antv/g-math": "3.1.0",
|
||||
"@antv/util": "^3.3.5",
|
||||
"@antv/vendor": "^1.0.3",
|
||||
"@babel/runtime": "^7.25.6",
|
||||
"eventemitter3": "^5.0.1",
|
||||
"gl-matrix": "^3.4.3",
|
||||
"tslib": "^2.5.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/g-math": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@antv/g-math/-/g-math-3.1.0.tgz",
|
||||
"integrity": "sha512-DtN1Gj/yI0UiK18nSBsZX8RK0LszGwqfb+cBYWgE+ddyTm8dZnW4tPUhV7QXePsS6/A5hHC+JFpAAK7OEGo5ZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@antv/util": "^3.3.5",
|
||||
"@babel/runtime": "^7.25.6",
|
||||
"gl-matrix": "^3.4.3",
|
||||
"tslib": "^2.5.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/g-plugin-dragndrop": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/@antv/g-plugin-dragndrop/-/g-plugin-dragndrop-2.1.1.tgz",
|
||||
"integrity": "sha512-+aesDUJVQDs6UJ2bOBbDlaGAPCfHmU0MbrMTlQlfpwNplWueqtgVAZ3L57oZ2ZGHRWUHiRwZGPjXMBM3O2LELw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@antv/g-lite": "2.7.0",
|
||||
"@antv/util": "^3.3.5",
|
||||
"@babel/runtime": "^7.25.6",
|
||||
"tslib": "^2.5.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/g2": {
|
||||
"version": "5.4.8",
|
||||
"resolved": "https://registry.npmmirror.com/@antv/g2/-/g2-5.4.8.tgz",
|
||||
"integrity": "sha512-IvgIpwmT4M5/QAd3Mn2WiHIDeBqFJ4WA2gcZhRRSZuZ2KmgCqZWZwwIT0hc+kIGxwYeDoCQqf//t6FMVu3ryBg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@antv/component": "^2.1.9",
|
||||
"@antv/coord": "^0.4.7",
|
||||
"@antv/event-emitter": "^0.1.3",
|
||||
"@antv/expr": "^1.0.2",
|
||||
"@antv/g": "^6.1.24",
|
||||
"@antv/g-canvas": "^2.0.43",
|
||||
"@antv/g-plugin-dragndrop": "^2.0.35",
|
||||
"@antv/scale": "^0.5.1",
|
||||
"@antv/util": "^3.3.10",
|
||||
"@antv/vendor": "^1.0.11",
|
||||
"flru": "^1.0.2",
|
||||
"pdfast": "^0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/g2-extension-plot": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/@antv/g2-extension-plot/-/g2-extension-plot-0.2.2.tgz",
|
||||
"integrity": "sha512-KJXCXO7as+h0hDqirGXf1omrNuYzQmY3VmBmp7lIvkepbQ7sz3pPwy895r1FWETGF3vTk5UeFcAF5yzzBHWgbw==",
|
||||
"dependencies": {
|
||||
"@antv/g2": "^5.1.8",
|
||||
"@antv/util": "^3.3.5",
|
||||
"@antv/vendor": "^1.0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/scale": {
|
||||
"version": "0.5.2",
|
||||
"resolved": "https://registry.npmmirror.com/@antv/scale/-/scale-0.5.2.tgz",
|
||||
"integrity": "sha512-rTHRAwvpHWC5PGZF/mJ2ZuTDqwwvVBDRph0Uu5PV9BXwzV7K8+9lsqGJ+XHVLxe8c6bKog5nlzvV/dcYb0d5Ow==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@antv/util": "^3.3.7",
|
||||
"color-string": "^1.5.5",
|
||||
"fecha": "^4.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/util": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmmirror.com/@antv/util/-/util-3.3.11.tgz",
|
||||
"integrity": "sha512-FII08DFM4ABh2q5rPYdr0hMtKXRgeZazvXaFYCs7J7uTcWDHUhczab2qOCJLNDugoj8jFag1djb7wS9ehaRYBg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"gl-matrix": "^3.3.0",
|
||||
"tslib": "^2.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@antv/vendor": {
|
||||
"version": "1.0.11",
|
||||
"resolved": "https://registry.npmmirror.com/@antv/vendor/-/vendor-1.0.11.tgz",
|
||||
"integrity": "sha512-LmhPEQ+aapk3barntaiIxJ5VHno/Tyab2JnfdcPzp5xONh/8VSfed4bo/9xKo5HcUAEydko38vYLfj6lJliLiw==",
|
||||
"license": "MIT AND ISC",
|
||||
"dependencies": {
|
||||
"@types/d3-array": "^3.2.1",
|
||||
"@types/d3-color": "^3.1.3",
|
||||
"@types/d3-dispatch": "^3.0.6",
|
||||
"@types/d3-dsv": "^3.0.7",
|
||||
"@types/d3-ease": "^3.0.2",
|
||||
"@types/d3-fetch": "^3.0.7",
|
||||
"@types/d3-force": "^3.0.10",
|
||||
"@types/d3-format": "^3.0.4",
|
||||
"@types/d3-geo": "^3.1.0",
|
||||
"@types/d3-hierarchy": "^3.1.7",
|
||||
"@types/d3-interpolate": "^3.0.4",
|
||||
"@types/d3-path": "^3.1.0",
|
||||
"@types/d3-quadtree": "^3.0.6",
|
||||
"@types/d3-random": "^3.0.3",
|
||||
"@types/d3-scale": "^4.0.9",
|
||||
"@types/d3-scale-chromatic": "^3.1.0",
|
||||
"@types/d3-shape": "^3.1.7",
|
||||
"@types/d3-time": "^3.0.4",
|
||||
"@types/d3-timer": "^3.0.2",
|
||||
"d3-array": "^3.2.4",
|
||||
"d3-color": "^3.1.0",
|
||||
"d3-dispatch": "^3.0.1",
|
||||
"d3-dsv": "^3.0.1",
|
||||
"d3-ease": "^3.0.1",
|
||||
"d3-fetch": "^3.0.1",
|
||||
"d3-force": "^3.0.0",
|
||||
"d3-force-3d": "^3.0.5",
|
||||
"d3-format": "^3.1.0",
|
||||
"d3-geo": "^3.1.1",
|
||||
"d3-geo-projection": "^4.0.0",
|
||||
"d3-hierarchy": "^3.1.2",
|
||||
"d3-interpolate": "^3.0.1",
|
||||
"d3-path": "^3.1.0",
|
||||
"d3-quadtree": "^3.0.1",
|
||||
"d3-random": "^3.0.1",
|
||||
"d3-regression": "^1.3.10",
|
||||
"d3-scale": "^4.0.2",
|
||||
"d3-scale-chromatic": "^3.1.0",
|
||||
"d3-shape": "^3.2.0",
|
||||
"d3-time": "^3.1.0",
|
||||
"d3-timer": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/runtime": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.29.7.tgz",
|
||||
@@ -981,6 +1258,141 @@
|
||||
"tslib": "^2.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-array": {
|
||||
"version": "3.2.2",
|
||||
"resolved": "https://registry.npmmirror.com/@types/d3-array/-/d3-array-3.2.2.tgz",
|
||||
"integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-color": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/@types/d3-color/-/d3-color-3.1.3.tgz",
|
||||
"integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-dispatch": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmmirror.com/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz",
|
||||
"integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-dsv": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmmirror.com/@types/d3-dsv/-/d3-dsv-3.0.7.tgz",
|
||||
"integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-ease": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/@types/d3-ease/-/d3-ease-3.0.2.tgz",
|
||||
"integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-fetch": {
|
||||
"version": "3.0.7",
|
||||
"resolved": "https://registry.npmmirror.com/@types/d3-fetch/-/d3-fetch-3.0.7.tgz",
|
||||
"integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-dsv": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-force": {
|
||||
"version": "3.0.10",
|
||||
"resolved": "https://registry.npmmirror.com/@types/d3-force/-/d3-force-3.0.10.tgz",
|
||||
"integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-format": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/@types/d3-format/-/d3-format-3.0.4.tgz",
|
||||
"integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-geo": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@types/d3-geo/-/d3-geo-3.1.0.tgz",
|
||||
"integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/geojson": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-hierarchy": {
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmmirror.com/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz",
|
||||
"integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-interpolate": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz",
|
||||
"integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-color": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-path": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/@types/d3-path/-/d3-path-3.1.1.tgz",
|
||||
"integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-quadtree": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz",
|
||||
"integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-random": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/@types/d3-random/-/d3-random-3.0.3.tgz",
|
||||
"integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-scale": {
|
||||
"version": "4.0.9",
|
||||
"resolved": "https://registry.npmmirror.com/@types/d3-scale/-/d3-scale-4.0.9.tgz",
|
||||
"integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-time": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-scale-chromatic": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
|
||||
"integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-shape": {
|
||||
"version": "3.1.8",
|
||||
"resolved": "https://registry.npmmirror.com/@types/d3-shape/-/d3-shape-3.1.8.tgz",
|
||||
"integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/d3-path": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/d3-time": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/@types/d3-time/-/d3-time-3.0.4.tgz",
|
||||
"integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/d3-timer": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/@types/d3-timer/-/d3-timer-3.0.2.tgz",
|
||||
"integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/geojson": {
|
||||
"version": "7946.0.16",
|
||||
"resolved": "https://registry.npmmirror.com/@types/geojson/-/geojson-7946.0.16.tgz",
|
||||
"integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.19.20",
|
||||
"resolved": "https://registry.npmmirror.com/@types/node/-/node-22.19.20.tgz",
|
||||
@@ -1114,6 +1526,15 @@
|
||||
"proxy-from-env": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/base64-arraybuffer": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
|
||||
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/call-bind-apply-helpers": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
|
||||
@@ -1159,6 +1580,22 @@
|
||||
"integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/color-string": {
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmmirror.com/color-string/-/color-string-1.9.1.tgz",
|
||||
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "^1.0.0",
|
||||
"simple-swizzle": "^0.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
@@ -1171,6 +1608,15 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/commander/-/commander-7.2.0.tgz",
|
||||
"integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/compute-scroll-into-view": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz",
|
||||
@@ -1186,12 +1632,309 @@
|
||||
"toggle-selection": "^1.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/css-line-break": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/css-line-break/-/css-line-break-2.1.0.tgz",
|
||||
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3-array": {
|
||||
"version": "3.2.4",
|
||||
"resolved": "https://registry.npmmirror.com/d3-array/-/d3-array-3.2.4.tgz",
|
||||
"integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"internmap": "1 - 2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-binarytree": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/d3-binarytree/-/d3-binarytree-1.0.2.tgz",
|
||||
"integrity": "sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3-color": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/d3-color/-/d3-color-3.1.0.tgz",
|
||||
"integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-dispatch": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/d3-dispatch/-/d3-dispatch-3.0.1.tgz",
|
||||
"integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-dsv": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/d3-dsv/-/d3-dsv-3.0.1.tgz",
|
||||
"integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"commander": "7",
|
||||
"iconv-lite": "0.6",
|
||||
"rw": "1"
|
||||
},
|
||||
"bin": {
|
||||
"csv2json": "bin/dsv2json.js",
|
||||
"csv2tsv": "bin/dsv2dsv.js",
|
||||
"dsv2dsv": "bin/dsv2dsv.js",
|
||||
"dsv2json": "bin/dsv2json.js",
|
||||
"json2csv": "bin/json2dsv.js",
|
||||
"json2dsv": "bin/json2dsv.js",
|
||||
"json2tsv": "bin/json2dsv.js",
|
||||
"tsv2csv": "bin/dsv2dsv.js",
|
||||
"tsv2json": "bin/dsv2json.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-ease": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/d3-ease/-/d3-ease-3.0.1.tgz",
|
||||
"integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==",
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-fetch": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/d3-fetch/-/d3-fetch-3.0.1.tgz",
|
||||
"integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-dsv": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-force": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/d3-force/-/d3-force-3.0.0.tgz",
|
||||
"integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-quadtree": "1 - 3",
|
||||
"d3-timer": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-force-3d": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/d3-force-3d/-/d3-force-3d-3.0.6.tgz",
|
||||
"integrity": "sha512-4tsKHUPLOVkyfEffZo1v6sFHvGFwAIIjt/W8IThbp08DYAsXZck+2pSHEG5W1+gQgEvFLdZkYvmJAbRM2EzMnA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"d3-binarytree": "1",
|
||||
"d3-dispatch": "1 - 3",
|
||||
"d3-octree": "1",
|
||||
"d3-quadtree": "1 - 3",
|
||||
"d3-timer": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-format": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/d3-format/-/d3-format-3.1.2.tgz",
|
||||
"integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-geo": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/d3-geo/-/d3-geo-3.1.1.tgz",
|
||||
"integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2.5.0 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-geo-projection": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/d3-geo-projection/-/d3-geo-projection-4.0.0.tgz",
|
||||
"integrity": "sha512-p0bK60CEzph1iqmnxut7d/1kyTmm3UWtPlwdkM31AU+LW+BXazd5zJdoCn7VFxNCHXRngPHRnsNn5uGjLRGndg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"commander": "7",
|
||||
"d3-array": "1 - 3",
|
||||
"d3-geo": "1.12.0 - 3"
|
||||
},
|
||||
"bin": {
|
||||
"geo2svg": "bin/geo2svg.js",
|
||||
"geograticule": "bin/geograticule.js",
|
||||
"geoproject": "bin/geoproject.js",
|
||||
"geoquantize": "bin/geoquantize.js",
|
||||
"geostitch": "bin/geostitch.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-hierarchy": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz",
|
||||
"integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-interpolate": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/d3-interpolate/-/d3-interpolate-3.0.1.tgz",
|
||||
"integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-octree": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/d3-octree/-/d3-octree-1.1.0.tgz",
|
||||
"integrity": "sha512-F8gPlqpP+HwRPMO/8uOu5wjH110+6q4cgJvgJT6vlpy3BEaDIKlTZrgHKZSp/i1InRpVfh4puY/kvL6MxK930A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/d3-path": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/d3-path/-/d3-path-3.1.0.tgz",
|
||||
"integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-quadtree": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/d3-quadtree/-/d3-quadtree-3.0.1.tgz",
|
||||
"integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-random": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/d3-random/-/d3-random-3.0.1.tgz",
|
||||
"integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-regression": {
|
||||
"version": "1.3.10",
|
||||
"resolved": "https://registry.npmmirror.com/d3-regression/-/d3-regression-1.3.10.tgz",
|
||||
"integrity": "sha512-PF8GWEL70cHHWpx2jUQXc68r1pyPHIA+St16muk/XRokETzlegj5LriNKg7o4LR0TySug4nHYPJNNRz/W+/Niw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/d3-scale": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/d3-scale/-/d3-scale-4.0.2.tgz",
|
||||
"integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2.10.0 - 3",
|
||||
"d3-format": "1 - 3",
|
||||
"d3-interpolate": "1.2.0 - 3",
|
||||
"d3-time": "2.1.1 - 3",
|
||||
"d3-time-format": "2 - 4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-scale-chromatic": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz",
|
||||
"integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-color": "1 - 3",
|
||||
"d3-interpolate": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-shape": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/d3-shape/-/d3-shape-3.2.0.tgz",
|
||||
"integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-path": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-time": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/d3-time/-/d3-time-3.1.0.tgz",
|
||||
"integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-array": "2 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-time-format": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/d3-time-format/-/d3-time-format-4.1.0.tgz",
|
||||
"integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"d3-time": "1 - 3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/d3-timer": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/d3-timer/-/d3-timer-3.0.1.tgz",
|
||||
"integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/dayjs": {
|
||||
"version": "1.11.21",
|
||||
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.21.tgz",
|
||||
@@ -1293,6 +2036,33 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/eventemitter3": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-5.0.4.tgz",
|
||||
"integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fecha": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/fecha/-/fecha-4.2.3.tgz",
|
||||
"integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/flru": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/flru/-/flru-1.0.2.tgz",
|
||||
"integrity": "sha512-kWyh8ADvHBFz6ua5xYOPnUroZTT/bwWfrCeL0Wj1dzG4/YOmOcfJ99W8dOVyyynJN35rZ9aCOtHChqQovV7yog==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||
@@ -1375,6 +2145,12 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/gl-matrix": {
|
||||
"version": "3.4.4",
|
||||
"resolved": "https://registry.npmmirror.com/gl-matrix/-/gl-matrix-3.4.4.tgz",
|
||||
"integrity": "sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz",
|
||||
@@ -1426,6 +2202,19 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/html2canvas": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/html2canvas/-/html2canvas-1.4.1.tgz",
|
||||
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"css-line-break": "^2.1.0",
|
||||
"text-segmentation": "^1.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/https-proxy-agent": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
|
||||
@@ -1439,6 +2228,33 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/iconv-lite": {
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz",
|
||||
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"safer-buffer": ">= 2.1.2 < 3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/internmap": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz",
|
||||
"integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/is-arrayish": {
|
||||
"version": "0.3.4",
|
||||
"resolved": "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.3.4.tgz",
|
||||
"integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -1454,6 +2270,12 @@
|
||||
"string-convert": "^0.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.18.1.tgz",
|
||||
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
@@ -1588,6 +2410,12 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/pdfast": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/pdfast/-/pdfast-0.2.0.tgz",
|
||||
"integrity": "sha512-cq6TTu6qKSFUHwEahi68k/kqN2mfepjkGrG9Un70cgdRRKLKY6Rf8P8uvP2NvZktaQZNF3YE7agEkLj0vGK9bA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -2274,6 +3102,18 @@
|
||||
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/rw": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmmirror.com/rw/-/rw-1.3.3.tgz",
|
||||
"integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/safer-buffer": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
|
||||
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.23.2",
|
||||
"resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.2.tgz",
|
||||
@@ -2350,6 +3190,15 @@
|
||||
"@img/sharp-win32-x64": "0.34.5"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-swizzle": {
|
||||
"version": "0.2.4",
|
||||
"resolved": "https://registry.npmmirror.com/simple-swizzle/-/simple-swizzle-0.2.4.tgz",
|
||||
"integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-arrayish": "^0.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
@@ -2394,6 +3243,21 @@
|
||||
"integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/svg-path-parser": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/svg-path-parser/-/svg-path-parser-1.1.0.tgz",
|
||||
"integrity": "sha512-jGCUqcQyXpfe38R7RFfhrMyfXcBmpMNJI/B+4CE9/Unkh98UporAc461GTthv+TVDuZXsBx7/WiwJb1Oh4tt4A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/text-segmentation": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/text-segmentation/-/text-segmentation-1.0.3.tgz",
|
||||
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"utrie": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/throttle-debounce": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/throttle-debounce/-/throttle-debounce-5.0.2.tgz",
|
||||
@@ -2436,9 +3300,18 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/utrie": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/utrie/-/utrie-1.0.2.tgz",
|
||||
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-arraybuffer": "^1.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@next/swc-win32-x64-msvc": {
|
||||
"version": "15.5.19",
|
||||
"resolved": "https://registry.npmmirror.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.19.tgz",
|
||||
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.5.19.tgz",
|
||||
"integrity": "sha512-PhmojAHyqMne56HBLGu9dhDnHPuFmEjrXSQMM/nW0J6j849lk3ESrVtqNJcCk8CKOV7brpTTbaYAjwKPzKM69w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
"@ant-design/cssinjs": "^1.24.0",
|
||||
"@ant-design/icons": "^5.5.2",
|
||||
"@ant-design/nextjs-registry": "^1.0.2",
|
||||
"@ant-design/plots": "^2.6.8",
|
||||
"@ant-design/v5-patch-for-react-19": "^1.0.3",
|
||||
"antd": "^5.22.0",
|
||||
"axios": "^1.7.7",
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
Col,
|
||||
DatePicker,
|
||||
Divider,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Popover,
|
||||
@@ -17,6 +19,7 @@ import {
|
||||
Select,
|
||||
Space,
|
||||
Statistic,
|
||||
Switch,
|
||||
Table,
|
||||
Tag,
|
||||
Typography,
|
||||
@@ -268,6 +271,95 @@ function TrendChart({ points }: { points: TrendPoint[] }) {
|
||||
|
||||
// 广告数据页:单表 —— 按 用户×类型×应用×代码位 聚合,一行同时给出
|
||||
// 展示条数 / 收益 / 应发金币 / 实发金币 / 是否一致(发奖对账下沉到聚合级)。
|
||||
// 广告配置面板:穿山甲 app_id / 各场景广告位ID / 验签密钥 / 各场景开关。
|
||||
// 客户端(发版后)从 /api/v1/platform/ad-config 拉取;此处经 admin /ad-config 读写。
|
||||
interface AdCfg {
|
||||
app_id: string;
|
||||
reward_code_id: string;
|
||||
compare_feed_code_id: string;
|
||||
coupon_feed_code_id: string;
|
||||
reward_mkey: string;
|
||||
reward_enabled: boolean;
|
||||
compare_ad_enabled: boolean;
|
||||
coupon_ad_enabled: boolean;
|
||||
}
|
||||
|
||||
function AdConfigPanel() {
|
||||
const { message } = App.useApp();
|
||||
const [form] = Form.useForm();
|
||||
const [saving, setSaving] = useState(false);
|
||||
const load = useCallback(async () => {
|
||||
try {
|
||||
const r = await api.get<AdCfg>('/admin/api/ad-config');
|
||||
form.setFieldsValue(r.data);
|
||||
} catch (e) {
|
||||
message.error(errMsg(e, '广告配置加载失败'));
|
||||
}
|
||||
}, [form, message]);
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [load]);
|
||||
const save = async () => {
|
||||
const v = await form.validateFields();
|
||||
setSaving(true);
|
||||
try {
|
||||
await api.patch('/admin/api/ad-config', v);
|
||||
message.success('已保存。客户端下次拉取生效(应用ID需冷启,广告位ID即时)');
|
||||
load();
|
||||
} catch (e) {
|
||||
message.error(errMsg(e, '保存失败'));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<Card
|
||||
title="广告配置(穿山甲)"
|
||||
style={{ marginBottom: 16 }}
|
||||
extra={
|
||||
<Button type="primary" loading={saving} onClick={save}>
|
||||
保存
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Alert
|
||||
type="info"
|
||||
style={{ marginBottom: 16 }}
|
||||
message="应用ID改了客户端需冷启生效,广告位ID即时;换激励位必须同步换验签密钥(m-key),否则发不出金币。这些配置需客户端发版接入后才会读取。"
|
||||
/>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="app_id" label="穿山甲应用ID (APP_ID)" rules={[{ required: true }]}>
|
||||
<Input placeholder="如 5830519" />
|
||||
</Form.Item>
|
||||
<Form.Item name="reward_code_id" label="福利页 · 激励视频 广告位ID" rules={[{ required: true }]}>
|
||||
<Input placeholder="如 104099389" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="reward_mkey"
|
||||
label="福利激励 · 验签密钥 m-key(机密,不下发客户端;换激励位必须同步换;留空回退 .env)"
|
||||
>
|
||||
<Input.Password placeholder="GroMore S2S 回调验签密钥" autoComplete="off" />
|
||||
</Form.Item>
|
||||
<Form.Item name="compare_feed_code_id" label="比价 · 信息流 广告位ID" rules={[{ required: true }]}>
|
||||
<Input placeholder="如 104090333" />
|
||||
</Form.Item>
|
||||
<Form.Item name="coupon_feed_code_id" label="领券 · 信息流 广告位ID" rules={[{ required: true }]}>
|
||||
<Input placeholder="如 104090333" />
|
||||
</Form.Item>
|
||||
<Form.Item name="reward_enabled" label="福利激励视频开关" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item name="compare_ad_enabled" label="比价广告开关" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
<Form.Item name="coupon_ad_enabled" label="领券广告开关" valuePropName="checked">
|
||||
<Switch />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
export default function AdRevenuePage() {
|
||||
const { message } = App.useApp();
|
||||
const [range, setRange] = useState<[Dayjs, Dayjs]>([dayjs(), dayjs()]);
|
||||
@@ -412,8 +504,9 @@ export default function AdRevenuePage() {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<AdConfigPanel />
|
||||
<Space align="center" style={{ display: 'flex', marginBottom: 16 }}>
|
||||
<h2 style={{ margin: 0 }}>广告数据</h2>
|
||||
<h2 style={{ margin: 0 }}>收益报表</h2>
|
||||
<Button
|
||||
type="link"
|
||||
size="small"
|
||||
|
||||
@@ -0,0 +1,334 @@
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import type { CSSProperties } from 'react';
|
||||
import {
|
||||
App, Button, Card, Collapse, Descriptions, Drawer, Input, InputNumber,
|
||||
Select, Space, Spin, Table, Tag, Typography,
|
||||
} from 'antd';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { api, errMsg } from '@/lib/api';
|
||||
import { formatWallTime, yuan } from '@/lib/format';
|
||||
import { usePagedList } from '@/lib/usePagedList';
|
||||
import type { ComparisonRecordDetail, ComparisonRecordListItem } from '@/lib/types';
|
||||
|
||||
const STATUS_COLOR: Record<string, string> = { success: 'green', failed: 'red', cancelled: 'default' };
|
||||
const STATUS_LABEL: Record<string, string> = { success: '成功', failed: '失败', cancelled: '已终止' };
|
||||
|
||||
// 「卡在哪一步」: platform_results[*].status 翻成人话(找店/加菜/起送/读价)
|
||||
const STUCK_LABEL: Record<string, string> = {
|
||||
success: '成功',
|
||||
store_not_found: '没找到店',
|
||||
items_not_found: '菜没匹配上',
|
||||
below_minimum: '未达起送',
|
||||
unsupported: '平台不支持',
|
||||
failed: '失败',
|
||||
};
|
||||
|
||||
const fmtMs = (ms: number | null) => (ms == null ? '-' : `${(ms / 1000).toFixed(1)}s`);
|
||||
const cents = (c: number | null) => (c == null ? '-' : yuan(c));
|
||||
|
||||
// LLM message content 常是 json.dumps 的卡片列表;能 parse 成 JSON 就缩进展开,否则原样。
|
||||
function pretty(s: string | null): string {
|
||||
if (!s) return '';
|
||||
try {
|
||||
return JSON.stringify(JSON.parse(s), null, 2);
|
||||
} catch {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
const preStyle: CSSProperties = {
|
||||
background: '#f6f8fa', padding: 8, borderRadius: 6, fontSize: 12,
|
||||
maxHeight: 340, overflow: 'auto', whiteSpace: 'pre-wrap', wordBreak: 'break-word', margin: 0,
|
||||
};
|
||||
|
||||
export default function ComparisonRecordsPage() {
|
||||
const { message } = App.useApp();
|
||||
|
||||
const [userId, setUserId] = useState<number | null>(null);
|
||||
const [phone, setPhone] = useState('');
|
||||
const [status, setStatus] = useState<string | undefined>();
|
||||
const [applied, setApplied] = useState<Record<string, unknown>>({});
|
||||
|
||||
const { items, total, page, pageSize, loading, onChange } =
|
||||
usePagedList<ComparisonRecordListItem>('/admin/api/comparison-records', applied);
|
||||
|
||||
const [detail, setDetail] = useState<ComparisonRecordDetail | null>(null);
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
|
||||
const search = () =>
|
||||
setApplied({ user_id: userId ?? undefined, phone: phone || undefined, status });
|
||||
|
||||
const reset = () => {
|
||||
setUserId(null);
|
||||
setPhone('');
|
||||
setStatus(undefined);
|
||||
setApplied({});
|
||||
};
|
||||
|
||||
const openDetail = async (id: number) => {
|
||||
setDetailLoading(true);
|
||||
setDetail(null);
|
||||
try {
|
||||
const { data } = await api.get<ComparisonRecordDetail>(`/admin/api/comparison-records/${id}`);
|
||||
setDetail(data);
|
||||
} catch (e) {
|
||||
message.error(errMsg(e));
|
||||
} finally {
|
||||
setDetailLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const closeDetail = () => {
|
||||
setDetail(null);
|
||||
setDetailLoading(false);
|
||||
};
|
||||
|
||||
const columns: ColumnsType<ComparisonRecordListItem> = [
|
||||
{ title: 'ID', dataIndex: 'id', width: 64 },
|
||||
{
|
||||
title: '用户',
|
||||
key: 'user',
|
||||
width: 140,
|
||||
render: (_, r) => (
|
||||
<div>
|
||||
<div>{r.phone || `#${r.user_id}`}</div>
|
||||
{r.nickname && <div style={{ color: '#999', fontSize: 12 }}>{r.nickname}</div>}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{ title: '状态', dataIndex: 'status', width: 72, render: (s: string) => <Tag color={STATUS_COLOR[s]}>{STATUS_LABEL[s] || s}</Tag> },
|
||||
{ title: '源平台', dataIndex: 'source_platform_name', width: 90, render: (v) => v || '-' },
|
||||
{ title: '店/商品', dataIndex: 'store_name', width: 150, ellipsis: true, render: (v) => v || '-' },
|
||||
{ title: '最优', dataIndex: 'best_platform_name', width: 80, render: (v) => v || '-' },
|
||||
{
|
||||
title: '省',
|
||||
key: 'saved',
|
||||
width: 84,
|
||||
render: (_, r) =>
|
||||
r.saved_amount_cents == null ? '-' : (
|
||||
<b style={{ color: r.saved_amount_cents > 0 ? '#3f8600' : '#999' }}>{yuan(r.saved_amount_cents)}</b>
|
||||
),
|
||||
},
|
||||
{ title: '耗时', dataIndex: 'total_ms', width: 64, render: fmtMs },
|
||||
{ title: '步', dataIndex: 'step_count', width: 48, render: (v) => v ?? '-' },
|
||||
{ title: 'LLM', dataIndex: 'llm_call_count', width: 56, render: (v) => v ?? '-' },
|
||||
{
|
||||
title: '重试',
|
||||
dataIndex: 'retry_count',
|
||||
width: 50,
|
||||
render: (v: number | null) => (v ? <span style={{ color: '#cf1322' }}>{v}</span> : v ?? '-'),
|
||||
},
|
||||
{
|
||||
title: '机型/ROM',
|
||||
key: 'device',
|
||||
width: 150,
|
||||
ellipsis: true,
|
||||
render: (_, r) => [r.device_model, r.rom_name].filter(Boolean).join(' / ') || '-',
|
||||
},
|
||||
{ title: '时间', dataIndex: 'created_at', width: 150, render: (v: string) => formatWallTime(v) },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'op',
|
||||
fixed: 'right',
|
||||
width: 80,
|
||||
render: (_, r) =>
|
||||
r.trace_url ? (
|
||||
<a href={r.trace_url} target="_blank" rel="noreferrer" onClick={(e) => e.stopPropagation()}>trace</a>
|
||||
) : (
|
||||
<span style={{ color: '#ccc' }}>-</span>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const platformResults =
|
||||
(detail?.raw_payload?.platform_results as Record<string, unknown>[] | undefined) || [];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2>比价记录(debug)</h2>
|
||||
<Space style={{ marginBottom: 16 }} wrap>
|
||||
<InputNumber placeholder="user_id" value={userId} onChange={setUserId} style={{ width: 110 }} />
|
||||
<Input
|
||||
placeholder="手机号前缀"
|
||||
value={phone}
|
||||
onChange={(e) => setPhone(e.target.value)}
|
||||
onPressEnter={search}
|
||||
allowClear
|
||||
style={{ width: 150 }}
|
||||
/>
|
||||
<Select
|
||||
placeholder="状态"
|
||||
value={status}
|
||||
onChange={setStatus}
|
||||
allowClear
|
||||
style={{ width: 110 }}
|
||||
options={[
|
||||
{ value: 'success', label: '成功' },
|
||||
{ value: 'failed', label: '失败' },
|
||||
{ value: 'cancelled', label: '已终止' },
|
||||
]}
|
||||
/>
|
||||
<Button type="primary" onClick={search}>查询</Button>
|
||||
<Button onClick={reset}>重置</Button>
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={items}
|
||||
loading={loading}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize,
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
showTotal: (t) => `共 ${t} 条`,
|
||||
onChange,
|
||||
}}
|
||||
scroll={{ x: 1320 }}
|
||||
onRow={(r) => ({ onClick: () => openDetail(r.id), style: { cursor: 'pointer' } })}
|
||||
/>
|
||||
|
||||
<Drawer title={detail ? `比价记录 #${detail.id}` : '详情'} width={780} open={detailLoading || !!detail} onClose={closeDetail}>
|
||||
{detailLoading && <Spin />}
|
||||
{detail && (
|
||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
||||
<Descriptions size="small" column={2} bordered>
|
||||
<Descriptions.Item label="用户">
|
||||
{detail.phone || `#${detail.user_id}`}{detail.nickname ? `(${detail.nickname})` : ''}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="状态"><Tag color={STATUS_COLOR[detail.status]}>{STATUS_LABEL[detail.status] || detail.status}</Tag></Descriptions.Item>
|
||||
<Descriptions.Item label="业务">{detail.business_type}</Descriptions.Item>
|
||||
<Descriptions.Item label="耗时">{fmtMs(detail.total_ms)}</Descriptions.Item>
|
||||
<Descriptions.Item label="步数">{detail.step_count ?? '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="LLM 次数/重试">{detail.llm_call_count ?? '-'} / {detail.retry_count ?? '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="源平台">{detail.source_platform_name || '-'}({cents(detail.source_price_cents)})</Descriptions.Item>
|
||||
<Descriptions.Item label="最优">{detail.best_platform_name || '-'}({cents(detail.best_price_cents)})</Descriptions.Item>
|
||||
<Descriptions.Item label="省" span={2}>{cents(detail.saved_amount_cents)}{detail.is_source_best ? '(源平台最便宜)' : ''}</Descriptions.Item>
|
||||
<Descriptions.Item label="店/商品" span={2}>{detail.store_name || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="结论文案" span={2}>{detail.information || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="trace" span={2}>
|
||||
{detail.trace_url
|
||||
? <a href={detail.trace_url} target="_blank" rel="noreferrer">{detail.trace_url}</a>
|
||||
: <span style={{ color: '#999' }}>{detail.trace_id}(无 trace_url)</span>}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="定位" span={2}>
|
||||
{detail.longitude != null ? `${detail.longitude}, ${detail.latitude}` : '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="最优深链" span={2}>
|
||||
{detail.best_deeplink
|
||||
? <Typography.Text copyable={{ text: detail.best_deeplink }} style={{ fontSize: 12, wordBreak: 'break-all' }}>{detail.best_deeplink}</Typography.Text>
|
||||
: '-'}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
|
||||
<Card size="small" title="设备环境">
|
||||
<Descriptions size="small" column={2}>
|
||||
<Descriptions.Item label="机型">{detail.device_model || '-'}({detail.device_manufacturer || '-'})</Descriptions.Item>
|
||||
<Descriptions.Item label="ROM">{[detail.rom_vendor, detail.rom_name, detail.rom_version].filter(Boolean).join(' ') || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="Android">{detail.android_version || '-'}(SDK {detail.android_sdk ?? '-'})</Descriptions.Item>
|
||||
<Descriptions.Item label="App 版本">{detail.app_version || '-'}({detail.app_version_code ?? '-'})</Descriptions.Item>
|
||||
<Descriptions.Item label="源 App 版本" span={2}>{detail.source_app_version || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="device_id" span={2}>{detail.device_id || '-'}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Card>
|
||||
|
||||
{((detail.items?.length ?? 0) > 0 || (detail.skipped_dish_names?.length ?? 0) > 0) && (
|
||||
<Card size="small" title={`下单商品${detail.total_dish_count != null ? `(共 ${detail.total_dish_count})` : ''}`}>
|
||||
{(detail.items?.length ?? 0) > 0 ? (
|
||||
<ul style={{ margin: 0, paddingLeft: 18 }}>
|
||||
{detail.items.map((it, i) => (
|
||||
<li key={i}>
|
||||
{it.name}{it.qty != null ? ` ×${it.qty}` : ''}
|
||||
{it.specs?.length ? <span style={{ color: '#999' }}>({it.specs.join('/')})</span> : null}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : <Typography.Text type="secondary">无商品明细</Typography.Text>}
|
||||
{(detail.skipped_dish_names?.length ?? 0) > 0 && (
|
||||
<div style={{ marginTop: 8, color: '#cf1322' }}>
|
||||
跳过{detail.skipped_dish_count != null ? ` ${detail.skipped_dish_count} ` : ''}个:{detail.skipped_dish_names.join('、')}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{platformResults.length > 0 && (
|
||||
<Card size="small" title="逐平台结局(卡在哪一步)">
|
||||
<Table
|
||||
size="small"
|
||||
rowKey={(_, i) => String(i)}
|
||||
pagination={false}
|
||||
dataSource={platformResults}
|
||||
columns={[
|
||||
{ title: '平台', dataIndex: 'platform_name', render: (v, r: Record<string, unknown>) => (v as string) || (r.platform_id as string) || '-' },
|
||||
{ title: '结局', dataIndex: 'status', render: (s: string) => <Tag color={s === 'success' ? 'green' : 'orange'}>{STUCK_LABEL[s] || s || '-'}</Tag> },
|
||||
{ title: '说明', dataIndex: 'reason', render: (v) => (v as string) || '-' },
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{(detail.comparison_results?.length ?? 0) > 0 && (
|
||||
<Collapse
|
||||
size="small"
|
||||
items={[{
|
||||
key: 'cmp',
|
||||
label: `逐平台对比(${detail.comparison_results.length})`,
|
||||
children: <pre style={preStyle}>{JSON.stringify(detail.comparison_results, null, 2)}</pre>,
|
||||
}]}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Card size="small" title={`LLM 调用明细(${detail.llm_calls?.length ?? 0} 次)`}>
|
||||
{detail.llm_calls && detail.llm_calls.length > 0 ? (
|
||||
<Collapse
|
||||
size="small"
|
||||
items={detail.llm_calls.map((c, i) => ({
|
||||
key: String(i),
|
||||
label: (
|
||||
<Space size={6} wrap>
|
||||
<Tag color="blue">{c.scene || 'llm'}</Tag>
|
||||
<span style={{ color: '#999', fontSize: 12 }}>{c.model}</span>
|
||||
<span style={{ color: '#999', fontSize: 12 }}>{c.latency_ms ?? '-'}ms</span>
|
||||
{c.usage?.total_tokens ? <span style={{ color: '#999', fontSize: 12 }}>{c.usage.total_tokens} tok</span> : null}
|
||||
{c.error ? <Tag color="red">error</Tag> : null}
|
||||
</Space>
|
||||
),
|
||||
children: (
|
||||
<div>
|
||||
{c.input_messages.map((m, j) => (
|
||||
<div key={j} style={{ marginBottom: 8 }}>
|
||||
<Tag>{m.role}</Tag>
|
||||
<pre style={preStyle}>{pretty(m.content)}</pre>
|
||||
</div>
|
||||
))}
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Tag color="green">output</Tag>
|
||||
<pre style={preStyle}>{c.error ? `【error】${c.error}` : pretty(c.output)}</pre>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
}))}
|
||||
/>
|
||||
) : (
|
||||
<Typography.Text type="secondary">无 LLM 调用明细(旧记录 / pricebot 未采集到 / 本次未调 LLM)</Typography.Text>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<Collapse
|
||||
size="small"
|
||||
items={[{
|
||||
key: 'raw',
|
||||
label: '原始上报 raw_payload',
|
||||
children: <pre style={preStyle}>{JSON.stringify(detail.raw_payload, null, 2)}</pre>,
|
||||
}]}
|
||||
/>
|
||||
</Space>
|
||||
)}
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,331 @@
|
||||
'use client';
|
||||
|
||||
// CPS 群对账详情:该群点击量/独立点击/复制/独立复制的天级 or 小时级变化折线图。
|
||||
// 时间范围: 近 3/7/14/30 天(天级) 或 今日(小时级)。数据由后端补零成连续桶,折线不断裂。
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import dynamic from 'next/dynamic';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import { Button, Card, Col, Empty, Row, Segmented, Space, Spin, Statistic, Table, message } from 'antd';
|
||||
import { api, errMsg } from '@/lib/api';
|
||||
import { formatUtcTime, yuan } from '@/lib/format';
|
||||
|
||||
// @ant-design/plots 依赖 DOM,关掉 SSR 仅在客户端渲染(否则 Next 预渲染报 document undefined)。
|
||||
const Line = dynamic(() => import('@ant-design/plots').then((m) => m.Line), { ssr: false });
|
||||
|
||||
interface TsPoint {
|
||||
label: string;
|
||||
click_pv: number;
|
||||
click_uv: number;
|
||||
copy_pv: number;
|
||||
copy_uv: number;
|
||||
}
|
||||
interface TsResp {
|
||||
group_id: number;
|
||||
group_name: string;
|
||||
granularity: string;
|
||||
points: TsPoint[];
|
||||
}
|
||||
|
||||
// 按天大表格一行(订单侧字段对淘宝/京东群为 null → 显示 "-")
|
||||
interface DailyRow {
|
||||
date: string;
|
||||
click_pv: number;
|
||||
click_uv: number;
|
||||
copy_pv: number;
|
||||
order_count: number | null;
|
||||
canceled_count: number | null;
|
||||
gmv_cents: number | null;
|
||||
est_commission_cents: number | null;
|
||||
refund_profit_cents: number | null;
|
||||
settled_commission_cents: number | null;
|
||||
}
|
||||
interface DailyResp {
|
||||
group_name: string;
|
||||
is_meituan: boolean;
|
||||
days: number;
|
||||
rows: DailyRow[];
|
||||
}
|
||||
|
||||
// 群内微信用户(落地页授权拿到;nickname/headimgurl 仅 userinfo 授权后有)
|
||||
interface WxUser {
|
||||
openid: string;
|
||||
nickname: string | null;
|
||||
headimgurl: string | null;
|
||||
first_seen: string;
|
||||
visit_count: number;
|
||||
copy_count: number;
|
||||
}
|
||||
|
||||
const RANGES = [
|
||||
{ label: '近 3 天', value: 'd3' },
|
||||
{ label: '近 7 天', value: 'd7' },
|
||||
{ label: '近 14 天', value: 'd14' },
|
||||
{ label: '近 30 天', value: 'd30' },
|
||||
{ label: '今日(按小时)', value: 'h' },
|
||||
];
|
||||
|
||||
const METRICS: { key: keyof Omit<TsPoint, 'label'>; name: string }[] = [
|
||||
{ key: 'click_pv', name: '点击量' },
|
||||
{ key: 'click_uv', name: '独立点击' },
|
||||
{ key: 'copy_pv', name: '复制次数' },
|
||||
{ key: 'copy_uv', name: '独立复制' },
|
||||
];
|
||||
|
||||
function rangeToQuery(r: string): string {
|
||||
if (r === 'h') return 'granularity=hour';
|
||||
const days: Record<string, number> = { d3: 3, d7: 7, d14: 14, d30: 30 };
|
||||
return `granularity=day&days=${days[r] ?? 7}`;
|
||||
}
|
||||
|
||||
// 大表格只按天:天级范围 → 天数;今日小时(h) → null(此时隐藏表格,小时维度看上面折线)
|
||||
function rangeToDays(r: string): number | null {
|
||||
return ({ d3: 3, d7: 7, d14: 14, d30: 30 } as Record<string, number>)[r] ?? null;
|
||||
}
|
||||
|
||||
export default function GroupDetailPage() {
|
||||
const params = useParams();
|
||||
const router = useRouter();
|
||||
const id = params.id as string;
|
||||
const [range, setRange] = useState('d7');
|
||||
const [data, setData] = useState<TsResp | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const r = await api.get<TsResp>(
|
||||
`/admin/api/cps/groups/${id}/timeseries?${rangeToQuery(range)}`,
|
||||
);
|
||||
setData(r.data);
|
||||
} catch (e) {
|
||||
message.error(errMsg(e, '加载失败'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [id, range]);
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
// 按天大表格(独立于折线;今日小时模式 tableDays=null 时不取、不显示)
|
||||
const [daily, setDaily] = useState<DailyResp | null>(null);
|
||||
const tableDays = rangeToDays(range);
|
||||
const loadDaily = useCallback(async () => {
|
||||
if (tableDays == null) return;
|
||||
try {
|
||||
const r = await api.get<DailyResp>(`/admin/api/cps/groups/${id}/daily?days=${tableDays}`);
|
||||
setDaily(r.data);
|
||||
} catch (e) {
|
||||
message.error(errMsg(e, '明细加载失败'));
|
||||
}
|
||||
}, [id, tableDays]);
|
||||
useEffect(() => {
|
||||
loadDaily();
|
||||
}, [loadDaily]);
|
||||
|
||||
// 群内微信用户(独立于时间范围,累计画像)
|
||||
const [wxUsers, setWxUsers] = useState<WxUser[]>([]);
|
||||
const loadWxUsers = useCallback(async () => {
|
||||
try {
|
||||
const r = await api.get<{ users: WxUser[] }>(`/admin/api/cps/groups/${id}/wx-users`);
|
||||
setWxUsers(r.data.users || []);
|
||||
} catch (e) {
|
||||
message.error(errMsg(e, '用户列表加载失败'));
|
||||
}
|
||||
}, [id]);
|
||||
useEffect(() => {
|
||||
loadWxUsers();
|
||||
}, [loadWxUsers]);
|
||||
|
||||
const points = useMemo(() => data?.points ?? [], [data]);
|
||||
// 展平成「长表」给折线图:每个 (时间桶 × 指标) 一行,colorField 按指标分 4 条线。
|
||||
const chartData = useMemo(
|
||||
() =>
|
||||
points.flatMap((p) =>
|
||||
METRICS.map((m) => ({ label: p.label, 指标: m.name, value: Number(p[m.key]) })),
|
||||
),
|
||||
[points],
|
||||
);
|
||||
const totals = useMemo(
|
||||
() => METRICS.map((m) => ({ name: m.name, sum: points.reduce((s, p) => s + Number(p[m.key]), 0) })),
|
||||
[points],
|
||||
);
|
||||
const hasData = points.some((p) => p.click_pv || p.click_uv || p.copy_pv || p.copy_uv);
|
||||
|
||||
const config = {
|
||||
data: chartData,
|
||||
xField: 'label',
|
||||
yField: 'value',
|
||||
colorField: '指标',
|
||||
height: 420,
|
||||
legend: { color: { position: 'top' as const } },
|
||||
// 折线带数据点(每桶一个);大小用 style.r 固定 —— sizeField/shapeField 是数据字段映射,
|
||||
// 给常量会被 G2 当字段名找而失效。
|
||||
point: { style: { r: 3 } },
|
||||
// y 轴恒从 0 起、上限自适应取整;CPS 数据量小,固定 0 基线对比更直观。
|
||||
scale: { y: { domainMin: 0, nice: true } },
|
||||
axis: { x: { title: range === 'h' ? '小时' : '日期' }, y: { title: '次数' } },
|
||||
interaction: { tooltip: { shared: true } },
|
||||
};
|
||||
|
||||
const dailyColumns: ColumnsType<DailyRow> = [
|
||||
{ title: '日期', dataIndex: 'date', width: 64, fixed: 'left' },
|
||||
{ title: '点击', dataIndex: 'click_pv', width: 56 },
|
||||
{ title: '独立', dataIndex: 'click_uv', width: 56 },
|
||||
{ title: '复制', dataIndex: 'copy_pv', width: 56, render: (v: number) => v || '-' },
|
||||
{
|
||||
title: '点击→下单',
|
||||
key: 'rate',
|
||||
width: 86,
|
||||
render: (_, r) =>
|
||||
r.order_count == null
|
||||
? '-'
|
||||
: r.click_uv
|
||||
? `${((r.order_count / r.click_uv) * 100).toFixed(0)}%`
|
||||
: '-',
|
||||
},
|
||||
{ title: '有效单', dataIndex: 'order_count', width: 64, render: (v: number | null) => (v == null ? '-' : v) },
|
||||
{
|
||||
title: '取消/风控',
|
||||
dataIndex: 'canceled_count',
|
||||
width: 78,
|
||||
render: (v: number | null) => (v == null ? '-' : v ? <span style={{ color: '#cf1322' }}>{v}</span> : 0),
|
||||
},
|
||||
{ title: '成交额', dataIndex: 'gmv_cents', width: 84, render: (v: number | null) => (v == null ? '-' : yuan(v)) },
|
||||
{
|
||||
title: '预估佣金',
|
||||
dataIndex: 'est_commission_cents',
|
||||
width: 84,
|
||||
render: (v: number | null) => (v == null ? '-' : yuan(v)),
|
||||
},
|
||||
{
|
||||
title: '退款佣金',
|
||||
dataIndex: 'refund_profit_cents',
|
||||
width: 84,
|
||||
render: (v: number | null) =>
|
||||
v == null ? '-' : v ? <span style={{ color: '#cf1322' }}>{yuan(v)}</span> : yuan(0),
|
||||
},
|
||||
{
|
||||
title: '净佣金',
|
||||
key: 'net',
|
||||
width: 84,
|
||||
render: (_, r) =>
|
||||
r.est_commission_cents == null ? (
|
||||
'-'
|
||||
) : (
|
||||
<b>{yuan(r.est_commission_cents - (r.refund_profit_cents || 0))}</b>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '结算佣金',
|
||||
dataIndex: 'settled_commission_cents',
|
||||
width: 84,
|
||||
render: (v: number | null) => (v == null ? '-' : <span style={{ color: '#3f8600' }}>{yuan(v)}</span>),
|
||||
},
|
||||
];
|
||||
|
||||
const wxColumns: ColumnsType<WxUser> = [
|
||||
{
|
||||
title: '用户',
|
||||
key: 'user',
|
||||
render: (_, u) => (
|
||||
<Space>
|
||||
{u.headimgurl ? (
|
||||
<img
|
||||
src={u.headimgurl}
|
||||
alt=""
|
||||
style={{ width: 32, height: 32, borderRadius: '50%', objectFit: 'cover' }}
|
||||
/>
|
||||
) : (
|
||||
<span
|
||||
style={{ width: 32, height: 32, borderRadius: '50%', background: '#f0f0f0', display: 'inline-block' }}
|
||||
/>
|
||||
)}
|
||||
<span>{u.nickname || '(未授权昵称)'}</span>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '领券次数',
|
||||
dataIndex: 'copy_count',
|
||||
width: 90,
|
||||
sorter: (a, b) => a.copy_count - b.copy_count,
|
||||
render: (v: number) => (v ? <b style={{ color: '#ff4d4f' }}>{v}</b> : 0),
|
||||
},
|
||||
{ title: '点击次数', dataIndex: 'visit_count', width: 90 },
|
||||
{ title: '首次进入', dataIndex: 'first_seen', width: 160, render: (v: string) => formatUtcTime(v) },
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Space style={{ marginBottom: 16 }} align="center" wrap>
|
||||
<Button onClick={() => router.push('/cps')}>← 返回</Button>
|
||||
<span style={{ fontSize: 18, fontWeight: 600 }}>
|
||||
群「{data?.group_name ?? ''}」点击趋势
|
||||
</span>
|
||||
</Space>
|
||||
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
<Segmented options={RANGES} value={range} onChange={(v) => setRange(v as string)} />
|
||||
</Card>
|
||||
|
||||
<Row gutter={16} style={{ marginBottom: 16 }}>
|
||||
{totals.map((t) => (
|
||||
<Col key={t.name} xs={12} sm={6}>
|
||||
<Card size="small">
|
||||
<Statistic title={`${t.name}(合计)`} value={t.sum} />
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
<Card>
|
||||
<Spin spinning={loading}>
|
||||
{hasData ? (
|
||||
<Line {...config} />
|
||||
) : (
|
||||
<Empty
|
||||
style={{ padding: '80px 0' }}
|
||||
description={loading ? '加载中…' : '该时间范围内暂无点击数据'}
|
||||
/>
|
||||
)}
|
||||
</Spin>
|
||||
</Card>
|
||||
|
||||
{tableDays != null && (
|
||||
<Card title={`每日明细(近 ${tableDays} 天)`} style={{ marginTop: 16 }}>
|
||||
{daily && !daily.is_meituan && (
|
||||
<div style={{ fontSize: 12, color: '#999', marginBottom: 8 }}>
|
||||
该群非美团渠道,订单/佣金类无对账数据,显示「-」
|
||||
</div>
|
||||
)}
|
||||
<Table<DailyRow>
|
||||
rowKey="date"
|
||||
size="small"
|
||||
columns={dailyColumns}
|
||||
dataSource={daily?.rows ?? []}
|
||||
pagination={false}
|
||||
scroll={{ x: 900 }}
|
||||
/>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card title={`群内微信用户(${wxUsers.length})`} style={{ marginTop: 16 }}>
|
||||
<div style={{ fontSize: 12, color: '#999', marginBottom: 8 }}>
|
||||
在微信里打开本群落地页并授权的用户。「领券次数」= 点了几次「复制口令」;昵称/头像需用户点领券时授权补充。
|
||||
</div>
|
||||
<Table<WxUser>
|
||||
rowKey="openid"
|
||||
size="small"
|
||||
columns={wxColumns}
|
||||
dataSource={wxUsers}
|
||||
pagination={false}
|
||||
scroll={{ x: 560 }}
|
||||
locale={{ emptyText: '暂无授权用户' }}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,963 @@
|
||||
'use client';
|
||||
|
||||
// CPS 优惠券分发与对账。平台:美团(actId+sid,完整对账) / 淘宝(淘口令) / 京东(链接)。
|
||||
// 淘宝/京东无 API → 只统计点击,对账字段显示 "-"。单页 Tabs:对账统计/群管理/活动管理/订单明细。
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import Link from 'next/link';
|
||||
import type { ColumnsType } from 'antd/es/table';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Checkbox,
|
||||
Form,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Select,
|
||||
Space,
|
||||
Statistic,
|
||||
Table,
|
||||
Tabs,
|
||||
Tag,
|
||||
Typography,
|
||||
Upload,
|
||||
message,
|
||||
} from 'antd';
|
||||
import { api, errMsg } from '@/lib/api';
|
||||
import { canDo } from '@/lib/auth';
|
||||
import { formatUtcTime, yuan } from '@/lib/format';
|
||||
import { usePagedList } from '@/lib/usePagedList';
|
||||
import type {
|
||||
CpsActivity,
|
||||
CpsGroup,
|
||||
CpsGroupStat,
|
||||
CpsOrder,
|
||||
CpsReconcileResult,
|
||||
CpsReferralLinks,
|
||||
CpsStats,
|
||||
} from '@/lib/types';
|
||||
|
||||
const MT_STATUS: Record<string, { text: string; color: string }> = {
|
||||
'2': { text: '已付款', color: 'blue' },
|
||||
'3': { text: '已完成', color: 'cyan' },
|
||||
'4': { text: '已取消', color: 'red' },
|
||||
'5': { text: '风控', color: 'orange' },
|
||||
'6': { text: '已结算', color: 'green' },
|
||||
};
|
||||
|
||||
const PLATFORM_LABEL: Record<string, string> = { meituan: '美团', taobao: '淘宝', jd: '京东' };
|
||||
const PLATFORM_COLOR: Record<string, string> = { meituan: 'gold', taobao: 'volcano', jd: 'red' };
|
||||
const PLATFORM_OPTIONS = [
|
||||
{ label: '美团', value: 'meituan' },
|
||||
{ label: '淘宝', value: 'taobao' },
|
||||
{ label: '京东', value: 'jd' },
|
||||
];
|
||||
|
||||
const fmtRate = (r: string | null) => (r ? `${Number(r) / 100}%` : '-');
|
||||
const DASH = <span style={{ color: '#bbb' }}>-</span>;
|
||||
const platformTags = (ps: string[] | undefined) =>
|
||||
(ps || []).map((p) => (
|
||||
<Tag key={p} color={PLATFORM_COLOR[p]} style={{ marginInlineEnd: 4 }}>
|
||||
{PLATFORM_LABEL[p] || p}
|
||||
</Tag>
|
||||
));
|
||||
|
||||
export default function CpsPage() {
|
||||
return (
|
||||
<div>
|
||||
<h2>CPS 优惠券分发与对账</h2>
|
||||
<Typography.Paragraph type="secondary" style={{ marginTop: -8 }}>
|
||||
美团(actId + sid,可完整对账下单/佣金) / 淘宝(淘口令) / 京东(链接)。淘宝、京东无开放 API,
|
||||
<b>只统计点击</b>,对账字段显示「-」。发群链接都是咱的落地页 /c/xxx。
|
||||
</Typography.Paragraph>
|
||||
<Tabs
|
||||
defaultActiveKey="stats"
|
||||
destroyInactiveTabPane
|
||||
items={[
|
||||
{ key: 'stats', label: '对账统计', children: <StatsTab /> },
|
||||
{ key: 'groups', label: '群管理', children: <GroupsTab /> },
|
||||
{ key: 'activities', label: '活动管理', children: <ActivitiesTab /> },
|
||||
{ key: 'orders', label: '订单明细', children: <OrdersTab /> },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ───────────── 对账统计 ─────────────
|
||||
function StatsTab() {
|
||||
const [days, setDays] = useState(30);
|
||||
const [data, setData] = useState<CpsStats | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [reconciling, setReconciling] = useState(false);
|
||||
const canReconcile = canDo(['finance']);
|
||||
|
||||
const load = useCallback(async (d: number) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const resp = await api.get<CpsStats>('/admin/api/cps/stats', { params: { days: d } });
|
||||
setData(resp.data);
|
||||
} catch (e) {
|
||||
message.error(errMsg(e));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
load(days);
|
||||
}, [days, load]);
|
||||
|
||||
const reconcile = async () => {
|
||||
setReconciling(true);
|
||||
try {
|
||||
const resp = await api.post<CpsReconcileResult>('/admin/api/cps/orders/reconcile', null, {
|
||||
params: { days },
|
||||
});
|
||||
const r = resp.data;
|
||||
message.success(`对账完成:拉取 ${r.fetched} 单(新增 ${r.inserted}、更新 ${r.updated})`);
|
||||
load(days);
|
||||
} catch (e) {
|
||||
message.error(errMsg(e));
|
||||
} finally {
|
||||
setReconciling(false);
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ColumnsType<CpsGroupStat> = [
|
||||
{
|
||||
title: '群',
|
||||
dataIndex: 'name',
|
||||
width: 140,
|
||||
ellipsis: true,
|
||||
fixed: 'left',
|
||||
render: (v: string, r) =>
|
||||
r.group_id ? <Link href={`/cps/groups/${r.group_id}`}>{v}</Link> : v,
|
||||
},
|
||||
{ title: '平台', key: 'platforms', width: 130, render: (_, r) => platformTags(r.platforms) },
|
||||
{ title: '人数', dataIndex: 'member_count', width: 64, render: (v) => v ?? '-' },
|
||||
{ title: '点击', dataIndex: 'click_pv', width: 64 },
|
||||
{ title: '独立', dataIndex: 'click_uv', width: 64 },
|
||||
{
|
||||
title: '复制',
|
||||
dataIndex: 'copy_pv',
|
||||
width: 64,
|
||||
render: (v: number) => (v ? v : DASH),
|
||||
},
|
||||
{
|
||||
title: '有效单',
|
||||
dataIndex: 'order_count',
|
||||
width: 70,
|
||||
render: (v: number | null) => (v == null ? DASH : v),
|
||||
},
|
||||
{
|
||||
title: '取消/风控',
|
||||
dataIndex: 'canceled_count',
|
||||
width: 80,
|
||||
render: (v: number | null) => (v == null ? DASH : v ? <span style={{ color: '#cf1322' }}>{v}</span> : 0),
|
||||
},
|
||||
{
|
||||
title: '点击→下单',
|
||||
key: 'rate',
|
||||
width: 88,
|
||||
render: (_, r) =>
|
||||
r.order_count == null ? DASH : r.click_uv ? `${((r.order_count / r.click_uv) * 100).toFixed(0)}%` : '-',
|
||||
},
|
||||
{
|
||||
title: '成交额',
|
||||
dataIndex: 'gmv_cents',
|
||||
width: 96,
|
||||
render: (v: number | null) => (v == null ? DASH : yuan(v)),
|
||||
},
|
||||
{
|
||||
title: '预估佣金',
|
||||
dataIndex: 'est_commission_cents',
|
||||
width: 100,
|
||||
sorter: (a, b) => (a.est_commission_cents || 0) - (b.est_commission_cents || 0),
|
||||
render: (v: number | null) => (v == null ? DASH : <b>{yuan(v)}</b>),
|
||||
},
|
||||
{
|
||||
title: '已结算佣金',
|
||||
dataIndex: 'settled_commission_cents',
|
||||
width: 100,
|
||||
render: (v: number | null) => (v == null ? DASH : <span style={{ color: '#3f8600' }}>{yuan(v)}</span>),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Space style={{ marginBottom: 16 }} wrap>
|
||||
<span>统计区间</span>
|
||||
<Select
|
||||
value={days}
|
||||
onChange={setDays}
|
||||
style={{ width: 120 }}
|
||||
options={[
|
||||
{ value: 7, label: '近 7 天' },
|
||||
{ value: 30, label: '近 30 天' },
|
||||
{ value: 90, label: '近 90 天' },
|
||||
]}
|
||||
/>
|
||||
{canReconcile && (
|
||||
<Button type="primary" loading={reconciling} onClick={reconcile}>
|
||||
刷新对账(拉美团订单)
|
||||
</Button>
|
||||
)}
|
||||
<Typography.Text type="secondary">淘宝/京东无对账,「-」即无法对账;佣金以「已结算」为准</Typography.Text>
|
||||
</Space>
|
||||
|
||||
<Space size="large" style={{ marginBottom: 16 }} wrap>
|
||||
<Card size="small">
|
||||
<Statistic title="有效订单数(美团)" value={data?.total_order_count ?? 0} />
|
||||
</Card>
|
||||
<Card size="small">
|
||||
<Statistic
|
||||
title="预估佣金"
|
||||
value={(data?.total_est_commission_cents ?? 0) / 100}
|
||||
precision={2}
|
||||
prefix="¥"
|
||||
/>
|
||||
</Card>
|
||||
<Card size="small">
|
||||
<Statistic
|
||||
title="已结算佣金"
|
||||
value={(data?.total_settled_commission_cents ?? 0) / 100}
|
||||
precision={2}
|
||||
prefix="¥"
|
||||
valueStyle={{ color: '#3f8600' }}
|
||||
/>
|
||||
</Card>
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
rowKey={(r) => r.sid ?? `gid-${r.group_id ?? 'none'}`}
|
||||
columns={columns}
|
||||
dataSource={data?.groups ?? []}
|
||||
loading={loading}
|
||||
pagination={false}
|
||||
scroll={{ x: 1200 }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ───────────── 群管理 ─────────────
|
||||
function GroupsTab() {
|
||||
const [keyword, setKeyword] = useState('');
|
||||
const [applied, setApplied] = useState<Record<string, unknown>>({});
|
||||
const { items, total, page, pageSize, loading, onChange, reload } = usePagedList<CpsGroup>(
|
||||
'/admin/api/cps/groups',
|
||||
applied,
|
||||
);
|
||||
const canManage = canDo(['operator']);
|
||||
|
||||
const [editing, setEditing] = useState<CpsGroup | null>(null);
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [linkGroup, setLinkGroup] = useState<CpsGroup | null>(null);
|
||||
|
||||
const delGroup = async (g: CpsGroup) => {
|
||||
try {
|
||||
await api.delete(`/admin/api/cps/groups/${g.id}`);
|
||||
message.success('已删除群');
|
||||
reload();
|
||||
} catch (e) {
|
||||
message.error(errMsg(e));
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ColumnsType<CpsGroup> = [
|
||||
{ title: 'ID', dataIndex: 'id', width: 60 },
|
||||
{ title: '群名', dataIndex: 'name', width: 160, ellipsis: true },
|
||||
{ title: '平台', key: 'platforms', width: 140, render: (_, g) => platformTags(g.platforms) },
|
||||
{
|
||||
title: 'sid',
|
||||
dataIndex: 'sid',
|
||||
width: 130,
|
||||
render: (v: string | null) => (v ? <Typography.Text copyable>{v}</Typography.Text> : DASH),
|
||||
},
|
||||
{ title: '人数', dataIndex: 'member_count', width: 70, render: (v) => v ?? '-' },
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 80,
|
||||
render: (s: string) => <Tag color={s === 'active' ? 'green' : 'default'}>{s}</Tag>,
|
||||
},
|
||||
{ title: '创建时间', dataIndex: 'created_at', width: 150, render: (v: string) => formatUtcTime(v) },
|
||||
{
|
||||
title: '操作',
|
||||
key: 'op',
|
||||
fixed: 'right',
|
||||
width: 200,
|
||||
render: (_, g) => (
|
||||
<Space onClick={(e) => e.stopPropagation()}>
|
||||
{canManage && <a onClick={() => setLinkGroup(g)}>生成链接</a>}
|
||||
{canManage && <a onClick={() => setEditing(g)}>编辑</a>}
|
||||
{canManage && (
|
||||
<Popconfirm
|
||||
title="删除这个群?"
|
||||
description="已发出的链接仍可用,但该群不再计入统计。"
|
||||
okText="删除"
|
||||
okButtonProps={{ danger: true }}
|
||||
onConfirm={() => delGroup(g)}
|
||||
>
|
||||
<a style={{ color: '#cf1322' }}>删除</a>
|
||||
</Popconfirm>
|
||||
)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Space style={{ marginBottom: 16 }} wrap>
|
||||
<Input
|
||||
placeholder="群名/sid"
|
||||
value={keyword}
|
||||
onChange={(e) => setKeyword(e.target.value)}
|
||||
onPressEnter={() => setApplied({ keyword: keyword || undefined })}
|
||||
allowClear
|
||||
style={{ width: 180 }}
|
||||
/>
|
||||
<Button type="primary" onClick={() => setApplied({ keyword: keyword || undefined })}>
|
||||
查询
|
||||
</Button>
|
||||
{canManage && <Button onClick={() => setCreateOpen(true)}>新建群</Button>}
|
||||
</Space>
|
||||
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={items}
|
||||
loading={loading}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize,
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
showTotal: (t) => `共 ${t} 个群`,
|
||||
onChange,
|
||||
}}
|
||||
scroll={{ x: 1090 }}
|
||||
/>
|
||||
|
||||
<GroupFormModal open={createOpen} group={null} onClose={() => setCreateOpen(false)} onDone={reload} />
|
||||
<GroupFormModal open={!!editing} group={editing} onClose={() => setEditing(null)} onDone={reload} />
|
||||
<ReferralLinkModal group={linkGroup} onClose={() => setLinkGroup(null)} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GroupFormModal({
|
||||
open,
|
||||
group,
|
||||
onClose,
|
||||
onDone,
|
||||
}: {
|
||||
open: boolean;
|
||||
group: CpsGroup | null;
|
||||
onClose: () => void;
|
||||
onDone: () => void;
|
||||
}) {
|
||||
const [form] = Form.useForm();
|
||||
const isEdit = !!group;
|
||||
const watchPlatforms = (Form.useWatch('platforms', form) as string[] | undefined) || [];
|
||||
const hasMeituan = watchPlatforms.includes('meituan');
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
if (group) {
|
||||
form.setFieldsValue({
|
||||
name: group.name,
|
||||
platforms: group.platforms,
|
||||
member_count: group.member_count,
|
||||
remark: group.remark,
|
||||
status: group.status,
|
||||
});
|
||||
} else {
|
||||
form.resetFields();
|
||||
form.setFieldsValue({ platforms: ['meituan'] });
|
||||
}
|
||||
}
|
||||
}, [open, group, form]);
|
||||
|
||||
const submit = async () => {
|
||||
const v = await form.validateFields();
|
||||
try {
|
||||
if (isEdit && group) {
|
||||
await api.patch(`/admin/api/cps/groups/${group.id}`, v);
|
||||
message.success('已更新群');
|
||||
} else {
|
||||
await api.post('/admin/api/cps/groups', v);
|
||||
message.success('已新建群');
|
||||
}
|
||||
onClose();
|
||||
onDone();
|
||||
} catch (e) {
|
||||
message.error(errMsg(e));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal title={isEdit ? '编辑群' : '新建群'} open={open} onOk={submit} onCancel={onClose} destroyOnHidden>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="name" label="群名" rules={[{ required: true, message: '请输入群名' }]}>
|
||||
<Input placeholder="如:宝妈优惠群1" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="platforms"
|
||||
label="这个群发哪些平台的券(可多选)"
|
||||
rules={[{ required: true, message: '至少选一个平台' }]}
|
||||
>
|
||||
<Checkbox.Group options={PLATFORM_OPTIONS} />
|
||||
</Form.Item>
|
||||
{hasMeituan && !isEdit && (
|
||||
<Form.Item
|
||||
name="sid"
|
||||
label="美团 sid(渠道标识,留空自动生成 g+ID)"
|
||||
rules={[{ pattern: /^[A-Za-z0-9]+$/, message: '仅字母和数字' }]}
|
||||
>
|
||||
<Input placeholder="留空自动生成;或自定义如 baoma1" maxLength={64} />
|
||||
</Form.Item>
|
||||
)}
|
||||
{hasMeituan && isEdit && (
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12, display: 'block', marginBottom: 12 }}>
|
||||
当前 sid:{group?.sid || '(无,保存后如缺会在生成美团链接时报错)'}
|
||||
</Typography.Text>
|
||||
)}
|
||||
<Form.Item name="member_count" label="群人数(用于估算转化率)">
|
||||
<InputNumber style={{ width: '100%' }} min={0} placeholder="如 200" />
|
||||
</Form.Item>
|
||||
{isEdit && (
|
||||
<Form.Item name="status" label="状态">
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'active', label: 'active(启用)' },
|
||||
{ value: 'archived', label: 'archived(归档)' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item name="remark" label="备注">
|
||||
<Input.TextArea rows={2} maxLength={256} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function ReferralLinkModal({ group, onClose }: { group: CpsGroup | null; onClose: () => void }) {
|
||||
const [activities, setActivities] = useState<CpsActivity[]>([]);
|
||||
const [activityIds, setActivityIds] = useState<number[]>([]);
|
||||
const [result, setResult] = useState<CpsReferralLinks | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!group) {
|
||||
setActivityIds([]);
|
||||
setResult(null);
|
||||
return;
|
||||
}
|
||||
api
|
||||
.get('/admin/api/cps/activities', { params: { status: 'active', limit: 100 } })
|
||||
.then((r) => setActivities(r.data.items as CpsActivity[]))
|
||||
.catch((e) => message.error(errMsg(e)));
|
||||
}, [group]);
|
||||
|
||||
// 只列群平台范围内的活动
|
||||
const groupActivities = activities.filter((a) => (group?.platforms || []).includes(a.platform));
|
||||
|
||||
const generate = async () => {
|
||||
if (!group || !activityIds.length) {
|
||||
message.warning('请勾选要生成的活动');
|
||||
return;
|
||||
}
|
||||
setLoading(true);
|
||||
try {
|
||||
const resp = await api.post<CpsReferralLinks>('/admin/api/cps/referral-links', {
|
||||
group_id: group.id,
|
||||
activity_ids: activityIds,
|
||||
});
|
||||
setResult(resp.data);
|
||||
} catch (e) {
|
||||
message.error(errMsg(e));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
title={`生成发群链接 - ${group?.name ?? ''}`}
|
||||
open={!!group}
|
||||
onCancel={onClose}
|
||||
onOk={generate}
|
||||
okText="生成"
|
||||
confirmLoading={loading}
|
||||
destroyOnHidden
|
||||
width={660}
|
||||
>
|
||||
<Typography.Paragraph type="secondary" style={{ fontSize: 12 }}>
|
||||
勾选要发的活动,每个生成一条咱的落地页链接;复制后发到群里。
|
||||
</Typography.Paragraph>
|
||||
{groupActivities.length === 0 ? (
|
||||
<Typography.Text type="warning">
|
||||
该群平台({(group?.platforms || []).map((p) => PLATFORM_LABEL[p]).join('/')})下没有活动,请先到「活动管理」新建。
|
||||
</Typography.Text>
|
||||
) : (
|
||||
<Checkbox.Group
|
||||
value={activityIds}
|
||||
onChange={(v) => setActivityIds(v as number[])}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Space direction="vertical" style={{ width: '100%' }}>
|
||||
{groupActivities.map((a) => (
|
||||
<Checkbox key={a.id} value={a.id}>
|
||||
<Tag color={PLATFORM_COLOR[a.platform]}>{PLATFORM_LABEL[a.platform]}</Tag>
|
||||
{a.name}
|
||||
{a.act_id ? <span style={{ color: '#999' }}> (actId={a.act_id})</span> : null}
|
||||
</Checkbox>
|
||||
))}
|
||||
</Space>
|
||||
</Checkbox.Group>
|
||||
)}
|
||||
|
||||
{result && (
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Typography.Text strong>👇 生成结果(逐条复制发群):</Typography.Text>
|
||||
{result.results.map((item) => (
|
||||
<div
|
||||
key={item.code}
|
||||
style={{
|
||||
marginTop: 8,
|
||||
padding: 10,
|
||||
background: '#f6ffed',
|
||||
border: '1px solid #b7eb8f',
|
||||
borderRadius: 6,
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: 4 }}>
|
||||
<Tag color={PLATFORM_COLOR[item.platform]}>{PLATFORM_LABEL[item.platform]}</Tag>
|
||||
<span style={{ fontSize: 13 }}>{item.activity_name}</span>
|
||||
</div>
|
||||
<Typography.Text copyable={{ text: item.redirect_url }} style={{ fontSize: 14, wordBreak: 'break-all' }}>
|
||||
{item.redirect_url}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
))}
|
||||
{result.results.some((i) => !i.redirect_url.startsWith('http')) && (
|
||||
<Typography.Text type="warning" style={{ fontSize: 12 }}>
|
||||
⚠️ 部分为相对路径:后端未配 CPS_REDIRECT_BASE,请配跳转域名后重启。
|
||||
</Typography.Text>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
// ───────────── 活动管理 ─────────────
|
||||
function ActivitiesTab() {
|
||||
const { items, total, page, pageSize, loading, onChange, reload } = usePagedList<CpsActivity>(
|
||||
'/admin/api/cps/activities',
|
||||
{},
|
||||
);
|
||||
const canManage = canDo(['operator']);
|
||||
const [createOpen, setCreateOpen] = useState(false);
|
||||
const [editing, setEditing] = useState<CpsActivity | null>(null);
|
||||
|
||||
const delActivity = async (a: CpsActivity) => {
|
||||
try {
|
||||
await api.delete(`/admin/api/cps/activities/${a.id}`);
|
||||
message.success('已删除活动');
|
||||
reload();
|
||||
} catch (e) {
|
||||
message.error(errMsg(e));
|
||||
}
|
||||
};
|
||||
|
||||
const columns: ColumnsType<CpsActivity> = [
|
||||
{ title: 'ID', dataIndex: 'id', width: 60 },
|
||||
{
|
||||
title: '平台',
|
||||
dataIndex: 'platform',
|
||||
width: 80,
|
||||
render: (v: string) => <Tag color={PLATFORM_COLOR[v]}>{PLATFORM_LABEL[v] || v}</Tag>,
|
||||
},
|
||||
{ title: '活动名', dataIndex: 'name', width: 180, ellipsis: true },
|
||||
{
|
||||
title: 'actId(美团)',
|
||||
dataIndex: 'act_id',
|
||||
width: 110,
|
||||
render: (v: string | null) => v || '-',
|
||||
},
|
||||
{
|
||||
title: '淘口令/链接',
|
||||
dataIndex: 'payload',
|
||||
width: 240,
|
||||
ellipsis: true,
|
||||
render: (v: string | null) => v || '-',
|
||||
},
|
||||
{
|
||||
title: '落地页图',
|
||||
dataIndex: 'image_url',
|
||||
width: 80,
|
||||
render: (v: string | null) =>
|
||||
v ? (
|
||||
<img src={v} alt="" style={{ width: 36, height: 46, objectFit: 'cover', borderRadius: 4 }} />
|
||||
) : (
|
||||
'-'
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
width: 80,
|
||||
render: (s: string) => <Tag color={s === 'active' ? 'green' : 'default'}>{s}</Tag>,
|
||||
},
|
||||
{ title: '创建时间', dataIndex: 'created_at', width: 150, render: (v: string) => formatUtcTime(v) },
|
||||
...(canManage
|
||||
? ([
|
||||
{
|
||||
title: '操作',
|
||||
key: 'op',
|
||||
fixed: 'right' as const,
|
||||
width: 120,
|
||||
render: (_: unknown, a: CpsActivity) => (
|
||||
<Space>
|
||||
<a onClick={() => setEditing(a)}>编辑</a>
|
||||
<Popconfirm
|
||||
title="删除这个活动?"
|
||||
description="已生成的链接仍可用;淘宝活动删除后其落地页改用默认图。"
|
||||
okText="删除"
|
||||
okButtonProps={{ danger: true }}
|
||||
onConfirm={() => delActivity(a)}
|
||||
>
|
||||
<a style={{ color: '#cf1322' }}>删除</a>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
] as ColumnsType<CpsActivity>)
|
||||
: []),
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
{canManage && (
|
||||
<Space style={{ marginBottom: 16 }}>
|
||||
<Button type="primary" onClick={() => setCreateOpen(true)}>
|
||||
新建活动
|
||||
</Button>
|
||||
</Space>
|
||||
)}
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={items}
|
||||
loading={loading}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize,
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
showTotal: (t) => `共 ${t} 个活动`,
|
||||
onChange,
|
||||
}}
|
||||
scroll={{ x: 1200 }}
|
||||
/>
|
||||
<ActivityFormModal open={createOpen} activity={null} onClose={() => setCreateOpen(false)} onDone={reload} />
|
||||
<ActivityFormModal open={!!editing} activity={editing} onClose={() => setEditing(null)} onDone={reload} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 淘宝活动落地页图选择器:上传新图 或 点选已有图。受控组件(value = image_url 绝对 URL)。
|
||||
function ImagePicker({ value, onChange }: { value?: string | null; onChange?: (v: string) => void }) {
|
||||
const [existing, setExisting] = useState<string[]>([]);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
api
|
||||
.get<{ images: string[] }>('/admin/api/cps/activity-images')
|
||||
.then((r) => setExisting(r.data.images || []))
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
const upload = async (file: File) => {
|
||||
setUploading(true);
|
||||
try {
|
||||
const fd = new FormData();
|
||||
fd.append('file', file);
|
||||
const r = await api.post<{ url: string }>('/admin/api/cps/upload-image', fd);
|
||||
onChange?.(r.data.url);
|
||||
setExisting((prev) => (prev.includes(r.data.url) ? prev : [r.data.url, ...prev]));
|
||||
message.success('图片已上传');
|
||||
} catch (e) {
|
||||
message.error(errMsg(e, '上传失败'));
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const thumb = (u: string, w: number, selected: boolean) => (
|
||||
<img
|
||||
key={u}
|
||||
src={u}
|
||||
alt="落地页图"
|
||||
onClick={() => onChange?.(u)}
|
||||
style={{
|
||||
width: w,
|
||||
height: Math.round(w * 1.27),
|
||||
objectFit: 'cover',
|
||||
borderRadius: 6,
|
||||
cursor: 'pointer',
|
||||
border: selected ? '2px solid #ff4d4f' : '2px solid #f0f0f0',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Upload
|
||||
accept="image/*"
|
||||
showUploadList={false}
|
||||
beforeUpload={(file) => {
|
||||
upload(file);
|
||||
return false; // 阻止 antd 默认上传,走我们自己的端点
|
||||
}}
|
||||
>
|
||||
<Button loading={uploading}>上传新图</Button>
|
||||
</Upload>
|
||||
{value && (
|
||||
<div style={{ marginTop: 8, display: 'flex', alignItems: 'center', gap: 8 }}>
|
||||
<span style={{ fontSize: 12, color: '#999' }}>当前:</span>
|
||||
{thumb(value, 90, true)}
|
||||
</div>
|
||||
)}
|
||||
{existing.length > 0 && (
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<div style={{ fontSize: 12, color: '#999', marginBottom: 6 }}>或选择已有图:</div>
|
||||
<Space wrap>{existing.map((u) => thumb(u, 56, u === value))}</Space>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ActivityFormModal({
|
||||
open,
|
||||
activity,
|
||||
onClose,
|
||||
onDone,
|
||||
}: {
|
||||
open: boolean;
|
||||
activity: CpsActivity | null;
|
||||
onClose: () => void;
|
||||
onDone: () => void;
|
||||
}) {
|
||||
const [form] = Form.useForm();
|
||||
const isEdit = !!activity;
|
||||
const platform = (Form.useWatch('platform', form) as string | undefined) || 'meituan';
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
if (activity) {
|
||||
form.setFieldsValue({
|
||||
name: activity.name,
|
||||
platform: activity.platform,
|
||||
act_id: activity.act_id,
|
||||
product_view_sign: activity.product_view_sign,
|
||||
payload: activity.payload,
|
||||
image_url: activity.image_url,
|
||||
remark: activity.remark,
|
||||
status: activity.status,
|
||||
});
|
||||
} else {
|
||||
form.resetFields();
|
||||
form.setFieldsValue({ platform: 'meituan' });
|
||||
}
|
||||
}
|
||||
}, [open, activity, form]);
|
||||
|
||||
const submit = async () => {
|
||||
const v = await form.validateFields();
|
||||
if (v.platform === 'meituan') {
|
||||
if (!v.act_id && !v.product_view_sign) {
|
||||
message.warning('美团活动:actId 与 productViewSign 至少填一个');
|
||||
return;
|
||||
}
|
||||
} else if (!v.payload) {
|
||||
message.warning(v.platform === 'taobao' ? '请填淘口令' : '请填京东链接');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
if (isEdit && activity) {
|
||||
await api.patch(`/admin/api/cps/activities/${activity.id}`, v);
|
||||
message.success('已更新活动');
|
||||
} else {
|
||||
await api.post('/admin/api/cps/activities', v);
|
||||
message.success('已新建活动');
|
||||
}
|
||||
onClose();
|
||||
onDone();
|
||||
} catch (e) {
|
||||
message.error(errMsg(e));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal title={isEdit ? '编辑活动' : '新建活动'} open={open} onOk={submit} onCancel={onClose} destroyOnHidden>
|
||||
<Form form={form} layout="vertical" initialValues={{ platform: 'meituan' }}>
|
||||
<Form.Item name="name" label="活动名" rules={[{ required: true }]}>
|
||||
<Input placeholder="如:点我领取大额红包" />
|
||||
</Form.Item>
|
||||
<Form.Item name="platform" label="平台" rules={[{ required: true }]}>
|
||||
<Select options={PLATFORM_OPTIONS} />
|
||||
</Form.Item>
|
||||
|
||||
{platform === 'meituan' && (
|
||||
<>
|
||||
<Form.Item name="act_id" label="actId(美团活动物料 ID,推荐)">
|
||||
<Input placeholder="美团联盟「我要推广-活动推广」第一列 ID,如 792" maxLength={64} />
|
||||
</Form.Item>
|
||||
<Form.Item name="product_view_sign" label="productViewSign(商品券,与 actId 二选一)">
|
||||
<Input maxLength={128} />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
{platform === 'taobao' && (
|
||||
<>
|
||||
<Form.Item
|
||||
name="payload"
|
||||
label="淘口令(整段,运营从淘宝联盟复制,落地页原样复制给用户)"
|
||||
rules={[{ required: true, message: '请填淘口令' }]}
|
||||
>
|
||||
<Input.TextArea rows={4} placeholder="如:8¥abcXYZ¥ https://m.tb.cn/h.xxx 长按复制本条..." maxLength={4096} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="image_url"
|
||||
label="落地页图(用户点链接后看到的页面主图,上传新图或选已有)"
|
||||
rules={[{ required: true, message: '请上传或选择落地页图' }]}
|
||||
>
|
||||
<ImagePicker />
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
{platform === 'jd' && (
|
||||
<Form.Item
|
||||
name="payload"
|
||||
label="京东推广链接(落地页 302 跳到它)"
|
||||
rules={[{ required: true, message: '请填京东链接' }]}
|
||||
>
|
||||
<Input placeholder="如:https://u.jd.com/xxxxxx" maxLength={1024} />
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item name="remark" label="备注">
|
||||
<Input.TextArea rows={2} maxLength={256} />
|
||||
</Form.Item>
|
||||
{isEdit && (
|
||||
<Form.Item name="status" label="状态">
|
||||
<Select
|
||||
options={[
|
||||
{ value: 'active', label: '启用(active)' },
|
||||
{ value: 'archived', label: '归档(archived)' },
|
||||
]}
|
||||
/>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
// ───────────── 订单明细 ─────────────
|
||||
function OrdersTab() {
|
||||
const [sid, setSid] = useState('');
|
||||
const [status, setStatus] = useState<string | undefined>();
|
||||
const [applied, setApplied] = useState<Record<string, unknown>>({});
|
||||
const { items, total, page, pageSize, loading, onChange } = usePagedList<CpsOrder>(
|
||||
'/admin/api/cps/orders',
|
||||
applied,
|
||||
);
|
||||
|
||||
const columns: ColumnsType<CpsOrder> = [
|
||||
{
|
||||
title: '订单号',
|
||||
dataIndex: 'order_id',
|
||||
width: 200,
|
||||
ellipsis: true,
|
||||
render: (v: string) => <Typography.Text copyable>{v}</Typography.Text>,
|
||||
},
|
||||
{ title: 'sid', dataIndex: 'sid', width: 120, render: (v) => v || '-' },
|
||||
{ title: '商品', dataIndex: 'product_name', width: 200, ellipsis: true, render: (v) => v || '-' },
|
||||
{
|
||||
title: '成交额',
|
||||
dataIndex: 'pay_price_cents',
|
||||
width: 100,
|
||||
render: (v: number | null) => (v != null ? yuan(v) : '-'),
|
||||
},
|
||||
{
|
||||
title: '佣金',
|
||||
dataIndex: 'commission_cents',
|
||||
width: 90,
|
||||
render: (v: number | null) => (v != null ? yuan(v) : '-'),
|
||||
},
|
||||
{ title: '佣金率', dataIndex: 'commission_rate', width: 80, render: fmtRate },
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'mt_status',
|
||||
width: 90,
|
||||
render: (s: string | null) => {
|
||||
const m = s ? MT_STATUS[s] : null;
|
||||
return m ? <Tag color={m.color}>{m.text}</Tag> : s || '-';
|
||||
},
|
||||
},
|
||||
{ title: '支付时间', dataIndex: 'pay_time', width: 150, render: (v) => formatUtcTime(v) },
|
||||
];
|
||||
|
||||
const search = () => setApplied({ sid: sid || undefined, mt_status: status });
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Space style={{ marginBottom: 16 }} wrap>
|
||||
<Input
|
||||
placeholder="sid"
|
||||
value={sid}
|
||||
onChange={(e) => setSid(e.target.value)}
|
||||
onPressEnter={search}
|
||||
allowClear
|
||||
style={{ width: 150 }}
|
||||
/>
|
||||
<Select
|
||||
placeholder="状态"
|
||||
value={status}
|
||||
onChange={setStatus}
|
||||
allowClear
|
||||
style={{ width: 120 }}
|
||||
options={Object.entries(MT_STATUS).map(([v, m]) => ({ value: v, label: m.text }))}
|
||||
/>
|
||||
<Button type="primary" onClick={search}>
|
||||
查询
|
||||
</Button>
|
||||
</Space>
|
||||
<Table
|
||||
rowKey="id"
|
||||
columns={columns}
|
||||
dataSource={items}
|
||||
loading={loading}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize,
|
||||
total,
|
||||
showSizeChanger: true,
|
||||
showTotal: (t) => `共 ${t} 单`,
|
||||
onChange,
|
||||
}}
|
||||
scroll={{ x: 1030 }}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -11,7 +11,9 @@ import {
|
||||
MessageOutlined,
|
||||
MobileOutlined,
|
||||
MoneyCollectOutlined,
|
||||
ProfileOutlined,
|
||||
SettingOutlined,
|
||||
ShareAltOutlined,
|
||||
TeamOutlined,
|
||||
UserOutlined,
|
||||
} from '@ant-design/icons';
|
||||
@@ -27,8 +29,10 @@ const MENU = [
|
||||
{ key: '/devices', icon: <MobileOutlined />, label: '设备管理' },
|
||||
{ key: '/withdraws', icon: <MoneyCollectOutlined />, label: '提现管理' },
|
||||
{ key: '/price-reports', icon: <FlagOutlined />, label: '上报审核' },
|
||||
{ key: '/comparison-records', icon: <ProfileOutlined />, label: '比价记录' },
|
||||
{ key: '/feedbacks', icon: <MessageOutlined />, label: '反馈工单' },
|
||||
{ key: '/ad-revenue', icon: <BarChartOutlined />, label: '广告数据' },
|
||||
{ key: '/ad-revenue', icon: <BarChartOutlined />, label: '广告管理' },
|
||||
{ key: '/cps', icon: <ShareAltOutlined />, label: 'CPS 分发' },
|
||||
{ key: '/config', icon: <SettingOutlined />, label: '系统配置' },
|
||||
{ key: '/admins', icon: <TeamOutlined />, label: '管理员', superOnly: true },
|
||||
{ key: '/audit-logs', icon: <FileSearchOutlined />, label: '审计日志' },
|
||||
|
||||
@@ -189,6 +189,70 @@ export interface AuditLog {
|
||||
}
|
||||
|
||||
// 广告收益报表聚合行下钻的单条展示明细
|
||||
// ===== 比价记录(admin debug 页)=====
|
||||
export interface ComparisonRecordListItem {
|
||||
id: number;
|
||||
user_id: number;
|
||||
phone: string | null;
|
||||
nickname: string | null;
|
||||
business_type: string;
|
||||
trace_id: string;
|
||||
trace_url: string | null;
|
||||
status: string;
|
||||
information: string | null;
|
||||
store_name: string | null;
|
||||
source_platform_name: string | null;
|
||||
best_platform_name: string | null;
|
||||
source_price_cents: number | null;
|
||||
best_price_cents: number | null;
|
||||
saved_amount_cents: number | null;
|
||||
total_ms: number | null;
|
||||
step_count: number | null;
|
||||
llm_call_count: number | null;
|
||||
retry_count: number | null;
|
||||
device_model: string | null;
|
||||
rom_vendor: string | null;
|
||||
rom_name: string | null;
|
||||
android_version: string | null;
|
||||
app_version: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
// 单次 LLM 调用明细(pricebot chat() 收口落盘,server 按 trace 拉来)
|
||||
export interface LlmCall {
|
||||
ts?: number;
|
||||
scene: string;
|
||||
model: string;
|
||||
input_messages: { role: string; content: string }[];
|
||||
output: string | null;
|
||||
usage: { prompt_tokens?: number; completion_tokens?: number; total_tokens?: number } | null;
|
||||
latency_ms: number | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
export interface ComparisonRecordDetail extends ComparisonRecordListItem {
|
||||
source_platform_id: string | null;
|
||||
source_package: string | null;
|
||||
best_platform_id: string | null;
|
||||
best_deeplink: string | null;
|
||||
is_source_best: boolean | null;
|
||||
total_dish_count: number | null;
|
||||
skipped_dish_count: number | null;
|
||||
device_id: string | null;
|
||||
items: { name: string; qty?: number; specs?: string[] }[];
|
||||
comparison_results: Record<string, unknown>[];
|
||||
skipped_dish_names: string[];
|
||||
device_manufacturer: string | null;
|
||||
rom_version: number | null;
|
||||
android_sdk: number | null;
|
||||
app_version_code: number | null;
|
||||
source_app_version: string | null;
|
||||
longitude: number | null;
|
||||
latitude: number | null;
|
||||
llm_calls: LlmCall[] | null;
|
||||
raw_payload: Record<string, unknown> | null;
|
||||
}
|
||||
|
||||
export interface AdRevenueImpression {
|
||||
id: number;
|
||||
created_at: string;
|
||||
@@ -303,3 +367,88 @@ export interface PriceReportSummary {
|
||||
rejected: number;
|
||||
total: number;
|
||||
}
|
||||
|
||||
// ===== CPS 分发与对账 =====
|
||||
export interface CpsGroup {
|
||||
id: number;
|
||||
sid: string | null; // 仅美团群有(渠道追踪位)
|
||||
name: string;
|
||||
platforms: string[]; // meituan/taobao/jd 多选
|
||||
member_count: number | null;
|
||||
status: string; // active / archived
|
||||
remark: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface CpsActivity {
|
||||
id: number;
|
||||
platform: string; // meituan / taobao / jd
|
||||
name: string;
|
||||
act_id: string | null; // 美团活动物料 ID
|
||||
product_view_sign: string | null;
|
||||
payload: string | null; // 淘宝整段淘口令 / 京东链接
|
||||
image_url: string | null; // 淘宝落地页主视觉图(绝对 URL)
|
||||
status: string;
|
||||
remark: string | null;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export interface CpsOrder {
|
||||
id: number;
|
||||
order_id: string;
|
||||
sid: string | null;
|
||||
act_id: string | null;
|
||||
pay_price_cents: number | null;
|
||||
commission_cents: number | null;
|
||||
commission_rate: string | null; // "300"=3% "10"=0.1%
|
||||
mt_status: string | null; // 2付款 3完成 4取消 5风控 6结算
|
||||
invalid_reason: string | null;
|
||||
product_name: string | null;
|
||||
pay_time: string | null;
|
||||
}
|
||||
|
||||
export interface CpsReferralLinkItem {
|
||||
activity_id: number;
|
||||
activity_name: string;
|
||||
platform: string;
|
||||
redirect_url: string; // 我们的落地页 /c/{code}(发群用)
|
||||
code: string;
|
||||
}
|
||||
|
||||
export interface CpsReferralLinks {
|
||||
group_name: string;
|
||||
results: CpsReferralLinkItem[];
|
||||
}
|
||||
|
||||
export interface CpsReconcileResult {
|
||||
fetched: number;
|
||||
inserted: number;
|
||||
updated: number;
|
||||
pages: number;
|
||||
}
|
||||
|
||||
export interface CpsGroupStat {
|
||||
group_id: number | null;
|
||||
sid: string | null;
|
||||
name: string;
|
||||
platforms: string[];
|
||||
member_count: number | null;
|
||||
click_pv: number;
|
||||
click_uv: number;
|
||||
copy_pv: number; // 淘宝"复制口令"次数
|
||||
copy_uv: number;
|
||||
// 对账类:淘宝/京东无法对账 → null(前端显示 "-")
|
||||
order_count: number | null;
|
||||
settled_count: number | null;
|
||||
canceled_count: number | null;
|
||||
gmv_cents: number | null;
|
||||
est_commission_cents: number | null;
|
||||
settled_commission_cents: number | null;
|
||||
}
|
||||
|
||||
export interface CpsStats {
|
||||
groups: CpsGroupStat[];
|
||||
total_order_count: number;
|
||||
total_est_commission_cents: number;
|
||||
total_settled_commission_cents: number;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user