feat(admin): rebuild the payment center overview with ECharts analytics

- Overview page: realtime today vs. same time yesterday, 6 KPI cards with
  period-over-period deltas, trend (paid / net / refund curves + order count
  line), category curves, payment method donut, top 10 paid content,
  conversion / close / refund rates, todos and shortcuts
- Date presets, custom range and category filter via
  GET aksrt/v1/admin/pay-overview (manage_options + wp_rest nonce)
- Metrics: paid amount by paid date including later-refunded orders, refunds
  by refund date, net = paid - refund; CNY only, points and recharge excluded,
  foreign-currency orders listed separately; integer cents internally
- Daily records cached per month in transients (today/yesterday live),
  invalidated on order status change and on purge
- ECharts 6.1.0 subset bundled to assets/js/runtime/libs/echarts-pay.min.js
  (npm run build:pay-charts), loaded only on the overview page
- Split payment-attempt methods out of OrderRepositoryCore into
  OrderRepositoryAttempts; remove dead get_stats(), cash_decimal(),
  Order::get_stats() and the old .aksrt-native-links styles

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Fable 5 committed 2026-11-01 00:22:44 +08:00
1 parent d98f8d7975
commit 9805a371d8
19 files changed
+2202 -404

No files matched your search

+119 -92
View File
@@ -21,35 +21,36 @@
}, },
"devDependencies": { "devDependencies": {
"@vitejs/plugin-vue": "^5.1.0", "@vitejs/plugin-vue": "^5.1.0",
"echarts": "^6.1.0",
"esbuild": "^0.25.12", "esbuild": "^0.25.12",
"vite": "^6.0.0" "vite": "^6.0.0"
} }
}, },
"node_modules/@babel/helper-string-parser": { "node_modules/@babel/helper-string-parser": {
"version": "7.27.1", "version": "7.29.11",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.11.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "integrity": "sha512-snZbO7MEDOof+hvNa97EjEY7qD7h2kwvQr5AJC3ZhQudKW5dnej6k3kP9NQV88hJwaS8vDqRLFo9rivLEhle2g==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/helper-validator-identifier": { "node_modules/@babel/helper-validator-identifier": {
"version": "7.28.5", "version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@babel/parser": { "node_modules/@babel/parser": {
"version": "7.29.3", "version": "7.29.12",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.3.tgz", "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.12.tgz",
"integrity": "sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==", "integrity": "sha512-WJ7JBp/Q4+z/ImydtI7jS6NVco5++632ExBemSX+Ysb+G1lCwQ5BVo3SY+jGGOcZXI4ZccbZSUSJLNBW6eEpJg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/types": "^7.29.0" "@babel/types": "^7.29.11"
}, },
"bin": { "bin": {
"parser": "bin/babel-parser.js" "parser": "bin/babel-parser.js"
@@ -59,13 +60,13 @@
} }
}, },
"node_modules/@babel/types": { "node_modules/@babel/types": {
"version": "7.29.0", "version": "7.29.11",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.11.tgz",
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "integrity": "sha512-WCsemW40Cpc6gouC1SrY6PBaZ+oLRlEVY9XBvZBI7il/HyUrb2nJ505wE1MHEFyDZNnrWDEuy4jL50WVHfTTSQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/helper-string-parser": "^7.27.1", "@babel/helper-string-parser": "^7.29.11",
"@babel/helper-validator-identifier": "^7.28.5" "@babel/helper-validator-identifier": "^7.29.7"
}, },
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@@ -676,9 +677,9 @@
} }
}, },
"node_modules/@jridgewell/sourcemap-codec": { "node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/@juggle/resize-observer": { "node_modules/@juggle/resize-observer": {
@@ -1137,53 +1138,53 @@
} }
}, },
"node_modules/@vue/compiler-core": { "node_modules/@vue/compiler-core": {
"version": "3.5.33", "version": "3.5.44",
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.33.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.44.tgz",
"integrity": "sha512-3PZLQwFw4Za3TC8t0FvTy3wI16Kt+pmwcgNZca4Pj9iWL2E72a/gZlpBtAJvEdDMdCxdG/qq0C7PN0bsJuv0Rw==", "integrity": "sha512-k34nCytbK8PQ1gR2mGROYOw6Pnv/mumqIdrZ3x2tbyXZDWm7ViKGLTfdB+DRrWqEQsLgGJvn0WN9jBif5zCTLQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/parser": "^7.29.2", "@babel/parser": "^7.29.9",
"@vue/shared": "3.5.33", "@vue/shared": "3.5.44",
"entities": "^7.0.1", "entities": "^7.0.1",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"source-map-js": "^1.2.1" "source-map-js": "^1.2.2"
} }
}, },
"node_modules/@vue/compiler-dom": { "node_modules/@vue/compiler-dom": {
"version": "3.5.33", "version": "3.5.44",
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.33.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.44.tgz",
"integrity": "sha512-PXq0yrfCLzzL07rbXO4awtXY1Z06LG2eu6Adg3RJFa/j3Cii217XxxLXG22N330gw7GmALCY0Z8RgXEviwgpjA==", "integrity": "sha512-W5AoBFWdw1lQAKiHowkfgFKFab82oSRT6sGsPfw/q1GRjsy/ZJFLHGhJyBRIFN9APLTUduDH9U2JAiPOIyKuYg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/compiler-core": "3.5.33", "@vue/compiler-core": "3.5.44",
"@vue/shared": "3.5.33" "@vue/shared": "3.5.44"
} }
}, },
"node_modules/@vue/compiler-sfc": { "node_modules/@vue/compiler-sfc": {
"version": "3.5.33", "version": "3.5.44",
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.33.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.44.tgz",
"integrity": "sha512-UTUvRO9cY+rROrx/pvN9P5Z7FgA6QGfokUCfhQE4EnmUj3rVnK+CHI0LsEO1pg+I7//iRYMUfcNcCPe7tg0CoA==", "integrity": "sha512-dzHmQ4jXTpdr5FjIQUeykVakN/uzqFbUo9/C5ikBFg3a3PeM6uRxeczx1ZbXBML0/zJstEh3ygYipzR5Z7b2Pw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@babel/parser": "^7.29.2", "@babel/parser": "^7.29.9",
"@vue/compiler-core": "3.5.33", "@vue/compiler-core": "3.5.44",
"@vue/compiler-dom": "3.5.33", "@vue/compiler-dom": "3.5.44",
"@vue/compiler-ssr": "3.5.33", "@vue/compiler-ssr": "3.5.44",
"@vue/shared": "3.5.33", "@vue/shared": "3.5.44",
"estree-walker": "^2.0.2", "estree-walker": "^2.0.2",
"magic-string": "^0.30.21", "magic-string": "^0.30.21",
"postcss": "^8.5.10", "postcss": "^8.5.29",
"source-map-js": "^1.2.1" "source-map-js": "^1.2.2"
} }
}, },
"node_modules/@vue/compiler-ssr": { "node_modules/@vue/compiler-ssr": {
"version": "3.5.33", "version": "3.5.44",
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.33.tgz", "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.44.tgz",
"integrity": "sha512-IErjYdnj1qIupG5xxiVIYiiRvDhGWV4zuh/RCrwfYpuL+HWQzeU6lCk/nF9r7olWMnjKxCAkOctT2qFWFkzb1A==", "integrity": "sha512-xr5gb1JVHlfXJwSlRz1FeP6uxoazdzHuMTVLE7up+4Vrqxk219CvxLPxD9NXBt47yvZW7+p4DXfyp4O1fkByIA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/compiler-dom": "3.5.33", "@vue/compiler-dom": "3.5.44",
"@vue/shared": "3.5.33" "@vue/shared": "3.5.44"
} }
}, },
"node_modules/@vue/devtools-api": { "node_modules/@vue/devtools-api": {
@@ -1193,53 +1194,51 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@vue/reactivity": { "node_modules/@vue/reactivity": {
"version": "3.5.33", "version": "3.5.44",
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.33.tgz", "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.44.tgz",
"integrity": "sha512-p8UfIqyIhb0rYGlSgSBV+lPhF2iUSBcRy7enhTmPqKWadHy9kcOFYF1AejYBP9P+avnd3OBbD49DU4pLWX/94A==", "integrity": "sha512-u+QJdKeBBEADkPdevsL+6E12QlYVfBD8Zcw5qjDQKtamI6l14KyqazJMobhCKelhWN5zpyKUiEDoTyHE93cqWw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/shared": "3.5.33" "@vue/shared": "3.5.44"
} }
}, },
"node_modules/@vue/runtime-core": { "node_modules/@vue/runtime-core": {
"version": "3.5.33", "version": "3.5.44",
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.33.tgz", "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.44.tgz",
"integrity": "sha512-UpFF45RI9//a7rvq7RdOQblb4tup7hHG9QsmIrxkFQLzQ7R8/iNQ5LE15NhLZ1/WcHMU2b47u6P33CPUelHyIQ==", "integrity": "sha512-uEa/ndwjhrrhnOfb2p+sZy6zeoamJOISqaLSJGk/8R6nABT1ZldpzcJoPzKj+zHVomVBPcmQYyC2r4LtvDV6CQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/reactivity": "3.5.33", "@vue/reactivity": "3.5.44",
"@vue/shared": "3.5.33" "@vue/shared": "3.5.44"
} }
}, },
"node_modules/@vue/runtime-dom": { "node_modules/@vue/runtime-dom": {
"version": "3.5.33", "version": "3.5.44",
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.33.tgz", "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.44.tgz",
"integrity": "sha512-IOxMsAOwquhfITgmOgaPYl7/j8gKUxUFoflRc+u4LxyD3+783xne8vNta1PONVCvCV9A0w7hkyEepINDqfO0tw==", "integrity": "sha512-3fc4ARcXVhVBaJ3ujWiNZJARD6pkyRcwG+ZYhMzjKtKUp7MAY0HLwr7r3Smep3Li6ZaWT6ewdmx8XqdaD5oNZg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/reactivity": "3.5.33", "@vue/reactivity": "3.5.44",
"@vue/runtime-core": "3.5.33", "@vue/runtime-core": "3.5.44",
"@vue/shared": "3.5.33", "@vue/shared": "3.5.44",
"csstype": "^3.2.3" "csstype": "^3.2.3"
} }
}, },
"node_modules/@vue/server-renderer": { "node_modules/@vue/server-renderer": {
"version": "3.5.33", "version": "3.5.44",
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.33.tgz", "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.44.tgz",
"integrity": "sha512-0xylq/8/h44lVG0pZFknv1XIdEgymq2E9n59uTWJBG+dIgiT0TMCSsxrN7nO16Z0MU0MPjFcguBbZV8Itk52Hw==", "integrity": "sha512-R4S1UoH4h5Frrauv8Z5XE5TxV9oBiUSgWIC4TyVK1FK+RocRnOLwFA5Xcmo2011RlgJp+AbZCnw6afrhlgRuUQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/compiler-ssr": "3.5.33", "@vue/compiler-ssr": "3.5.44",
"@vue/shared": "3.5.33" "@vue/runtime-dom": "3.5.44",
}, "@vue/shared": "3.5.44"
"peerDependencies": {
"vue": "3.5.33"
} }
}, },
"node_modules/@vue/shared": { "node_modules/@vue/shared": {
"version": "3.5.33", "version": "3.5.44",
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.33.tgz", "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.44.tgz",
"integrity": "sha512-5vR2QIlmaLG77Ygd4pMP6+SGQ5yox9VhtnbDWTy9DzMzdmeLxZ1QqxrywEZ9sa1AVubfIJyaCG3ytyWU81ufcQ==", "integrity": "sha512-H82XLH0DjmSOdbEwO15x3QTo1f5PWEtv237eLpYKc0mqLu+B8hnq16lFWmuBmNurkeiG3wr+oM1FhOll/YVFRg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/async-validator": { "node_modules/async-validator": {
@@ -1310,6 +1309,17 @@
"date-fns": "^3.0.0 || ^4.0.0" "date-fns": "^3.0.0 || ^4.0.0"
} }
}, },
"node_modules/echarts": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-6.1.0.tgz",
"integrity": "sha512-q0yaFPggC9FUdsWH4blavRWFmxdrIodbkoKNAjJudAI6CA9gNPxHtV2RcZNEepZVlk4yvBYkOkbk6HIVpIyHZA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"tslib": "2.3.0",
"zrender": "6.1.0"
}
},
"node_modules/entities": { "node_modules/entities": {
"version": "7.0.1", "version": "7.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
@@ -1472,9 +1482,9 @@
} }
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.12", "version": "3.3.20",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz",
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -1531,9 +1541,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.13", "version": "8.5.32",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.13.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.32.tgz",
"integrity": "sha512-qif0+jGGZoLWdHey3UFHHWP0H7Gbmsk8T5VEqyYFbWqPr1XqvLGBbk/sl8V5exGmcYJklJOhOQq1pV9IcsiFag==", "integrity": "sha512-LJlKCCAUFKOuZxipUdYw1NzIITEsPcgyOG9LkvRJvuKj/bRzbuRY0ARXvL0pWGpsZvn8jku+LMep2eU0seHp8g==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -1550,9 +1560,9 @@
], ],
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.20",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
"source-map-js": "^1.2.1" "source-map-js": "^1.2.2"
}, },
"engines": { "engines": {
"node": "^10 || ^12 || >=14" "node": "^10 || ^12 || >=14"
@@ -1616,9 +1626,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.2.1", "version": "1.2.2",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@@ -1653,6 +1663,13 @@
"integrity": "sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg==", "integrity": "sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"dev": true,
"license": "0BSD"
},
"node_modules/vdirs": { "node_modules/vdirs": {
"version": "0.1.8", "version": "0.1.8",
"resolved": "https://registry.npmjs.org/vdirs/-/vdirs-0.1.8.tgz", "resolved": "https://registry.npmjs.org/vdirs/-/vdirs-0.1.8.tgz",
@@ -1666,9 +1683,9 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "6.4.2", "version": "6.4.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.2.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.5.tgz",
"integrity": "sha512-2N/55r4JDJ4gdrCvGgINMy+HH3iRpNIz8K6SFwVsA+JbQScLiC+clmAxBgwiSPgcG9U15QmvqCGWzMbqda5zGQ==", "integrity": "sha512-4enwjFfi4IwtEh0w+jPzpLVOFQiPHmmzPxcHXQOyEur47o6u0jE573DmGpjA+7irXoMLYRZD5ZaFUEneS3abPg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1753,16 +1770,16 @@
} }
}, },
"node_modules/vue": { "node_modules/vue": {
"version": "3.5.33", "version": "3.5.44",
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.33.tgz", "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.44.tgz",
"integrity": "sha512-1AgChhx5w3ALgT4oK3acm2Es/7jyZhWSVUfs3rOBlGQC0rjEDkS7G4lWlJJGGNQD+BV3reCwbQrOe1mPNwKHBQ==", "integrity": "sha512-dvvmaOCG7lAMkJTPqPwWeGMsPS6DIsFsfZDUgBp9V6mHf9b3+/wyVwYR4Es+WV6uJC0FKRxp81Obcq0ZwUcGFQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@vue/compiler-dom": "3.5.33", "@vue/compiler-dom": "3.5.44",
"@vue/compiler-sfc": "3.5.33", "@vue/compiler-sfc": "3.5.44",
"@vue/runtime-dom": "3.5.33", "@vue/runtime-dom": "3.5.44",
"@vue/server-renderer": "3.5.33", "@vue/server-renderer": "3.5.44",
"@vue/shared": "3.5.33" "@vue/shared": "3.5.44"
}, },
"peerDependencies": { "peerDependencies": {
"typescript": "*" "typescript": "*"
@@ -1849,6 +1866,16 @@
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"license": "MIT" "license": "MIT"
},
"node_modules/zrender": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-6.1.0.tgz",
"integrity": "sha512-oEGMDB6pOP2S6OwRR4PdVv610zrjnA3Bh+JnSG12fYJlBKjtNAoEb5fSUoCOOINlH96I2fU38/A2UpRKs67xYQ==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"tslib": "2.3.0"
}
} }
} }
} }
+1 -1
View File
@@ -1 +1 @@
{"name":"aksrt-admin","version":"1.0.0","private":true,"type":"module","scripts":{"dev":"vite","build":"vite build && node extract-naive-styles.mjs","build:public":"node ../../tools/build-public-assets.mjs","preview":"vite preview"},"dependencies":{"@codemirror/lang-css":"^6.3.1","@codemirror/lang-html":"^6.4.11","@codemirror/lang-javascript":"^6.2.5","@codemirror/theme-one-dark":"^6.1.3","codemirror":"^6.0.2","naive-ui":"^2.40.0","pinia":"^2.2.0","vue":"^3.5.0","vue-router":"^4.4.0","vuedraggable":"^4.1.0"},"devDependencies":{"@vitejs/plugin-vue":"^5.1.0","esbuild":"^0.25.12","vite":"^6.0.0"}} {"name":"aksrt-admin","version":"1.0.0","private":true,"type":"module","scripts":{"dev":"vite","build":"vite build && node extract-naive-styles.mjs","build:public":"node ../../tools/build-public-assets.mjs","build:pay-charts":"node ../../tools/build-pay-charts.mjs","preview":"vite preview"},"dependencies":{"@codemirror/lang-css":"^6.3.1","@codemirror/lang-html":"^6.4.11","@codemirror/lang-javascript":"^6.2.5","@codemirror/theme-one-dark":"^6.1.3","codemirror":"^6.0.2","naive-ui":"^2.40.0","pinia":"^2.2.0","vue":"^3.5.0","vue-router":"^4.4.0","vuedraggable":"^4.1.0"},"devDependencies":{"@vitejs/plugin-vue":"^5.1.0","echarts":"^6.1.0","esbuild":"^0.25.12","vite":"^6.0.0"},"allowScripts":{"esbuild@0.25.12":true,"vue-demi@0.14.10":true}}
-25
View File
@@ -516,31 +516,6 @@ a.aksrt-native-stat:focus { outline: 2px solid color-mix(in srgb, var(--an-tone)
.aksrt-native-callout.is-warning { background: #fff8eb; border-left-color: var(--an-warning); } .aksrt-native-callout.is-warning { background: #fff8eb; border-left-color: var(--an-warning); }
.aksrt-native-callout.is-danger { background: #fdf0f2; border-left-color: var(--an-danger); } .aksrt-native-callout.is-danger { background: #fdf0f2; border-left-color: var(--an-danger); }
/* 快捷入口网格 */
.aksrt-native-links {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 10px;
margin: 0;
padding: 0;
list-style: none;
}
.aksrt-native-links li { margin: 0; }
.aksrt-native-links a {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 14px;
color: var(--an-text);
background: var(--an-bg-soft);
border-radius: var(--an-radius-sm);
text-decoration: none;
transition: background .15s, color .15s;
}
.aksrt-native-links a:hover { color: var(--an-primary); background: var(--an-primary-soft); }
.aksrt-native-links a::after { content: "\f345"; margin-left: auto; color: var(--an-muted); font: normal 14px/1 dashicons; }
.aksrt-native-links .dashicons { color: var(--an-primary); }
/* ---------- 分页条(AdminListPagination / paginate_links) ---------- */ /* ---------- 分页条(AdminListPagination / paginate_links) ---------- */
.aksrt-native .tablenav { height: auto; margin: 0; padding: 12px 20px; } .aksrt-native .tablenav { height: auto; margin: 0; padding: 12px 20px; }
.aksrt-native .aksrt-native-card .tablenav.bottom { border-top: 1px solid var(--an-border-soft); } .aksrt-native .aksrt-native-card .tablenav.bottom { border-top: 1px solid var(--an-border-soft); }
+284
View File
@@ -0,0 +1,284 @@
/* 支付中心概览(aksrt_pay):今日实时 + 筛选条 + 指标卡 + ECharts 图表 + 转化与退款 + 热销排行 + 待办 + 快捷入口。
* 复用 native-ui.css 的 --an-* 令牌,规则限定在本页类名下。图表由 assets/js/admin-pay-overview.js 绘制。 */
.aksrt-po-page [hidden] { display: none !important; }
/* ---------- 今日实时 ---------- */
.aksrt-po-live {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 8px 24px;
margin: 0 0 16px;
padding: 14px 20px;
background: var(--an-card);
border-radius: var(--an-radius);
box-shadow: var(--an-shadow);
}
.aksrt-po-live__main { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 12px; }
.aksrt-po-live__dot {
width: 8px;
height: 8px;
background: var(--an-primary);
border-radius: 50%;
box-shadow: 0 0 0 4px var(--an-primary-soft);
animation: aksrt-po-pulse 2s ease-in-out infinite;
}
@keyframes aksrt-po-pulse { 50% { box-shadow: 0 0 0 7px transparent; } }
@media (prefers-reduced-motion: reduce) { .aksrt-po-live__dot { animation: none; } }
.aksrt-po-live__label { color: var(--an-muted); }
.aksrt-po-live__value { color: var(--an-text); font-size: 22px; font-weight: 600; font-variant-numeric: tabular-nums; }
.aksrt-po-live__meta { display: flex; flex-wrap: wrap; gap: 4px 16px; color: var(--an-muted); font-size: 12px; }
/* ---------- 筛选条 ---------- */
.aksrt-po-toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 10px 16px;
margin: 0 0 16px;
}
.aksrt-po-seg {
display: inline-flex;
flex-wrap: wrap;
gap: 2px;
padding: 3px;
background: var(--an-card);
border-radius: var(--an-radius-sm);
box-shadow: var(--an-shadow);
}
.aksrt-po-seg button {
padding: 5px 12px;
color: var(--an-text-2);
font-size: 12px;
line-height: 1.5;
background: transparent;
border: 0;
border-radius: 6px;
cursor: pointer;
transition: background .15s, color .15s;
}
.aksrt-po-seg button:hover { color: var(--an-primary); }
.aksrt-po-seg button[aria-pressed="true"] { color: var(--an-primary); font-weight: 600; background: var(--an-primary-soft); }
.aksrt-po-seg button:focus-visible { outline: 2px solid var(--an-primary); outline-offset: 1px; }
.aksrt-po-range { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; margin: 0; color: var(--an-muted); }
.aksrt-po-range label { display: inline-flex; margin: 0; }
.aksrt-po-range-label { color: var(--an-muted); font-size: 12px; }
/* ---------- 提示 ---------- */
.aksrt-po-notice {
margin: 0 0 16px;
padding: 10px 14px;
color: var(--an-text-2);
font-size: 12px;
background: #fff8eb;
border-left: 3px solid var(--an-warning);
border-radius: var(--an-radius-sm);
}
.aksrt-po-page .aksrt-po-error { margin: 0 0 16px; }
/* ---------- 指标卡 ---------- */
.aksrt-po-kpis {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 12px;
margin: 0 0 16px;
}
.aksrt-po-kpi {
--po-tone: var(--an-primary);
position: relative;
display: flex;
flex-direction: column;
gap: 6px;
min-width: 0;
padding: 16px 18px 14px;
background: var(--an-card);
border-radius: var(--an-radius);
box-shadow: var(--an-shadow);
overflow: hidden;
}
.aksrt-po-kpi::before { content: ""; position: absolute; top: 0; left: 0; right: 0; height: 3px; background: var(--po-tone); opacity: .85; }
.aksrt-po-kpi--success { --po-tone: var(--an-primary); }
.aksrt-po-kpi--info { --po-tone: var(--an-info); }
.aksrt-po-kpi--warning { --po-tone: var(--an-warning); }
.aksrt-po-kpi--danger { --po-tone: var(--an-danger); }
.aksrt-po-kpi--primary { --po-tone: #8a63d2; }
.aksrt-po-kpi__label { color: var(--an-muted); font-size: 13px; }
.aksrt-po-kpi__value {
color: var(--an-text);
font-size: 24px;
font-weight: 600;
line-height: 1.2;
font-variant-numeric: tabular-nums;
word-break: break-all;
}
.aksrt-po-kpi__foot { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; font-size: 12px; }
.aksrt-po-kpi__meta { color: var(--an-muted); }
.aksrt-po-delta {
display: inline-flex;
align-items: center;
padding: 1px 6px;
font-size: 12px;
font-variant-numeric: tabular-nums;
border-radius: 4px;
}
.aksrt-po-delta.is-up { color: var(--an-primary); background: var(--an-primary-soft); }
.aksrt-po-delta.is-down { color: var(--an-danger); background: #fdf0f2; }
.aksrt-po-delta.is-flat { color: var(--an-muted); background: var(--an-bg-soft); }
/* ---------- 栅格 / 加载态 ---------- */
.aksrt-po-body { transition: opacity .15s; }
.aksrt-po-body.is-loading { opacity: .55; pointer-events: none; }
.aksrt-po-grid {
display: grid;
gap: 16px;
align-items: stretch;
margin: 0 0 16px;
}
.aksrt-po-grid--main { grid-template-columns: minmax(0, 2fr) minmax(300px, 1fr); }
.aksrt-po-grid > .aksrt-native-card { margin: 0; }
.aksrt-po-card { display: flex; flex-direction: column; min-width: 0; }
.aksrt-po-card > .aksrt-native-card__body { flex: 1; }
/* ---------- 图表 ---------- */
.aksrt-po-chart { width: 100%; min-width: 0; }
.aksrt-po-chart--lg { height: 340px; }
.aksrt-po-chart--md { height: 280px; }
/* ---------- 转化与退款 ---------- */
.aksrt-po-funnel { display: flex; flex-direction: column; gap: 16px; margin: 0; padding: 0; list-style: none; }
.aksrt-po-funnel li { display: flex; flex-direction: column; gap: 6px; margin: 0; }
.aksrt-po-funnel__head { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
.aksrt-po-funnel__label { color: var(--an-text-2); }
.aksrt-po-funnel__value { color: var(--an-text); font-size: 18px; font-weight: 600; font-variant-numeric: tabular-nums; }
.aksrt-po-funnel__bar { display: block; height: 6px; background: var(--an-bg-soft); border-radius: 3px; overflow: hidden; }
.aksrt-po-funnel__bar i { display: block; width: 0; height: 100%; background: var(--an-primary); border-radius: 3px; transition: width .3s; }
.aksrt-po-funnel li[data-metric="close_rate"] .aksrt-po-funnel__bar i { background: var(--an-warning); }
.aksrt-po-funnel li[data-metric="refund_rate"] .aksrt-po-funnel__bar i { background: var(--an-danger); }
.aksrt-po-funnel li[data-metric="created_count"] .aksrt-po-funnel__bar { display: none; }
.aksrt-po-funnel__hint { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; color: var(--an-muted); font-size: 12px; }
/* ---------- 热销排行 ---------- */
.aksrt-po-top .column-rank { width: 44px; text-align: center; }
.aksrt-po-top .column-num { width: 110px; text-align: right; }
.aksrt-po-top .column-share { width: 160px; }
.aksrt-po-top td { vertical-align: middle; }
.aksrt-po-top td.column-num { font-variant-numeric: tabular-nums; }
.aksrt-po-top td a { color: var(--an-text); font-weight: 500; text-decoration: none; }
.aksrt-po-top td a:hover { color: var(--an-primary); }
.aksrt-po-rank {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
color: var(--an-muted);
font-size: 12px;
font-weight: 600;
background: var(--an-bg-soft);
border-radius: 6px;
}
.aksrt-po-rank.is-top { color: #fff; background: var(--an-primary); }
.aksrt-po-share { display: flex; align-items: center; gap: 8px; color: var(--an-muted); font-size: 12px; font-variant-numeric: tabular-nums; }
.aksrt-po-share > span { flex: 1; height: 6px; background: var(--an-bg-soft); border-radius: 3px; overflow: hidden; }
.aksrt-po-share > span > i { display: block; height: 100%; background: var(--an-primary); border-radius: 3px; }
.aksrt-po-top__empty { padding: 32px 12px !important; color: var(--an-muted); text-align: center; }
/* ---------- 待办 ---------- */
.aksrt-po-todo { display: flex; flex-direction: column; gap: 6px; margin: 0; padding: 0; list-style: none; }
.aksrt-po-todo li { margin: 0; }
.aksrt-po-todo__item {
--po-tone: var(--an-muted);
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
color: var(--an-text-2);
background: var(--an-bg-soft);
border-radius: var(--an-radius-sm);
text-decoration: none;
transition: background .15s, color .15s;
}
.aksrt-po-todo__item:hover,
.aksrt-po-todo__item:focus { color: var(--an-text); background: color-mix(in srgb, var(--po-tone) 10%, #fff); }
.aksrt-po-todo__item--danger { --po-tone: var(--an-danger); }
.aksrt-po-todo__item--warning { --po-tone: var(--an-warning); }
.aksrt-po-todo__item--info { --po-tone: var(--an-info); }
.aksrt-po-todo__item--neutral { --po-tone: var(--an-muted); }
.aksrt-po-todo__icon.dashicons {
display: inline-flex;
align-items: center;
justify-content: center;
flex: none;
width: 30px;
height: 30px;
color: var(--po-tone);
font-size: 16px;
background: color-mix(in srgb, var(--po-tone) 12%, #fff);
border-radius: 8px;
}
.aksrt-po-todo__icon.dashicons:before { line-height: 30px; }
.aksrt-po-todo__label { flex: 1; min-width: 0; }
.aksrt-po-todo__value { color: var(--an-text); font-size: 15px; font-weight: 600; font-variant-numeric: tabular-nums; }
.aksrt-po-todo__item--danger .aksrt-po-todo__value { color: var(--an-danger); }
.aksrt-po-todo__item--warning .aksrt-po-todo__value { color: #c27c0e; }
.aksrt-po-todo__item--neutral .aksrt-po-todo__value { color: var(--an-muted); }
/* ---------- 快捷入口 ---------- */
.aksrt-po-links {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
gap: 8px;
margin: 0;
padding: 0;
list-style: none;
}
.aksrt-po-links li { margin: 0; }
.aksrt-po-links a {
display: flex;
align-items: center;
gap: 10px;
height: 100%;
padding: 10px 12px;
color: var(--an-text);
border: 1px solid var(--an-border-soft);
border-radius: var(--an-radius-sm);
text-decoration: none;
box-sizing: border-box;
transition: border-color .15s, background .15s;
}
.aksrt-po-links a:hover,
.aksrt-po-links a:focus { color: var(--an-text); background: var(--an-primary-soft); border-color: color-mix(in srgb, var(--an-primary) 30%, transparent); }
.aksrt-po-links__icon.dashicons {
display: inline-flex;
align-items: center;
justify-content: center;
flex: none;
width: 32px;
height: 32px;
color: var(--an-primary);
font-size: 17px;
background: var(--an-primary-soft);
border-radius: 8px;
}
.aksrt-po-links__icon.dashicons:before { line-height: 32px; }
.aksrt-po-links__text { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.aksrt-po-links__text strong { font-size: 13px; font-weight: 600; }
.aksrt-po-links__text span { color: var(--an-muted); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* ---------- 响应式 ---------- */
@media (max-width: 1440px) {
.aksrt-po-kpis { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 1080px) {
.aksrt-po-grid--main { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 782px) {
.aksrt-po-kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; }
.aksrt-po-kpi { padding: 12px 14px; }
.aksrt-po-kpi__value { font-size: 19px; }
.aksrt-po-chart--lg { height: 280px; }
.aksrt-po-chart--md { height: 240px; }
.aksrt-po-top .column-share { display: none; }
}
+407
View File
@@ -0,0 +1,407 @@
/**
* 支付中心概览:区间筛选 + 指标卡 + ECharts 图表(成交趋势 / 分类成交 / 支付方式)+ 转化与退款 + 热销排行。
*
* 首屏报表来自页面内 #aksrt-pay-overview-data(PayOverview 输出),之后切换区间 / 分类请求
* GET aksrt/v1/admin/pay-overview(PayOverviewController)。ECharts 子集由
* assets/js/runtime/libs/echarts-pay.min.js 提供(window.echarts)。仅本页入队。
*/
(function () {
'use strict';
var root = document.querySelector('.aksrt-po-page');
var dataEl = document.getElementById('aksrt-pay-overview-data');
if (!root || !dataEl || !window.echarts) return;
var boot;
try { boot = JSON.parse(dataEl.textContent || '{}'); } catch (e) { return; }
var echarts = window.echarts;
var i18n = boot.i18n || {};
var currency = boot.currency || '';
var COLORS = { paid: '#18a058', net: '#2080f0', refund: '#d03050', count: '#f0a020' };
var PALETTE = ['#18a058', '#2080f0', '#f0a020', '#8a63d2', '#86909c', '#13c2c2', '#eb2f96'];
var TEXT = '#4e5969', MUTED = '#86909c', LINE = '#f0f1f4';
var state = { from: boot.initial.from, to: boot.initial.to, group: boot.initial.group || '', report: boot.initial.report };
var charts = {};
var reqSeq = 0;
/* ---------------- 格式化 ---------------- */
function money(v) {
return currency + Number(v || 0).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 });
}
function int(v) { return Number(v || 0).toLocaleString(); }
function compact(v) {
var n = Math.abs(v);
if (n >= 1e8) return (v / 1e8).toFixed(1).replace(/\.0$/, '') + '亿';
if (n >= 1e4) return (v / 1e4).toFixed(1).replace(/\.0$/, '') + '万';
return String(Math.round(v * 100) / 100);
}
function pct(v) { return v === null || v === undefined ? '—' : Number(v).toFixed(1) + '%'; }
function shortLabel(label) { return label.length === 10 ? label.slice(5) : label; }
/** 环比:基期为 0 时,本期有数为「新增」,否则不可比。 */
function deltaInfo(cur, prev) {
cur = Number(cur || 0); prev = Number(prev || 0);
if (prev <= 0) return cur > 0 ? { cls: 'is-up', text: i18n.new } : { cls: 'is-flat', text: '—' };
var d = (cur - prev) / prev * 100;
var cls = d > 0.05 ? 'is-up' : (d < -0.05 ? 'is-down' : 'is-flat');
return { cls: cls, text: (d > 0.05 ? '↑' : (d < -0.05 ? '↓' : '')) + Math.abs(d).toFixed(1) + '%' };
}
/* ---------------- 日期 ---------------- */
function parse(s) { var p = s.split('-'); return new Date(Date.UTC(+p[0], +p[1] - 1, +p[2])); }
function fmt(d) { return d.toISOString().slice(0, 10); }
function shift(s, days) { var d = parse(s); d.setUTCDate(d.getUTCDate() + days); return fmt(d); }
function spanDays(a, b) { return Math.round((parse(b) - parse(a)) / 864e5) + 1; }
function presetRange(key) {
var today = boot.today;
if (key === 'this_month') return [today.slice(0, 8) + '01', today];
if (key === 'last_month') {
var first = parse(today.slice(0, 8) + '01');
var lastEnd = new Date(first.getTime() - 864e5);
return [fmt(lastEnd).slice(0, 8) + '01', fmt(lastEnd)];
}
var n = parseInt(key.slice(1), 10);
return [shift(today, -(n - 1)), today];
}
/* ---------------- 数据加载 ---------------- */
function setBusy(busy) {
root.classList.toggle('is-loading', busy);
var body = root.querySelector('[data-aksrt-po-body]');
if (body) body.setAttribute('aria-busy', busy ? 'true' : 'false');
}
function showError(msg) {
var box = root.querySelector('[data-aksrt-po-error]');
if (!box) return;
box.hidden = !msg;
box.querySelector('p').textContent = msg || '';
}
function load(from, to, group) {
if (spanDays(from, to) < 1 || spanDays(from, to) > boot.maxDays || to > boot.today) {
showError(i18n.badRange);
return;
}
var seq = ++reqSeq;
var url = boot.rest.url + (boot.rest.url.indexOf('?') === -1 ? '?' : '&')
+ 'from=' + encodeURIComponent(from) + '&to=' + encodeURIComponent(to) + '&group=' + encodeURIComponent(group);
setBusy(true);
fetch(url, { credentials: 'same-origin', headers: { 'X-WP-Nonce': boot.rest.nonce } })
.then(function (res) { return res.json().then(function (json) { return { ok: res.ok, json: json }; }); })
.then(function (r) {
if (seq !== reqSeq) return;
if (!r.ok || !r.json || !r.json.success) throw new Error((r.json && r.json.message) || i18n.loadError);
state = { from: from, to: to, group: group, report: r.json.data };
showError('');
renderAll();
})
.catch(function (err) { if (seq === reqSeq) showError(err && err.message ? err.message : i18n.loadError); })
.then(function () { if (seq === reqSeq) setBusy(false); });
}
/* ---------------- 指标卡 / 转化 / 排行 / 提示 ---------------- */
function renderKpis(rep) {
var t = rep.totals || {}, p = rep.previous || {};
var defs = {
paid_amount: [money, function () { return int(t.paid_count) + ' ' + i18n.unit; }],
net_amount: [money, function () { return i18n.refund + ' ' + money(t.refund_amount); }],
paid_count: [int, function () { return ''; }],
buyers: [int, function () { return ''; }],
avg_amount: [money, function () { return ''; }],
refund_amount: [money, function () { return int(t.refund_count) + ' ' + i18n.unit; }],
};
root.querySelectorAll('[data-kpi]').forEach(function (card) {
var key = card.getAttribute('data-kpi'), def = defs[key];
if (!def) return;
card.querySelector('[data-kpi-value]').textContent = def[0](t[key]);
card.querySelector('[data-kpi-meta]').textContent = def[1]();
var d = deltaInfo(t[key], p[key]);
var el = card.querySelector('[data-kpi-delta]');
// 退款上升是坏事:颜色反转。
var cls = key === 'refund_amount' ? (d.cls === 'is-up' ? 'is-down' : (d.cls === 'is-down' ? 'is-up' : d.cls)) : d.cls;
el.className = 'aksrt-po-delta ' + cls;
el.textContent = i18n.basis + ' ' + d.text;
});
var label = root.querySelector('[data-aksrt-po-range-label]');
if (label && rep.range) label.textContent = rep.range.from + ' ~ ' + rep.range.to + '(' + rep.range.days + ' 天)';
}
function renderFunnel(rep) {
var t = rep.totals || {}, p = rep.previous || {};
root.querySelectorAll('[data-aksrt-po-funnel] [data-metric]').forEach(function (li) {
var key = li.getAttribute('data-metric');
var isCount = key === 'created_count';
li.querySelector('[data-value]').textContent = isCount ? int(t[key]) : pct(t[key]);
var bar = li.querySelector('[data-bar]');
bar.style.width = isCount ? '100%' : Math.max(0, Math.min(100, Number(t[key] || 0))) + '%';
var deltaEl = li.querySelector('[data-delta]');
if (isCount) {
var d = deltaInfo(t[key], p[key]);
deltaEl.className = 'aksrt-po-delta ' + d.cls;
deltaEl.textContent = i18n.basis + ' ' + d.text;
} else if (p[key] !== null && p[key] !== undefined && t[key] !== null && t[key] !== undefined) {
var diff = Number(t[key]) - Number(p[key]);
var good = key === 'conversion' ? diff >= 0 : diff <= 0;
deltaEl.className = 'aksrt-po-delta ' + (Math.abs(diff) < 0.05 ? 'is-flat' : (good ? 'is-up' : 'is-down'));
deltaEl.textContent = i18n.basis + ' ' + (diff > 0 ? '+' : '') + diff.toFixed(1) + 'pt';
} else {
deltaEl.className = 'aksrt-po-delta is-flat';
deltaEl.textContent = '';
}
});
}
function renderTop(rep) {
var body = root.querySelector('[data-aksrt-po-top]');
if (!body) return;
body.textContent = '';
var rows = rep.top || [];
if (!rows.length) {
var tr = body.insertRow(), td = tr.insertCell();
td.colSpan = 5;
td.className = 'aksrt-po-top__empty';
td.textContent = i18n.noTop;
return;
}
var total = rows.reduce(function (s, r) { return s + Number(r.amount || 0); }, 0) || 1;
rows.forEach(function (r, i) {
var tr = body.insertRow();
var rank = tr.insertCell();
rank.className = 'column-rank';
rank.innerHTML = '<span class="aksrt-po-rank' + (i < 3 ? ' is-top' : '') + '"></span>';
rank.firstChild.textContent = String(i + 1);
var title = tr.insertCell();
if (r.url) {
var a = document.createElement('a');
a.href = r.url;
a.textContent = r.title;
title.appendChild(a);
} else {
title.textContent = r.title;
}
var count = tr.insertCell(); count.className = 'column-num'; count.textContent = int(r.count);
var amount = tr.insertCell(); amount.className = 'column-num'; amount.textContent = money(r.amount);
var share = tr.insertCell(); share.className = 'column-share';
var ratio = Number(r.amount || 0) / total * 100;
share.innerHTML = '<span class="aksrt-po-share"><i></i></span><span></span>';
share.querySelector('i').style.width = ratio.toFixed(1) + '%';
share.lastChild.textContent = ratio.toFixed(1) + '%';
});
}
function renderExcluded(rep) {
var box = root.querySelector('[data-aksrt-po-excluded]');
if (!box) return;
var list = rep.excluded || [];
box.hidden = !list.length;
box.textContent = list.length ? i18n.excluded + list.map(function (x) {
return x.currency + ' ' + Number(x.amount).toFixed(2) + '(' + x.count + ' ' + i18n.unit + ')';
}).join(';') : '';
}
/* ---------------- 图表 ---------------- */
function chart(key) {
var host = root.querySelector('[data-aksrt-po-chart="' + key + '"]');
if (!host) return null;
if (!charts[key]) charts[key] = echarts.init(host, null, { renderer: 'svg' });
return charts[key];
}
function baseAxis(labels) {
return {
type: 'category',
boundaryGap: false,
data: labels,
axisLine: { lineStyle: { color: '#e5e6eb' } },
axisTick: { show: false },
axisLabel: { color: MUTED, hideOverlap: true, formatter: shortLabel },
};
}
function valueAxis(formatter, show) {
return {
type: 'value',
show: show !== false,
splitLine: { lineStyle: { color: LINE } },
axisLabel: { color: MUTED, formatter: formatter },
minInterval: 1,
};
}
function tooltip(formatters) {
return {
trigger: 'axis',
confine: true,
axisPointer: { type: 'line', lineStyle: { color: '#c9cdd4', type: 'dashed' } },
backgroundColor: '#fff',
borderColor: '#e5e6eb',
textStyle: { color: TEXT, fontSize: 12 },
extraCssText: 'box-shadow:0 8px 24px rgba(15,23,42,.12);border-radius:8px;',
valueFormatter: formatters,
};
}
function area(color) {
return {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: color + '38' },
{ offset: 1, color: color + '00' },
]),
};
}
function zoom(n) {
return n > 45 ? [{ type: 'inside', start: 0, end: 100 }, { type: 'slider', height: 18, bottom: 6, borderColor: 'transparent', fillerColor: 'rgba(24,160,88,.12)', handleSize: '80%', showDetail: false }] : [];
}
function emptyGraphic(isEmpty) {
return isEmpty ? [{ type: 'text', left: 'center', top: 'middle', silent: true, style: { text: i18n.empty, fill: MUTED, fontSize: 13 } }] : [];
}
function renderTrend(rep) {
var c = chart('trend');
if (!c) return;
var s = rep.series || { labels: [], paid: [], net: [], refund: [], count: [] };
var n = s.labels.length;
var isEmpty = !s.paid.some(function (v) { return v > 0; });
var smoothLine = function (name, data, color, extra) {
return Object.assign({ name: name, type: 'line', data: data, smooth: 0.35, showSymbol: false, symbolSize: 6, color: color, lineStyle: { width: 2.25 }, emphasis: { focus: 'series' } }, extra || {});
};
c.setOption({
aria: { enabled: true },
color: [COLORS.paid, COLORS.net, COLORS.refund, COLORS.count],
grid: { left: 8, right: 8, top: 40, bottom: n > 45 ? 36 : 8, containLabel: true },
legend: { top: 0, left: 0, icon: 'roundRect', itemWidth: 12, itemHeight: 4, textStyle: { color: TEXT }, selected: { [i18n.refund]: true } },
tooltip: tooltip(function (v) { return typeof v === 'number' && !Number.isInteger(v) ? money(v) : v; }),
xAxis: baseAxis(s.labels),
yAxis: [valueAxis(compact), valueAxis(function (v) { return v; })],
dataZoom: zoom(n),
graphic: emptyGraphic(isEmpty),
series: [
smoothLine(i18n.paid, s.paid, COLORS.paid, { areaStyle: area(COLORS.paid), tooltip: { valueFormatter: money } }),
smoothLine(i18n.net, s.net, COLORS.net, { tooltip: { valueFormatter: money } }),
smoothLine(i18n.refund, s.refund, COLORS.refund, { lineStyle: { width: 1.5, type: 'dashed' }, tooltip: { valueFormatter: money } }),
{
name: i18n.count, type: 'line', yAxisIndex: 1, data: s.count, smooth: false,
showSymbol: n <= 31, symbol: 'circle', symbolSize: 5, color: COLORS.count,
lineStyle: { width: 1.5 }, itemStyle: { borderColor: '#fff', borderWidth: 1.5 },
tooltip: { valueFormatter: function (v) { return int(v) + ' ' + i18n.unit; } },
},
],
}, true);
}
function renderGroups(rep) {
var c = chart('groups');
if (!c) return;
var s = rep.series || { labels: [], groups: [] };
var groups = s.groups || [];
var n = s.labels.length;
c.setOption({
aria: { enabled: true },
color: PALETTE,
grid: { left: 8, right: 8, top: 40, bottom: n > 45 ? 36 : 8, containLabel: true },
legend: { top: 0, left: 0, icon: 'roundRect', itemWidth: 12, itemHeight: 4, textStyle: { color: TEXT } },
tooltip: tooltip(money),
xAxis: baseAxis(s.labels),
yAxis: valueAxis(compact),
dataZoom: zoom(n),
graphic: emptyGraphic(!groups.length),
series: groups.map(function (g) {
return { name: g.name, type: 'line', data: g.data, smooth: 0.35, showSymbol: false, lineStyle: { width: 2 }, emphasis: { focus: 'series' } };
}),
}, true);
}
function renderMethods(rep) {
var c = chart('methods');
if (!c) return;
var list = rep.methods || [];
var total = list.reduce(function (s, m) { return s + Number(m.amount || 0); }, 0);
c.setOption({
aria: { enabled: true },
color: PALETTE,
tooltip: {
trigger: 'item', confine: true, backgroundColor: '#fff', borderColor: '#e5e6eb', textStyle: { color: TEXT, fontSize: 12 },
formatter: function (p) { return p.marker + p.name + '<br>' + money(p.value) + '(' + p.percent + '%,' + int(p.data.count) + ' ' + i18n.unit + ')'; },
},
legend: { bottom: 0, left: 'center', icon: 'circle', itemWidth: 8, itemHeight: 8, textStyle: { color: TEXT } },
graphic: list.length ? [{
type: 'group', left: 'center', top: '36%', silent: true,
children: [
{ type: 'text', style: { text: i18n.total, fill: MUTED, fontSize: 12, align: 'center' }, left: 'center', top: 0 },
{ type: 'text', style: { text: money(total), fill: '#1f2329', fontSize: 16, fontWeight: 600, align: 'center' }, left: 'center', top: 18 },
],
}] : emptyGraphic(true),
series: [{
type: 'pie', radius: ['52%', '72%'], center: ['50%', '44%'], avoidLabelOverlap: true,
itemStyle: { borderColor: '#fff', borderWidth: 2, borderRadius: 4 },
label: { show: false }, labelLine: { show: false },
emphasis: { scale: true, scaleSize: 4 },
data: list.map(function (m) { return { name: m.name, value: m.amount, count: m.count }; }),
}],
}, true);
}
function renderAll() {
var rep = state.report || {};
renderKpis(rep);
renderFunnel(rep);
renderTop(rep);
renderExcluded(rep);
renderTrend(rep);
renderGroups(rep);
renderMethods(rep);
}
/* ---------------- 交互 ---------------- */
var presets = root.querySelector('[data-aksrt-po-presets]');
var form = root.querySelector('[data-aksrt-po-range]');
function syncForm() {
if (!form) return;
form.elements.from.value = state.from;
form.elements.to.value = state.to;
form.elements.group.value = state.group;
}
function pressPreset(btn) {
if (!presets) return;
presets.querySelectorAll('button').forEach(function (b) { b.setAttribute('aria-pressed', b === btn ? 'true' : 'false'); });
}
if (presets) {
presets.addEventListener('click', function (e) {
var btn = e.target.closest('button[data-preset]');
if (!btn) return;
var r = presetRange(btn.getAttribute('data-preset'));
pressPreset(btn);
state.from = r[0]; state.to = r[1];
syncForm();
load(r[0], r[1], form ? form.elements.group.value : state.group);
});
}
if (form) {
form.addEventListener('submit', function (e) {
e.preventDefault();
var from = form.elements.from.value, to = form.elements.to.value;
if (!from || !to || from > to) { showError(i18n.badRange); return; }
pressPreset(null);
load(from, to, form.elements.group.value);
});
form.elements.group.addEventListener('change', function () { load(state.from, state.to, form.elements.group.value); });
}
var resizeTimer;
window.addEventListener('resize', function () {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(function () { Object.keys(charts).forEach(function (k) { charts[k].resize(); }); }, 120);
});
syncForm();
renderAll();
})();
+42
View File
@@ -0,0 +1,42 @@
(()=>{var lM=Object.defineProperty;var er=(t,e)=>{for(var r in e)lM(t,r,{get:e[r],enumerable:!0})};var ay={};er(ay,{Axis:()=>Kp,ChartView:()=>ft,ComponentModel:()=>Se,ComponentView:()=>et,List:()=>Ss,Model:()=>Ce,PRIORITY:()=>vg,SeriesModel:()=>Tt,color:()=>of,connect:()=>UR,dataTool:()=>ZR,dependencies:()=>SR,disConnect:()=>FR,disconnect:()=>uw,dispose:()=>VR,env:()=>oe,extendChartView:()=>Sk,extendComponentModel:()=>yk,extendComponentView:()=>_k,extendSeriesModel:()=>xk,format:()=>Jg,getCoordinateSystemDimensions:()=>HR,getInstanceByDom:()=>gg,getInstanceById:()=>GR,getMap:()=>XR,graphic:()=>Qg,helper:()=>Hg,init:()=>zR,innerDrawElementOnCanvas:()=>lp,matrix:()=>wi,number:()=>Kg,parseGeoJSON:()=>Zp,parseGeoJson:()=>Zp,registerAction:()=>mi,registerCoordinateSystem:()=>Sg,registerCustomSeries:()=>YR,registerLayout:()=>bg,registerLoading:()=>xp,registerLocale:()=>dc,registerMap:()=>wg,registerPostInit:()=>_g,registerPostUpdate:()=>xg,registerPreprocessor:()=>yp,registerProcessor:()=>_p,registerTheme:()=>yg,registerTransform:()=>Cg,registerUpdateLifecycle:()=>eu,registerVisual:()=>Ui,setCanvasCreator:()=>WR,setPlatformAPI:()=>Vu,throttle:()=>Xa,time:()=>jg,use:()=>yt,util:()=>$g,vector:()=>gr,version:()=>xR,zrUtil:()=>pe,zrender:()=>Ef});/*! *****************************************************************************
Copyright (c) Microsoft Corporation.
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
PERFORMANCE OF THIS SOFTWARE.
***************************************************************************** */var Ph=function(t,e){return Ph=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(r,i){r.__proto__=i}||function(r,i){for(var n in i)Object.prototype.hasOwnProperty.call(i,n)&&(r[n]=i[n])},Ph(t,e)};function L(t,e){if(typeof e!="function"&&e!==null)throw new TypeError("Class extends value "+String(e)+" is not a constructor or null");Ph(t,e);function r(){this.constructor=t}t.prototype=e===null?Object.create(e):(r.prototype=e.prototype,new r)}var Ef={};er(Ef,{dispose:()=>ZI,disposeAll:()=>qI,getElementSSRData:()=>Vv,getInstance:()=>KI,init:()=>Df,registerPainter:()=>Fv,registerSSRDataGetter:()=>Gv,version:()=>jI});var uM=(function(){function t(){this.firefox=!1,this.ie=!1,this.edge=!1,this.newEdge=!1,this.weChat=!1}return t})(),fM=(function(){function t(){this.browser=new uM,this.node=!1,this.wxa=!1,this.worker=!1,this.svgSupported=!1,this.touchEventsSupported=!1,this.pointerEventsSupported=!1,this.domSupported=!1,this.transformSupported=!1,this.transform3dSupported=!1,this.hasGlobalWindow=typeof window!="undefined"}return t})(),Xi=new fM;typeof wx=="object"&&typeof wx.getSystemInfoSync=="function"?(Xi.wxa=!0,Xi.touchEventsSupported=!0):typeof document=="undefined"&&typeof self!="undefined"?Xi.worker=!0:!Xi.hasGlobalWindow||"Deno"in window||typeof navigator!="undefined"&&typeof navigator.userAgent=="string"&&navigator.userAgent.indexOf("Node.js")>-1?(Xi.node=!0,Xi.svgSupported=!0):cM(navigator.userAgent,Xi);function cM(t,e){var r=e.browser,i=t.match(/Firefox\/([\d.]+)/),n=t.match(/MSIE\s([\d.]+)/)||t.match(/Trident\/.+?rv:(([\d.]+))/),a=t.match(/Edge?\/([\d.]+)/),o=/micromessenger/i.test(t);i&&(r.firefox=!0,r.version=i[1]),n&&(r.ie=!0,r.version=n[1]),a&&(r.edge=!0,r.version=a[1],r.newEdge=+a[1].split(".")[0]>18),o&&(r.weChat=!0),e.svgSupported=typeof SVGRect!="undefined",e.touchEventsSupported="ontouchstart"in window&&!r.ie&&!r.edge,e.pointerEventsSupported="onpointerdown"in window&&(r.edge||r.ie&&+r.version>=11);var s=e.domSupported=typeof document!="undefined";if(s){var l=document.documentElement.style;e.transform3dSupported=(r.ie&&"transition"in l||r.edge||"WebKitCSSMatrix"in window&&"m11"in new WebKitCSSMatrix||"MozPerspective"in l)&&!("OTransition"in l),e.transformSupported=e.transform3dSupported||r.ie&&+r.version>=9}}var oe=Xi;var pe={};er(pe,{EPSILON:()=>MM,HashMap:()=>F0,RADIAN_TO_DEGREE:()=>Hn,assert:()=>ze,assignProps:()=>Uh,bind:()=>J,clone:()=>$,concatArray:()=>Vh,createCanvas:()=>bM,createHashMap:()=>G,createObject:()=>bi,curry:()=>be,defaults:()=>re,disableUserSelect:()=>EM,each:()=>T,eqNaN:()=>Si,extend:()=>P,filter:()=>Ae,find:()=>Fh,guid:()=>Vs,hasOwn:()=>Oe,indexOf:()=>ie,inherits:()=>Hs,isArray:()=>k,isArrayLike:()=>je,isBuiltInObject:()=>kh,isDom:()=>zn,isFunction:()=>X,isGradientObject:()=>Un,isImagePatternObject:()=>wM,isNumber:()=>se,isObject:()=>Y,isPrimitive:()=>po,isRegExp:()=>CM,isString:()=>U,isStringSafe:()=>vo,isTypedArray:()=>Ve,keys:()=>ne,logError:()=>Gs,map:()=>z,merge:()=>he,mergeAll:()=>SM,mixin:()=>ct,noop:()=>Me,normalizeCssArray:()=>Vn,reduce:()=>Ct,retrieve:()=>Fn,retrieve2:()=>W,retrieve3:()=>Gt,setAsPrimitive:()=>Gn,slice:()=>Ys,trim:()=>pt});var Fu=12,Nh="sans-serif",Vt=Fu+"px "+Nh,pM=20,hM=100,vM="007LLmW'55;N0500LLLLLLLLLL00NNNLzWW\\\\WQb\\0FWLg\\bWb\\WQ\\WrWWQ000CL5LLFLL0LL**F*gLLLL5F0LF\\FFF5.5N";function dM(t){var e={};if(typeof JSON=="undefined")return e;for(var r=0;r<t.length;r++){var i=String.fromCharCode(r+32),n=(t.charCodeAt(r)-pM)/hM;e[i]=n}return e}var mM=dM(vM),at={createCanvas:function(){return typeof document!="undefined"&&document.createElement("canvas")},measureText:(function(){var t,e;return function(r,i){if(!t){var n=at.createCanvas();t=n&&n.getContext("2d")}if(t)return e!==i&&(e=t.font=i||Vt),t.measureText(r);r=r||"",i=i||Vt;var a=/((?:\d+)?\.?\d*)px/.exec(i),o=a&&+a[1]||Fu,s=0;if(i.indexOf("mono")>=0)s=o*r.length;else for(var l=0;l<r.length;l++){var u=mM[r[l]];s+=u==null?o:u*o}return{width:s}}})(),loadImage:function(t,e,r){var i=new Image;return i.onload=e,i.onerror=r,i.src=t,i},getTime:function(){return Date.now?Date.now():+new Date}};function Vu(t){for(var e in at)at.hasOwnProperty(e)&&t[e]&&(at[e]=t[e])}var k0=Ct(["Function","RegExp","Date","Error","CanvasGradient","CanvasPattern","Image","Canvas"],function(t,e){return t["[object "+e+"]"]=!0,t},{}),B0=Ct(["Int8","Uint8","Uint8Clamped","Int16","Uint16","Int32","Uint32","Float32","Float64"],function(t,e){return t["[object "+e+"Array]"]=!0,t},{}),ho=Object.prototype.toString,Hu=Array.prototype,gM=Hu.forEach,yM=Hu.filter,Bh=Hu.slice,_M=Hu.map,R0=function(){}.constructor,Gu=R0?R0.prototype:null,zh="__proto__",Rh=2311,xM=Math.pow(2,53)-1;function Vs(){return Rh>=xM&&(Rh=0),Rh++}function Gs(){for(var t=[],e=0;e<arguments.length;e++)t[e]=arguments[e];typeof console!="undefined"&&console.error.apply(console,t)}function $(t){if(t==null||typeof t!="object")return t;var e=t,r=ho.call(t);if(r==="[object Array]"){if(Line truncated
`),a=n.length;if(a===1)return q_(n[0],e,r,i);for(var o=new ae(0,0,0,0),s=0;s<n.length;s++){var l=q_(n[s],e,r,i);s===0?o.copy(l):o.union(l)}return o}function tn(t,e,r,i){return r==="right"?i?t+=e:t-=e:r==="center"&&(i?t+=e/2:t-=e/2),t}function Di(t,e,r,i){return r==="middle"?i?t+=e/2:t-=e/2:r==="bottom"&&(i?t+=e:t-=e),t}function na(t){return Ur(t).stWideCharWidth}function zr(t,e){return typeof t=="string"?t.lastIndexOf("%")>=0?parseFloat(t)/100*e:parseFloat(t):t}function aa(t,e,r){var i=e.position||"inside",n=e.distance!=null?e.distance:5,a=r.height,o=r.width,s=a/2,l=r.x,u=r.y,f="left",c="top";if(i instanceof Array)l+=zr(i[0],r.width),u+=zr(i[1],r.height),f=null,c=null;else switch(i){case"left":l-=n,u+=s,f="right",c="middle";break;case"right":l+=n+o,u+=s,c="middle";break;case"top":l+=o/2,u-=n,f="center",c="bottom";break;case"bottom":l+=o/2,u+=a+n,f="center";break;case"inside":l+=o/2,u+=s,f="center",c="middle";break;case"insideLeft":l+=n,u+=s,c="middle";break;case"insideRight":l+=o-n,u+=s,f="right",c="middle";break;case"insideTop":l+=o/2,u+=n,f="center";break;case"insideBottom":l+=o/2,u+=a-n,f="center",c="bottom";break;case"insideTopLeft":l+=n,u+=n;break;case"insideTopRight":l+=o-n,u+=n,f="right";break;case"insideBottomLeft":l+=n,u+=a-n,c="bottom";break;case"insideBottomRight":l+=o-n,u+=a-n,f="right",c="bottom";break}return t=t||{},t.x=l,t.y=u,t.align=f,t.verticalAlign=c,t}var Nv="__zr_normal__",Rv=Eo.concat(["ignore"]),zI=Ct(Eo,function(t,e){return t[e]=!0,t},{ignore:!1}),Io={},UI=new ae(0,0,0,0),Tf=[],wf=0,Oo=1,zv=(function(){function t(e){this.id=Vs(),this.animators=[],this.currentStates=[],this.states={},this._init(e)}return t.prototype._init=function(e){this.attr(e)},t.prototype.drift=function(e,r,i){switch(this.draggable){case"horizontal":r=0;break;case"vertical":e=0;break}var n=this.transform;n||(n=this.transform=[1,0,0,1,0,0]),n[4]+=e,n[5]+=r,this.decomposeTransform(),this.markRedraw()},t.prototype.beforeUpdate=function(){},t.prototype.afterUpdate=function(){},t.prototype.update=function(){this.updateTransform(),this.__dirty&&this.updateInnerText()},t.prototype.updateInnerText=function(e){var r=this._textContent;if(r&&(!r.ignore||e)){this.textConfig||(this.textConfig={});var i=this.textConfig,n=i.local,a=r.innerTransformable,o=void 0,s=void 0,l=!1;a.parent=n?this:null;var u=!1;a.copyTransform(r);var f=i.position!=null,c=i.autoOverflowArea,p=void 0;if((c||f)&&(p=UI,i.layoutRect?p.copy(i.layoutRect):p.copy(this.getBoundingRect()),n||p.applyTransform(this.transform)),f){this.calculateTextPosition?this.calculateTextPosition(Io,i,p):aa(Io,i,p),a.x=Io.x,a.y=Io.y,o=Io.align,s=Io.verticalAlign;var h=i.origin;if(h&&i.rotation!=null){var v=void 0,d=void 0;h==="center"?(v=p.width*.5,d=p.height*.5):(v=zr(h[0],p.width),d=zr(h[1],p.height)),u=!0,a.originX=-a.x+v+(n?0:p.x),a.originY=-a.y+d+(n?0:p.y)}}i.rotation!=null&&(a.rotation=i.rotation);var m=i.offset;m&&(a.x+=m[0],a.y+=m[1],u||(a.originX=-m[0],a.originY=-m[1]));var g=this._innerTextDefaultStyle||(this._innerTextDefaultStyle={});if(c){var y=g.overflowRect=g.overflowRect||new ae(0,0,0,0);a.getLocalTransform(Tf),tr(Tf,Tf),ae.copy(y,p),y.applyTransform(Tf)}else g.overflowRect=null;var _=i.inside==null?typeof i.position=="string"&&i.position.indexOf("inside")>=0:i.inside,x=void 0,S=void 0,b=void 0;_&&this.canBeInsideText()?(x=i.insideFill,S=i.insideStroke,(x==null||x==="auto")&&(x=this.getInsideTextFill()),(S==null||S==="auto")&&(S=this.getInsideTextStroke(x),b=!0)):(x=i.outsideFill,S=i.outsideStroke,(x==null||x==="auto")&&(x=this.getOutsideFill()),(S==null||S==="auto")&&(S=this.getOutsideStroke(x),b=!0)),x=x||"#000",(x!==g.fill||S!==g.stroke||b!==g.autoStroke||o!==g.align||s!==g.verticalAlign)&&(l=!0,g.fill=x,g.stroke=S,g.autoStroke=b,g.align=o,g.verticalAlign=s,r.setDefaultTextStyle(g)),r.__dirty|=At,l&&r.dirtyStyle(!0)}},t.prototype.canBeInsideText=function(){return!0},t.prototype.getInsideTextFill=function(){return"#fff"},t.prototype.getInsideTextStroke=function(e){return"#000"},t.prototype.getOutsideFill=function(){return this.__zr&&this.__zr.isDarkMode()?ul:ll},t.prototype.getOutsideStroke=function(e){var r=this.__zr&&this.__zr.getBackgroundColor(),i=typeof r=="string"&&ht(r);i||(i=[255,255,255,1]);for(var n=i[3],a=this.__zr.isDarkMode(),o=0;o<3;o++)i[o]=i[o]*n+(a?0:255)*(1-n);return i[3]=1,ei(i,"rgba")},t.prototype.traverse=function(e,r){},t.prototype.attrKV=function(e,r){e==="textConfig"?this.setTextConfig(r):e==="textContent"?this.setTextContent(r):e==="clipPath"?this.setClipPath(r):e==="extra"?(this.extra=this.extra||{},P(this.extra,r)):this[e]=r},t.prototype.hide=function(){this.ignore=!0,this.markRedraw()},t.prototype.show=function(){this.ignore=!1,this.markRedraw()},t.prototype.attr=function(e,r){if(typeof e=="string")this.attrKV(e,r);else if(Y(e))for(var i=e,n=ne(i),a=0;a<n.length;a++){var o=n[a];this.attrKV(o,e[o])}return this.markRedraw(),this},t.prototype.saveCurrentToNormalState=function(e){this._innerSaveToNormal(e);for(var r=this._normalState,i Line truncated
* ZRender, a high performance 2d drawing library.
*
* Copyright (c) 2013, Baidu Inc.
* All rights reserved.
*
* LICENSE
* https://github.com/ecomfe/zrender/blob/master/LICENSE
*/var Af={},oa={};function YI(t){delete oa[t]}function WI(t){if(!t)return!1;if(typeof t=="string")return ea(t,1)<sl;if(t.colorStops){for(var e=t.colorStops,r=0,i=e.length,n=0;n<i;n++)r+=ea(e[n].color,1);return r/=i,r<sl}return!1}var XI=(function(){function t(e,r,i){var n=this;this._sleepAfterStill=10,this._stillFrameAccum=0,this._needsRefresh=!0,this._needsRefreshHover=!1,this._darkMode=!1,i=i||{},this.dom=r,this.id=e;var a=new u_,o=i.renderer||"canvas";Af[o]||(o=ne(Af)[0]),i.useDirtyRect=i.useDirtyRect==null?!1:i.useDirtyRect;var s=new Af[o](r,a,i,e),l=i.ssr||s.ssrOnly;this.storage=a,this.painter=s;var u=!oe.node&&!oe.worker&&!l?new V_(s.getViewportRoot(),s.root):null,f=i.useCoarsePointer,c=f==null||f==="auto"?oe.touchEventsSupported:!!f,p=44,h;c&&(h=W(i.pointerSize,p)),this.handler=new i_(a,s,u,s.root,h),this.animation=new k_({stage:{update:l?null:function(){return n._flush(!1)}}}),l||this.animation.start()}return t.prototype.add=function(e){this._disposed||!e||(this.storage.addRoot(e),e.addSelfToZr(this),this.refresh())},t.prototype.remove=function(e){this._disposed||!e||(this.storage.delRoot(e),e.removeSelfFromZr(this),this.refresh())},t.prototype.configLayer=function(e,r){this._disposed||(this.painter.configLayer&&this.painter.configLayer(e,r),this.refresh())},t.prototype.setBackgroundColor=function(e){this._disposed||(this.painter.setBackgroundColor&&this.painter.setBackgroundColor(e),this.refresh(),this._backgroundColor=e,this._darkMode=WI(e))},t.prototype.getBackgroundColor=function(){return this._backgroundColor},t.prototype.setDarkMode=function(e){this._darkMode=e},t.prototype.isDarkMode=function(){return this._darkMode},t.prototype.refreshImmediately=function(e){this._disposed||this._refresh({animUpdate:!e,refresh:!0,refreshHover:!1})},t.prototype._refresh=function(e){e.animUpdate&&this.animation.update(!0),this._needsRefresh=this._needsRefreshHover=!1,this.painter.refresh({refresh:e.refresh,refreshHover:e.refreshHover}),this._needsRefresh=this._needsRefreshHover=!1},t.prototype.refresh=function(){this._disposed||(this._needsRefresh=!0,this.animation.start())},t.prototype.flush=function(){this._disposed||this._flush(!0)},t.prototype._flush=function(e){var r,i=ta(),n=this._needsRefresh,a=this._needsRefreshHover;(n||a)&&(r=!0,this._refresh({animUpdate:e,refresh:n,refreshHover:a}));var o=ta();r?(this._stillFrameAccum=0,this.trigger("rendered",{elapsedTime:o-i})):this._sleepAfterStill>0&&(this._stillFrameAccum++,this._stillFrameAccum>this._sleepAfterStill&&this.animation.stop())},t.prototype.setSleepAfterStill=function(e){this._sleepAfterStill=e},t.prototype.wakeUp=function(){this._disposed||(this.animation.start(),this._stillFrameAccum=0)},t.prototype.refreshHover=function(){this._needsRefreshHover=!0},t.prototype.refreshHoverImmediately=function(){this._disposed||this._refresh({animUpdate:!1,refresh:!1,refreshHover:!0})},t.prototype.resize=function(e){this._disposed||(e=e||{},this.painter.resize(e.width,e.height),this.handler.resize())},t.prototype.clearAnimation=function(){this._disposed||this.animation.clear()},t.prototype.getWidth=function(){if(!this._disposed)return this.painter.getWidth()},t.prototype.getHeight=function(){if(!this._disposed)return this.painter.getHeight()},t.prototype.setCursorStyle=function(e){this._disposed||this.handler.setCursorStyle(e)},t.prototype.findHover=function(e,r){if(!this._disposed)return this.handler.findHover(e,r)},t.prototype.on=function(e,r,i){return this._disposed||this.handler.on(e,r,i),this},t.prototype.off=function(e,r){this._disposed||this.handler.off(e,r)},t.prototype.trigger=function(e,r){this._disposed||this.handler.trigger(e,r)},t.prototype.clear=function(){if(!this._disposed){for(var e=this.storage.getRoots(),r=0;r<e.length;r++)e[r]instanceof xe&&e[r].removeSelfFromZr(this);this.storage.delAllRoots(),this.painter.clear()}},t.prototype.dispose=function(){this._disposed||(this.animation.stop(),this.clear(),this.storage.dispose(),this.painter.dispose(),this.handler.dispose(),this.animation=this.storage=this.painter=this.handler=null,this._disposed=!0,YI(this.id))},t})();function Df(t,e){var r=new XI(Vs(),t,e);return oa[r.id]=r,r}function ZI(t){t.dispose()}function qI(){for(var t in oa)oa.hasOwnProperty(t)&&oa[t].dispose();oa={}}function KI(t){return oa[t]}function Fv(t,e){Af[t]=e}var Uv;function Vv(t){if(typeof Uv=="function")return Uv(t)}function Gv(t){Uv=t}var jI="6.1.0";var tx=1e-4,Mf=20;function QI(t){return t.replace(/^\s+|\s+$/g,"")}var Ne=Math.min,ve=Math.max,Ie=Math.abs,Yt=Math.round,Wt=Math.floor,ri=Math.ceil,nn=Math.pow,rn=Math.log,fl=Math.LN10,JI=Math.PI,$I=Math.random;function Le(t,e,r,i){var n=e[0],a=e[1],o=r[0],s=r[1],l=a-n,u=s-o;if(l===0)return u===0?o:(o+s)/2;if(i)if(l>0){if(t<=n)return o;if(t>=a)return s}else{if(t>=n)return o;if(t<=a)return s}else{if(t===n)return o;if(t===a)return s}return(t-n)/l*u+o}var we=eO;function eO(t,e,r){switch(t){case"center":case"middle":t="50%";break;case"left":case"top":t="0%";break;case"right":case"bottomLine truncated
`);a=Bx(r,i,n,a);for(var s=!1,l={},u=0,f=o.length;u<f;u++)zx(l,o[u],a),o[u]=l.textLine,s=s||l.isTruncated;t.text=o.join(`
`),t.isTruncated=s}function Bx(t,e,r,i){i=i||{};var n=P({},i);r=W(r,"..."),n.maxIterations=W(i.maxIterations,2);var a=n.minChar=W(i.minChar,0),o=n.fontMeasureInfo=Ur(e),s=o.asciiCharWidth;n.placeholder=W(i.placeholder,"");for(var l=t=Math.max(0,t-1),u=0;u<a&&l>=s;u++)l-=s;var f=Fr(o,r);return f>l&&(r="",f=0),l=t-f,n.ellipsis=r,n.ellipsisWidth=f,n.contentWidth=l,n.containerWidth=t,n}function zx(t,e,r){var i=r.containerWidth,n=r.contentWidth,a=r.fontMeasureInfo;if(!i){t.textLine="",t.isTruncated=!1;return}var o=Fr(a,e);if(o<=i){t.textLine=e,t.isTruncated=!1;return}for(var s=0;;s++){if(o<=n||s>=r.maxIterations){e+=r.ellipsis;break}var l=s===0?CO(e,n,a):o>0?Math.floor(e.length*n/o):0;e=e.substr(0,l),o=Fr(a,e)}e===""&&(e=r.placeholder),t.textLine=e,t.isTruncated=!0}function CO(t,e,r){for(var i=0,n=0,a=t.length;n<a&&i<e;n++)i+=Pv(r,t.charCodeAt(n));return n}function Ux(t,e,r,i){var n=ad(t),a=e.overflow,o=e.padding,s=o?o[1]+o[3]:0,l=o?o[0]+o[2]:0,u=e.font,f=a==="truncate",c=na(u),p=W(e.lineHeight,c),h=e.lineOverflow==="truncate",v=!1,d=e.width;d==null&&r!=null&&(d=r-s);var m=e.height;m==null&&i!=null&&(m=i-l);var g;d!=null&&(a==="break"||a==="breakAll")?g=n?Vx(n,e.font,d,a==="breakAll",0).lines:[]:g=n?n.split(`
`):[];var y=g.length*p;if(m==null&&(m=y),y>m&&h){var _=Math.floor(m/p);v=v||g.length>_,g=g.slice(0,_),y=g.length*p}if(n&&f&&d!=null)for(var x=Bx(d,u,e.ellipsis,{minChar:e.truncateMinChar,placeholder:e.placeholder}),S={},b=0;b<g.length;b++)zx(S,g[b],x),g[b]=S.textLine,v=v||S.isTruncated;for(var w=m,A=0,C=Ur(u),b=0;b<g.length;b++)A=Math.max(Fr(C,g[b]),A);d==null&&(d=A);var D=d;return w+=l,D+=s,{lines:g,height:m,outerWidth:D,outerHeight:w,lineHeight:p,calculatedLineHeight:c,contentWidth:A,contentHeight:y,width:d,isTruncated:v}}var AO=(function(){function t(){}return t})(),Px=(function(){function t(e){this.tokens=[],e&&(this.tokens=e)}return t})(),DO=(function(){function t(){this.width=0,this.height=0,this.contentWidth=0,this.contentHeight=0,this.outerWidth=0,this.outerHeight=0,this.lines=[],this.isTruncated=!1}return t})();function Fx(t,e,r,i,n){var a=new DO,o=ad(t);if(!o)return a;var s=e.padding,l=s?s[1]+s[3]:0,u=s?s[0]+s[2]:0,f=e.width;f==null&&r!=null&&(f=r-l);var c=e.height;c==null&&i!=null&&(c=i-u);for(var p=e.overflow,h=(p==="break"||p==="breakAll")&&f!=null?{width:f,accumWidth:0,breakAll:p==="breakAll"}:null,v=td.lastIndex=0,d;(d=td.exec(o))!=null;){var m=d.index;m>v&&rd(a,o.substring(v,m),e,h),rd(a,d[2],e,h,d[1]),v=td.lastIndex}v<o.length&&rd(a,o.substring(v,o.length),e,h);var g=[],y=0,_=0,x=p==="truncate",S=e.lineOverflow==="truncate",b={};function w(ye,He,Fe){ye.width=He,ye.lineHeight=Fe,y+=Fe,_=Math.max(_,He)}e:for(var A=0;A<a.lines.length;A++){for(var C=a.lines[A],D=0,E=0,I=0;I<C.tokens.length;I++){var M=C.tokens[I],O=M.styleName&&e.rich[M.styleName]||{},N=M.textPadding=O.padding,R=N?N[1]+N[3]:0,F=M.font=O.font||e.font;M.contentHeight=na(F);var B=W(O.height,M.contentHeight);if(M.innerHeight=B,N&&(B+=N[0]+N[2]),M.height=B,M.lineHeight=Gt(O.lineHeight,e.lineHeight,B),M.align=O&&O.align||n,M.verticalAlign=O&&O.verticalAlign||"middle",S&&c!=null&&y+M.lineHeight>c){var V=a.lines.length;I>0?(C.tokens=C.tokens.slice(0,I),w(C,E,D),a.lines=a.lines.slice(0,A+1)):a.lines=a.lines.slice(0,A),a.isTruncated=a.isTruncated||a.lines.length<V;break e}var H=O.width,Z=H==null||H==="auto";if(typeof H=="string"&&H.charAt(H.length-1)==="%")M.percentWidth=H,g.push(M),M.contentWidth=Fr(Ur(F),M.text);else{if(Z){var Q=O.backgroundColor,ee=Q&&Q.image;ee&&(ee=Lx(ee),Fo(ee)&&(M.width=Math.max(M.width,ee.width*B/ee.height)))}var j=x&&f!=null?f-E:null;j!=null&&j<M.width?!Z||j<R?(M.text="",M.width=M.contentWidth=0):(kx(b,M.text,j-R,F,e.ellipsis,{minChar:e.truncateMinChar}),M.text=b.text,a.isTruncated=a.isTruncated||b.isTruncated,M.width=M.contentWidth=Fr(Ur(F),M.text)):M.contentWidth=Fr(Ur(F),M.text)}M.width+=R,E+=M.width,O&&(D=Math.max(D,M.lineHeight))}w(C,E,D)}a.outerWidth=a.width=W(f,_),a.outerHeight=a.height=W(c,y),a.contentHeight=y,a.contentWidth=_,a.outerWidth+=l,a.outerHeight+=u;for(var A=0;A<g.length;A++){var M=g[A],ce=M.percentWidth;M.width=parseInt(ce,10)/100*a.width}return a}function rd(t,e,r,i,n){var a=e==="",o=n&&r.rich[n]||{},s=t.lines,l=o.font||r.font,u=!1,f,c;if(i){var p=o.padding,h=p?p[1]+p[3]:0;if(o.width!=null&&o.width!=="auto"){var v=zr(o.width,i.width)+h;s.length>0&&v+i.accumWidth>i.width&&(f=e.split(`
`),u=!0),i.accumWidth=v}else{var d=Vx(e,l,i.width,i.breakAll,i.accumWidth);i.accumWidth=d.accumWidth+h,c=d.linesWidths,f=d.lines}}f||(f=e.split(`
`));for(var m=Ur(l),g=0;g<f.length;g++){var y=f[g],_=new AO;if(_.styleName=n,_.text=y,_.isLineHolder=!y&&!a,typeof o.width=="number"?_.width=o.width:_.width=c?c[g]:Fr(m,y),!g&&!u){var x=(s[s.length-1]||(s[0]=new Px)).tokens,S=x.length;S===1&&x[0].isLineHolder?x[0]=_:(y||!S||a)&&x.push(_)}else s.push(new Px([_]))}}function EO(t){var e=t.charCodeAt(0);return e>=32&&e<=591||e>=880&&e<=4351||e>=4608&&e<=5119||e>=7680&&e<=8303}var MO=Ct(",&?/;] ".split(""),function(t,e){return t[e]=!0,t},{});function IO(t){return EO(t)?!!MO[t]:!0}function Vx(t,e,r,i,n){for(var a=[],o=[],s="",l="",u=0,f=0,c=Ur(e),p=0;p<t.length;p++){var h=t.charAt(p);if(h===`
`){l&&(s+=l,f+=u),a.push(s),o.push(f),s="",l="",u=0,f=0;continue}var v=Pv(c,h.charCodeAt(0)),d=i?!1:!IO(h);if(a.length?f+v>r:n+f+v>r){f?(s||l)&&(d?(s||(s=l,l="",u=0,f=u),a.push(s),o.push(f-u),l+=h,u+=v,s="",f=u):(l&&(s+=l,l="",u=0),a.push(s),o.push(f),s=h,f=v)):d?(a.push(l),o.push(u),l=h,u=v):(a.push(h),o.push(v));continue}f+=v,d?(l+=h,u+=v):(l&&(s+=l,l="",u=0),s+=h)}return l&&(s+=l),s&&(a.push(s),o.push(f)),a.length===1&&(f+=n),{accumWidth:f,lines:a,linesWidths:o}}function nd(t,e,r,i,n,a){if(t.baseX=r,t.baseY=i,t.outerWidth=t.outerHeight=null,!!e){var o=e.width*2,s=e.height*2;ae.set(Nx,tn(r,o,n),Di(i,s,a),o,s),ae.intersect(e,Nx,null,Rx);var l=Rx.outIntersectRect;t.outerWidth=l.width,t.outerHeight=l.height,t.baseX=tn(l.x,l.width,n,!0),t.baseY=Di(l.y,l.height,a,!0)}}var Nx=new ae(0,0,0,0),Rx={outIntersectRect:{},clamp:!0};function ad(t){return t!=null?t+="":t=""}function Gx(t){var e=ad(t.text),r=t.font,i=Fr(Ur(r),e),n=na(r);return Rf(t,i,n,null)}function Rf(t,e,r,i){var n=new ae(tn(t.x||0,e,t.textAlign),Di(t.y||0,r,t.textBaseline),e,r),a=i!=null?i:od(t)?t.lineWidth:0;return a>0&&(n.x-=a/2,n.y-=a/2,n.width+=a,n.height+=a),n}function od(t){var e=t.stroke;return e!=null&&e!=="none"&&t.lineWidth>0}var ud="__zr_style_"+Math.round(Math.random()*10),ni={shadowBlur:0,shadowOffsetX:0,shadowOffsetY:0,shadowColor:"#000",opacity:1,blend:"source-over"},ua={style:{shadowBlur:!0,shadowOffsetX:!0,shadowOffsetY:!0,shadowColor:!0,opacity:!0}};ni[ud]=!0;var Hx=["z","z2","invisible"],OO=["invisible"],LO=(function(t){L(e,t);function e(r){return t.call(this,r)||this}return e.prototype._init=function(r){for(var i=ne(r),n=0;n<i.length;n++){var a=i[n];a==="style"?this.useStyle(r[a]):t.prototype.attrKV.call(this,a,r[a])}this.style||this.useStyle({})},e.prototype.beforeBrush=function(r){},e.prototype.afterBrush=function(){},e.prototype.innerBeforeBrush=function(){},e.prototype.innerAfterBrush=function(){},e.prototype.shouldBePainted=function(r,i,n,a){var o=this.transform;if(this.ignore||this.invisible||this.style.opacity===0||this.culling&&PO(this,r,i)||o&&!o[0]&&!o[3])return!1;if(n&&this.__clipPaths&&this.__clipPaths.length){for(var s=0;s<this.__clipPaths.length;++s)if(this.__clipPaths[s].isZeroArea())return!1}if(a&&this.parent)for(var l=this.parent;l;){if(l.ignore)return!1;l=l.parent}return!0},e.prototype.contain=function(r,i){return this.rectContain(r,i)},e.prototype.traverse=function(r,i){r.call(i,this)},e.prototype.rectContain=function(r,i){var n=this.transformCoordToLocal(r,i),a=this.getBoundingRect();return a.contain(n[0],n[1])},e.prototype.getPaintRect=function(){var r=this._paintRect;if(!this._paintRect||this.__dirty){var i=this.transform,n=this.getBoundingRect(),a=this.style,o=a.shadowBlur||0,s=a.shadowOffsetX||0,l=a.shadowOffsetY||0;r=this._paintRect||(this._paintRect=new ae(0,0,0,0)),i?ae.applyTransform(r,n,i):r.copy(n),(o||s||l)&&(r.width+=o*2+Math.abs(s),r.height+=o*2+Math.abs(l),r.x=Math.min(r.x,r.x+s-o),r.y=Math.min(r.y,r.y+l-o));var u=this.dirtyRectTolerance;r.isZero()||(r.x=Math.floor(r.x-u),r.y=Math.floor(r.y-u),r.width=Math.ceil(r.width+1+u*2),r.height=Math.ceil(r.height+1+u*2))}return r},e.prototype.setPrevPaintRect=function(r){r?(this._prevPaintRect=this._prevPaintRect||new ae(0,0,0,0),this._prevPaintRect.copy(r)):this._prevPaintRect=null},e.prototype.getPrevPaintRect=function(){return this._prevPaintRect},e.prototype.animateStyle=function(r){return this.animate("style",r)},e.prototype.updateDuringAnimation=function(r){r==="style"?this.dirtyStyle():this.markRedraw()},e.prototype.attrKV=function(r,i){r!=="style"?t.prototype.attrKV.call(this,r,i):this.style?this.setStyle(i):this.useStyle(i)},e.prototype.setStyle=function(r,i){return typeof r=="string"?this.style[r]=i:P(this.style,r),this.dirtyStyle(),this},e.prototype.dirtyStyle=function(r){r||this.markRedraw(),this.__dirty|=Jn,this._rect&&(this._rect=null)},e.prototype.dirty=function(){this.dirtyStyle()},e.prototype.styleChanged=function(){return!!(this.__dirty&Jn)},e.prototype.styleUpdated=function(){this.__dirty&=~Jn},e.prototype.createStyle=function(r){return bi(ni,r)},e.prototype.useStyle=function(r){r[ud]||(r=this.createStyle(r)),this.style=r,this.dirtyStyle()},e.prototype._useHoverStyle=function(r){this.__hoverStyle=r},e.prototype.isStyleObject=function(r){return r[ud]},e.prototype._innerSaveToNormal=function(r){t.prototype._innerSaveToNormal.call(this,r);var i=this._normalState;r.style&&!i.style&&(i.style=this._mergeStyle(this.createStyle(),this.style)),this._savePrimaryToNormal(r,i,Hx)},e.prototype._applyStateObj=function(r,i,n,a,o,s){t.prototype._applyStateObj.call(this,r,i,n,a,o,s);var l=!(i&&a),u=this.__inHover===Oo,f;if(i&&i.style?o?a?f=i.style:(f=this._mergeStyle(this.createStyle(),n.style),this._mergeStyle(f,i.style)):(f=this._mergeStyle(this.createStyle(),a?this.style:n.style),this._mergeStyle(f,i.style)):l&&(f=n.style),f)if(o){var c=this.style;if(this.style=this.createStyle(l?{}:c),l)for(var p=ne(c),h=0;h<p.length;h++){var v=p[h];v in f&&(f[v]=f[v Line truncated
yyyy`);var i=Pt(e),n=r?"getUTC":"get",a=i[n+"FullYear"](),o=i[n+"Month"]()+1,s=i[n+"Date"](),l=i[n+"Hours"](),u=i[n+"Minutes"](),f=i[n+"Seconds"](),c=i[n+"Milliseconds"]();return t=t.replace("MM",It(o,2)).replace("M",o).replace("yyyy",a).replace("yy",It(a%100+"",2)).replace("dd",It(s,2)).replace("d",s).replace("hh",It(l,2)).replace("h",l).replace("mm",It(u,2)).replace("m",u).replace("ss",It(f,2)).replace("s",f).replace("SSS",It(c,3)),t}function Vb(t){return t&&t.charAt(0).toUpperCase()+t.substr(1)}function Yr(t,e){return e=e||"transparent",U(t)?t:Y(t)&&t.colorStops&&(t.colorStops[0]||{}).color||e}var Mc={},bm={},yP=(function(){function t(){this._normalMasterList=[],this._nonSeriesBoxMasterList=[]}return t.prototype.create=function(e,r){this._nonSeriesBoxMasterList=i(Mc,!0),this._normalMasterList=i(bm,!1);function i(n,a){var o=[];return T(n,function(s,l){var u=s.create(e,r);o=o.concat(u||[])}),o}},t.prototype.update=function(e,r){T(this._normalMasterList,function(i){i.update&&i.update(e,r)})},t.prototype.getCoordinateSystems=function(){return this._normalMasterList.concat(this._nonSeriesBoxMasterList)},t.register=function(e,r){if(e==="matrix"||e==="calendar"){Mc[e]=r;return}bm[e]=r},t.get=function(e){return bm[e]||Mc[e]},t})();function _P(t){return!!Mc[t]}var xP=1,Tm=2;function Gb(t){Hb.set(t.fullType,{getCoord2:void 0}).getCoord2=t.getCoord2}var Hb=G();function Oc(t){var e=t.getShallow("coord",!0),r=xP;if(e==null){var i=Hb.get(t.type);i&&i.getCoord2&&(r=Tm,e=i.getCoord2(t))}return{coord:e,from:r}}var ls=0,Ic=1,SP=2;function bP(t,e){var r=t.getShallow("coordinateSystem"),i=t.getShallow("coordinateSystemUsage",!0),n=i!=null,a=ls;if(r){var o=t.mainType==="series";i==null&&(i=o?"data":"box"),i==="data"?(a=Ic,o||(a=ls)):i==="box"&&(a=SP,!o&&!_P(r)&&(a=ls))}return{coordSysType:r,kind:a}}function Yb(t){var e=t.targetModel,r=t.coordSysType,i=t.coordSysProvider,n=t.isDefaultDataCoordSys,a=t.allowNotFound,o=bP(e,!0),s=o.kind,l=o.coordSysType;if(n&&s!==Ic&&(s=Ic,l=r),s===ls||l!==r)return ls;var u=i(r,e);return u?(s===Ic?e.coordinateSystem=u:e.boxCoordinateSystem=u,s):ls}var us=yP;var Pc=T,TP=["left","right","top","bottom","width","height"],Lc=[["width","left","right"],["height","top","bottom"]];function wm(t,e,r,i,n){var a=0,o=0;i==null&&(i=1/0),n==null&&(n=1/0);var s=0;e.eachChild(function(l,u){var f=l.getBoundingRect(),c=e.childAt(u+1),p=c&&c.getBoundingRect(),h,v;if(t==="horizontal"){var d=f.width+(p?-p.x+f.x:0);h=a+d,h>i||l.newline?(a=0,h=d,o+=s+r,s=f.height):s=Math.max(s,f.height)}else{var m=f.height+(p?-p.y+f.y:0);v=o+m,v>n||l.newline?(a+=s+r,o=0,v=m,s=f.width):s=Math.max(s,f.width)}l.newline||(l.x=a,l.y=o,l.markRedraw(),t==="horizontal"?a=h+r:o=v+r)})}var Ba=wm,H3=be(wm,"vertical"),Y3=be(wm,"horizontal");function Wb(t,e){return{left:t.getShallow("left",e),top:t.getShallow("top",e),right:t.getShallow("right",e),bottom:t.getShallow("bottom",e),width:t.getShallow("width",e),height:t.getShallow("height",e)}}function wP(t,e){var r=za(t,e,{enableLayoutOnlyByCenter:!0}),i=t.getBoxLayoutParams(),n,a;if(r.type===Fl.point)a=r.refPoint,n=jt(i,{width:e.getWidth(),height:e.getHeight()});else{var o=t.get("center"),s=k(o)?o:[o,o];n=jt(i,r.refContainer),a=r.boxCoordFrom===Tm?r.refPoint:[we(s[0],n.width)+n.x,we(s[1],n.height)+n.y]}return{viewRect:n,center:a}}function Xb(t,e){var r=wP(t,e),i=r.viewRect,n=r.center,a=t.get("radius");k(a)||(a=[0,a]);var o=we(i.width,e.getWidth()),s=we(i.height,e.getHeight()),l=Math.min(o,s),u=we(a[0],l/2),f=we(a[1],l/2);return{cx:n[0],cy:n[1],r0:u,r:f,viewRect:i}}function jt(t,e,r){r=di(r||0);var i=e.width,n=e.height,a=we(t.left,i),o=we(t.top,n),s=we(t.right,i),l=we(t.bottom,n),u=we(t.width,i),f=we(t.height,n),c=r[2]+r[0],p=r[1]+r[3],h=t.aspect;switch(isNaN(u)&&(u=i-s-p-a),isNaN(f)&&(f=n-l-c-o),h!=null&&(isNaN(u)&&isNaN(f)&&(h>i/n?u=i*.8:f=n*.8),isNaN(u)&&(u=h*f),isNaN(f)&&(f=u/h)),isNaN(a)&&(a=i-s-u-p),isNaN(o)&&(o=n-l-f-c),t.left||t.right){case"center":a=i/2-u/2-r[3];break;case"right":a=i-u-p;break}switch(t.top||t.bottom){case"middle":case"center":o=n/2-f/2-r[0];break;case"bottom":o=n-f-c;break}a=a||0,o=o||0,isNaN(u)&&(u=i-p-a-(s||0)),isNaN(f)&&(f=n-c-o-(l||0));var v=new ae((e.x||0)+a+r[3],(e.y||0)+o+r[0],u,f);return v.margin=r,v}var Fl={rect:1,point:2};function za(t,e,r){var i,n,a,o=t.boxCoordinateSystem,s;if(o){var l=Oc(t),u=l.coord,f=l.from;if(o.dataToLayout){a=Fl.rect,s=f;var c=o.dataToLayout(u);i=c.contentRect||c.rect}else r&&r.enableLayoutOnlyByCenter&&o.dataToPoint&&(a=Fl.point,s=f,n=o.dataToPoint(u))}return a==null&&(a=Fl.rect),a===Fl.rect&&(i||(i={x:0,y:0,width:e.getWidth(),height:e.getHeight()}),n=[i.x+i.width/2,i.y+i.height/2]),{type:a,refContainer:i,refPoint:n,boxCoordFrom:s}}function Sn(t){var e=t.layoutMode||t.constructor.layoutMode;return Y(e)?e:e?{type:e}:null}function pr(t,e,r){var i=r&&r.ignoreSize;!k(i)&&(i=[i,i]);var n=o(Lc[0],0),a=o(Lc[1],1);l(Lc[0],t,n),l(Lc[1],t,a);function o(u,f){var c={},p=0,h={},v=0,d=2;if(Pc(u,function(y){h[y]=t[y]}),Pc(u,function(y){Oe(e,y)&&(c[y]=h[y Line truncated
`,`
`,`
`];function Ya(t,e){return e.type=t,e}function Fm(t){return t.type==="section"}function zT(t){return Fm(t)?yN:_N}function UT(t){if(Fm(t)){var e=0,r=t.blocks.length,i=r>1||r>0&&!t.noHeader;return T(t.blocks,function(n){var a=UT(n);a>=e&&(e=a+ +(i&&(!a||Fm(n)&&!n.noHeader)))}),e}return 0}function yN(t,e,r,i){var n=e.noHeader,a=xN(UT(e)),o=[],s=e.blocks||[];ze(!s||k(s)),s=s||[];var l=t.orderMode;if(e.sortBlocks&&l){s=s.slice();var u={valueAsc:"asc",valueDesc:"desc"};if(Oe(u,l)){var f=new DT(u[l],null);s.sort(function(d,m){return f.evaluate(d.sortParam,m.sortParam)})}else l==="seriesDesc"&&s.reverse()}T(s,function(d,m){var g=e.valueFormatter,y=zT(d)(g?P(P({},t),{valueFormatter:g}):t,d,m>0?a.html:0,i);y!=null&&o.push(y)});var c=t.renderMode==="richText"?o.join(a.richText):Vm(i,o.join(""),n?r:a.html);if(n)return c;var p=Dc(e.header,"ordinal",t.useUTC),h=BT(i,t.renderMode).nameStyle,v=kT(i);return t.renderMode==="richText"?FT(t,p,h)+a.richText+c:Vm(i,'<div style="'+h+";"+v+';">'+Je(p)+"</div>"+c,r)}function _N(t,e,r,i){var n=t.renderMode,a=e.noName,o=e.noValue,s=!e.markerType,l=e.name,u=t.useUTC,f=e.valueFormatter||t.valueFormatter||function(x){return x=k(x)?x:[x],z(x,function(S,b){return Dc(S,k(h)?h[b]:h,u)})};if(!(a&&o)){var c=s?"":t.markupStyleCreator.makeTooltipMarker(e.markerType,e.markerColor||K.color.secondary,n),p=a?"":Dc(l,"ordinal",u),h=e.valueType,v=o?[]:f(e.value,e.rawDataIndex),d=!s||!a,m=!s&&a,g=BT(i,n),y=g.nameStyle,_=g.valueStyle;return n==="richText"?(s?"":c)+(a?"":FT(t,p,y))+(o?"":TN(t,v,d,m,_)):Vm(i,(s?"":c)+(a?"":SN(p,!s,y))+(o?"":bN(v,d,m,_)),r)}}function Gm(t,e,r,i,n,a){if(t){var o=zT(t),s={useUTC:n,renderMode:r,orderMode:i,markupStyleCreator:e,valueFormatter:t.valueFormatter};return o(s,t,0,a)}}function xN(t){return{html:mN[t],richText:gN[t]}}function Vm(t,e,r){var i='<div style="clear:both"></div>',n="margin: "+r+"px 0 0",a=kT(t);return'<div style="'+n+";"+a+';">'+e+i+"</div>"}function SN(t,e,r){var i=e?"margin-left:2px":"";return'<span style="'+r+";"+i+'">'+Je(t)+"</span>"}function bN(t,e,r,i){var n=r?"10px":"20px",a=e?"float:right;margin-left:"+n:"";return t=k(t)?t:[t],'<span style="'+a+";"+i+'">'+z(t,function(o){return Je(o)}).join("&nbsp;&nbsp;")+"</span>"}function FT(t,e,r){return t.markupStyleCreator.wrapRichTextStyle(e,r)}function TN(t,e,r,i,n){var a=[n],o=i?10:20;return r&&a.push({padding:[0,0,0,o],align:"right"}),t.markupStyleCreator.wrapRichTextStyle(k(e)?e.join(" "):e,a)}function VT(t,e){var r=t.getData().getItemVisual(e,"style"),i=r[t.visualDrawType];return Yr(i)}function qc(t,e){var r=t.get("padding");return r!=null?r:e==="richText"?[8,10]:10}var Kc=(function(){function t(){this.richTextStyles={},this._nextStyleNameId=vl()}return t.prototype._generateStyleName=function(){return"__EC_aUTo_"+this._nextStyleNameId++},t.prototype.makeTooltipMarker=function(e,r,i){var n=i==="richText"?this._generateStyleName():null,a=Ec({color:r,type:e,renderMode:i,markerId:n});return U(a)?a:(this.richTextStyles[n]=a.style,a.content)},t.prototype.wrapRichTextStyle=function(e,r){var i={};k(r)?T(r,function(a){return P(i,a)}):P(i,r);var n=this._generateStyleName();return this.richTextStyles[n]=i,"{"+n+"|"+e+"}"},t})();function GT(t){var e=t.series,r=t.dataIndex,i=t.multipleSeries,n=e.getData(),a=n.mapDimensionsAll("defaultedTooltip"),o=a.length,s=e.getRawValue(r),l=k(s),u=VT(e,r),f,c,p,h;if(o>1||l&&!o){var v=wN(s,e,r,a,u);f=v.inlineValues,c=v.inlineValueTypes,p=v.blocks,h=v.inlineValues[0]}else if(o){var d=n.getDimensionInfo(a[0]);h=f=Bi(n,r,a[0]),c=d.type}else h=f=l?s[0]:s;var m=Ro(e),g=m&&e.name||"",y=n.getName(r),_=i?g:y;return Ya("section",{header:g,noHeader:i||!m,sortParam:h,blocks:[Ya("nameValue",{markerType:"item",markerColor:u,name:_,noName:!pt(_),value:f,valueType:c,rawDataIndex:n.getRawIndex(r)})].concat(p||[])})}function wN(t,e,r,i,n){var a=e.getData(),o=Ct(t,function(c,p,h){var v=a.getDimensionInfo(h);return c=c||v&&v.tooltip!==!1&&v.displayName!=null},!1),s=[],l=[],u=[];i.length?T(i,function(c){f(Bi(a,r,c),c)}):T(t,f);function f(c,p){var h=a.getDimensionInfo(p);!h||h.otherDims.tooltip===!1||(o?u.push(Ya("nameValue",{markerType:"subItem",markerColor:n,name:h.displayName,value:c,valueType:h.type})):(s.push(c),l.push(h.type)))}return{inlineValues:s,inlineValueTypes:l,blocks:u}}var wn=te();function jc(t,e){return t.getName(e)||t.getId(e)}var CN="__universalTransitionEnabled",Qc=(function(t){L(e,t);function e(){var r=t!==null&&t.apply(this,arguments)||this;return r._selectedDataIndicesMap={},r}return e.prototype.init=function(r,i,n){this.seriesIndex=this.componentIndex,this.dataTask=Tn({count:DN,reset:EN}),this.dataTask.context={model:this},this.mergeDefaultAndTheme(r,n);var a=wn(this).sourceManager=new RT(this);a.prepareSource();var o=this.getInitialData(r,n);YT(o,this),this.dataTask.context.data=o,wn(this).dataBeforeProcessed=o,HT(this),this._initSelectedMapFromData(o)},e.prototype.mergeDefaultAndTheme=function(r,i){var n=Sn(this),a=n?Dr(r):{},o=this.subType;Se.hasClass(o)&&(o+="Series"),he Line truncated
`:"<br/>",g=c.join(m);this._showOrMove(s,function(){this._updateContentNotChangedOnAxis(r,u)?this._updatePosition(s,h,o[0],o[1],this._tooltipContent,u):this._showTooltipContent(s,g,u,Math.random()+"",o[0],o[1],h,null,p)})},e.prototype._showSeriesItemTooltip=function(r,i,n){var a=this._ecModel,o=me(i),s=o.seriesIndex,l=a.getSeriesByIndex(s),u=o.dataModel||l,f=o.dataIndex,c=o.dataType,p=u.getData(c),h=this._renderMode,v=r.positionDefault,d=Du([p.getItemModel(f),u,l&&(l.coordinateSystem||{}).model],this._tooltipModel,v?{position:v}:null),m=d.get("trigger");if(!(m!=null&&m!=="item")){var g=u.getDataParams(f,c),y=new Kc;g.marker=y.makeTooltipMarker("item",Yr(g.color),h);var _=km(u.formatTooltip(f,!1,c)),x=d.get("order"),S=d.get("valueFormatter"),b=_.frag,w=b?Gm(S?P({valueFormatter:S},b):b,y,h,x,a.get("useUTC"),d.get("textStyle")):_.text,A="item_"+u.name+"_"+f;this._showOrMove(d,function(){this._showTooltipContent(d,w,g,A,r.offsetX,r.offsetY,r.position,r.target,y)}),n({type:"showTip",dataIndexInside:f,dataIndex:p.getRawIndex(f),seriesIndex:s,from:this.uid})}},e.prototype._showComponentItemTooltip=function(r,i,n){var a=this._renderMode==="html",o=me(i),s=o.tooltipConfig,l=s.option||{},u=l.encodeHTMLContent;if(U(l)){var f=l;l={content:f,formatter:f},u=!0}u&&a&&l.content&&(l=$(l),l.content=Je(l.content));var c=[l],p=this._ecModel.getComponent(o.componentMainType,o.componentIndex);p&&c.push(p),c.push({formatter:l.content});var h=r.positionDefault,v=Du(c,this._tooltipModel,h?{position:h}:null),d=v.get("content"),m=Math.random()+"",g=new Kc;this._showOrMove(v,function(){var y=$(v.get("formatterParams")||{});this._showTooltipContent(v,d,y,m,r.offsetX,r.offsetY,r.position,i,g)}),n({type:"showTip",from:this.uid})},e.prototype._showTooltipContent=function(r,i,n,a,o,s,l,u,f){if(this._ticket="",!(!r.get("showContent")||!r.get("show"))){var c=this._tooltipContent;c.setEnterable(r.get("enterable"));var p=r.get("formatter");l=l||r.get("position");var h=i,v=this._getNearestPoint([o,s],n,r.get("trigger"),r.get("borderColor"),r.get("defaultBorderColor",!0)),d=v.color;if(p)if(U(p)){var m=r.ecModel.get("useUTC"),g=k(n)?n[0]:n,y=g&&g.axisType&&g.axisType.indexOf("time")>=0;h=p,y&&(h=Ri(g.axisValue,h,m)),h=ss(h,n,!0)}else if(X(p)){var _=J(function(x,S){x===this._ticket&&(c.setContent(S,f,r,d,l),this._updatePosition(r,l,o,s,c,n,u))},this);this._ticket=a,h=p(n,a,_)}else h=p;c.setContent(h,f,r,d,l),c.show(r,d),this._updatePosition(r,l,o,s,c,n,u)}},e.prototype._getNearestPoint=function(r,i,n,a,o){if(n==="axis"||k(i))return{color:a||o};if(!k(i))return{color:a||i.color||i.borderColor}},e.prototype._updatePosition=function(r,i,n,a,o,s,l){var u=this._api.getWidth(),f=this._api.getHeight();i=i||r.get("position");var c=o.getSize(),p=r.get("align"),h=r.get("verticalAlign"),v=l&&l.getBoundingRect().clone();if(l&&v.applyTransform(l.transform),X(i)&&(i=i([n,a],s,o.el,v,{viewSize:[u,f],contentSize:c.slice()})),k(i))n=we(i[0],u),a=we(i[1],f);else if(Y(i)){var d=i;d.width=c[0],d.height=c[1];var m=jt(d,{width:u,height:f});n=m.x,a=m.y,p=null,h=null}else if(U(i)&&l){var g=jz(i,v,c,r.get("borderWidth"));n=g[0],a=g[1]}else{var g=qz(n,a,o,u,f,p?null:20,h?null:20);n=g[0],a=g[1]}if(p&&(n-=dE(p)?c[0]/2:p==="right"?c[0]:0),h&&(a-=dE(h)?c[1]/2:h==="bottom"?c[1]:0),bh(r)){var g=Kz(n,a,o,u,f);n=g[0],a=g[1]}o.moveTo(n,a)},e.prototype._updateContentNotChangedOnAxis=function(r,i){var n=this._lastDataByCoordSys,a=this._cbParamsList,o=!!n&&n.length===r.length;return o&&T(n,function(s,l){var u=s.dataByAxis||[],f=r[l]||{},c=f.dataByAxis||[];o=o&&u.length===c.length,o&&T(u,function(p,h){var v=c[h]||{},d=p.seriesDataIndices||[],m=v.seriesDataIndices||[];o=o&&p.value===v.value&&p.axisType===v.axisType&&p.axisId===v.axisId&&d.length===m.length,o&&T(d,function(g,y){var _=m[y];o=o&&g.seriesIndex===_.seriesIndex&&g.dataIndex===_.dataIndex}),a&&T(p.seriesDataIndices,function(g){var y=g.seriesIndex,_=i[y],x=a[y];_&&x&&x.data!==_.data&&(o=!1)})})}),this._lastDataByCoordSys=r,this._cbParamsList=i,!!o},e.prototype._hide=function(r){this._lastDataByCoordSys=null,this._cbParamsList=null,r({type:"hideTip",from:this.uid})},e.prototype.dispose=function(r,i){oe.node||!i.getDom()||(Za(this,"_updatePosition"),this._tooltipContent.dispose(),wu("itemTooltip",i),this._tooltipContent=null,this._tooltipModel=null,this._lastDataByCoordSys=null,this._cbParamsList=null)},e.type="tooltip",e})(et);function Du(t,e,r){var i=e.ecModel,n;r?(n=new Ce(r,i,i),n=new Ce(e.option,n,i)):n=e;for(var a=t.length-1;a>=0;a--){var o=t[a];o&&(o instanceof Ce&&(o=o.get("tooltip",!0)),U(o)&&(o={formatter:o}),o&&(n=new Ce(o,n,i)))}return n}function vE(t,e){return t.dispatchAction||J(e.dispatchAction,e)}function qz(t,e,r,i,n,a,o){var s=r.getSize(),l=s[0],u=s[1];return a!=null&&(t+l+a+2>i?t-=l+a:t+=a),o!=null&&(e+u+o>n?e-=u+o:e+=o),[t,e]}function Kz(t,e,r,i,n){var a=r.getSize(),o=a[0],s=a[1];return t=Math.min(t+o,i)-o,e=Math.min(e+s,n)-s,t=Math.max(t,0),e=Math.max(e,0),[t,e]}function jz(t,e,r,i){var n=r[0],a=r[1],o=Math.c Line truncated
`)){var y=new Th;y.newline=!0,u.add(y);return}var _=n.getSeriesByName(g)[0];if(!f.get(g))if(_){var x=_.getData(),S=x.getVisual("legendLineStyle")||{},b=x.getVisual("legendIcon"),w=x.getVisual("style"),A=this._createItem(_,g,d,v,i,r,S,w,b,c,a);A.on("click",Us(gE,g,null,a,h)).on("mouseover",Us(t0,_.name,null,a,h)).on("mouseout",Us(r0,_.name,null,a,h)),n.ssr&&A.eachChild(function(C){var D=me(C);D.seriesIndex=_.seriesIndex,D.dataIndex=d,D.ssrType="legend"}),p&&A.eachChild(function(C){m.packEventData(C,i,_,d,g)}),f.set(g,!0)}else n.eachRawSeries(function(C){var D=this;if(!f.get(g)&&C.legendVisualProvider){var E=C.legendVisualProvider;if(!E.containName(g))return;var I=E.indexOfName(g),M=E.getItemVisual(I,"style"),O=E.getItemVisual(I,"legendIcon"),N=ht(M.fill);N&&N[3]===0&&(N[3]=.2,M=P(P({},M),{fill:ei(N,"rgba")}));var R=this._createItem(C,g,d,v,i,r,{},M,O,c,a);R.on("click",Us(gE,null,g,a,h)).on("mouseover",Us(t0,null,g,a,h)).on("mouseout",Us(r0,null,g,a,h)),n.ssr&&R.eachChild(function(F){var B=me(F);B.seriesIndex=C.seriesIndex,B.dataIndex=d,B.ssrType="legend"}),p&&R.eachChild(function(F){D.packEventData(F,i,C,d,g)}),f.set(g,!0)}},this)},this),o&&this._createSelector(o,i,a,s,l)},e.prototype.packEventData=function(r,i,n,a,o){var s={componentType:"legend",componentIndex:i.componentIndex,dataIndex:a,value:o,seriesIndex:n.seriesIndex};me(r).eventData=s},e.prototype._createSelector=function(r,i,n,a,o){var s=this.getSelectorGroup();e0(r,function(u){var f=u.type,c=new De({style:{x:0,y:0,align:"center",verticalAlign:"middle"},onclick:function(){n.dispatchAction({type:f==="all"?"legendAllSelect":"legendInverseSelect",legendId:i.id})}});s.add(c);var p=i.getModel("selectorLabel"),h=i.getModel(["emphasis","selectorLabel"]);hi(c,{normal:p,emphasis:h},{defaultText:u.title}),dn(c)})},e.prototype._createItem=function(r,i,n,a,o,s,l,u,f,c,p){var h=r.visualDrawType,v=o.get("itemWidth"),d=o.get("itemHeight"),m=o.isSelected(i),g=a.get("symbolRotate"),y=a.get("symbolKeepAspect"),_=a.get("icon");f=_||f||"roundRect";var x=tU(f,a,l,u,h,m,p),S=new Th,b=a.getModel("textStyle");if(X(r.getLegendIcon)&&(!_||_==="inherit"))S.add(r.getLegendIcon({itemWidth:v,itemHeight:d,icon:f,iconRotate:g,itemStyle:x.itemStyle,lineStyle:x.lineStyle,symbolKeepAspect:y}));else{var w=_==="inherit"&&r.getData().getVisual("symbol")?g==="inherit"?r.getData().getVisual("symbolRotate"):g:0;S.add(rU({itemWidth:v,itemHeight:d,icon:f,iconRotate:w,itemStyle:x.itemStyle,lineStyle:x.lineStyle,symbolKeepAspect:y}))}var A=s==="left"?v+5:-5,C=s,D=o.get("formatter"),E=i;U(D)&&D?E=D.replace("{name}",i!=null?i:""):X(D)&&(E=D(i));var I=m?b.getTextColor():a.get("inactiveColor");S.add(new De({style:fr(b,{text:E,x:A,y:d/2,fill:I,align:C,verticalAlign:"middle"},{inheritColor:I})}));var M=new ge({shape:S.getBoundingRect(),style:{fill:"transparent"}}),O=a.getModel("tooltip");return O.get("show")&&$o({el:M,componentModel:o,itemName:i,itemTooltipOption:O.option}),S.add(M),S.eachChild(function(N){N.silent=!0}),M.silent=!c,this.getContentGroup().add(S),dn(S),S.__legendDataIndex=n,S},e.prototype.layoutInner=function(r,i,n,a,o,s){var l=this.getContentGroup(),u=this.getSelectorGroup();Ba(r.get("orient"),l,r.get("itemGap"),n.width,n.height);var f=l.getBoundingRect(),c=[-f.x,-f.y];if(u.markRedraw(),l.markRedraw(),o){Ba("horizontal",u,r.get("selectorItemGap",!0));var p=u.getBoundingRect(),h=[-p.x,-p.y],v=r.get("selectorButtonGap",!0),d=r.getOrient().index,m=d===0?"width":"height",g=d===0?"height":"width",y=d===0?"y":"x";s==="end"?h[d]+=f[m]+v:c[d]+=p[m]+v,h[1-d]+=f[g]/2-p[g]/2,u.x=h[0],u.y=h[1],l.x=c[0],l.y=c[1];var _={x:0,y:0};return _[m]=f[m]+v+p[m],_[g]=Math.max(f[g],p[g]),_[y]=Math.min(0,p[y]+h[1-d]),_}else return l.x=c[0],l.y=c[1],this.group.getBoundingRect()},e.prototype.remove=function(){this.getContentGroup().removeAll(),this._isFirstRender=!0},e.type="legend.plain",e})(et);function tU(t,e,r,i,n,a,o){function s(m,g){m.lineWidth==="auto"&&(m.lineWidth=g.lineWidth>0?2:0),e0(m,function(y,_){m[_]==="inherit"&&(m[_]=g[_])})}var l=e.getModel("itemStyle"),u=l.getItemStyle(),f=t.lastIndexOf("empty",0)===0?"fill":"stroke",c=l.getShallow("decal");u.decal=!c||c==="inherit"?i.decal:$l(c,o),u.fill==="inherit"&&(u.fill=i[n]),u.stroke==="inherit"&&(u.stroke=i[f]),u.opacity==="inherit"&&(u.opacity=(n==="fill"?i:r).opacity),s(u,i);var p=e.getModel("lineStyle"),h=p.getLineStyle();if(s(h,r),u.fill==="auto"&&(u.fill=i.fill),u.stroke==="auto"&&(u.stroke=i.fill),h.stroke==="auto"&&(h.stroke=i.fill),!a){var v=e.get("inactiveBorderWidth"),d=u[f];u.lineWidth=v==="auto"?i.lineWidth>0&&d?2:0:u.lineWidth,u.fill=e.get("inactiveColor"),u.stroke=e.get("inactiveBorderColor"),h.stroke=p.get("inactiveColor"),h.lineWidth=p.get("inactiveWidth")}return{itemStyle:u,lineStyle:h}}function rU(t){var e=t.icon||"roundRect",r=Lt(e,0,0,t.itemWidth,t.itemHeight,t.itemStyle.fill,t.symbolKeepAspect);return r.setStyle(t.itemStyle),r.rotation=(t.iconRotate||0)*Math.PI/180,r.setOrigin([t.itemWidth/2,t.itemHeight/2]),e.indexOf("empty")>-1 Line truncated
`||n==="")){var a=t.isSelected(n);Oe(r,n)?r[n]=r[n]&&a:r[n]=a}}),r}function _E(t){t.registerAction("legendToggleSelect","legendselectchanged",be(Mu,"toggleSelected")),t.registerAction("legendAllSelect","legendselectall",be(Mu,"allSelect")),t.registerAction("legendInverseSelect","legendinverseselect",be(Mu,"inverseSelect")),t.registerAction("legendSelect","legendselected",be(Mu,"select")),t.registerAction("legendUnSelect","legendunselected",be(Mu,"unSelect"))}var xE=on(iU);function iU(t){var e=t.findComponents({mainType:"legend"});e&&e.length&&t.filterSeries(function(r){for(var i=0;i<e.length;i++)if(!e[i].isSelected(r.name))return!1;return!0})}function Ch(t){t.registerComponentModel(Eu),t.registerComponentView(wh),t.registerProcessor(t.PRIORITY.PROCESSOR.SERIES_FILTER,xE),t.registerSubTypeDefaulter("legend",function(){return"plain"}),_E(t)}var nU=(function(t){L(e,t);function e(){var r=t!==null&&t.apply(this,arguments)||this;return r.type=e.type,r}return e.prototype.setScrollDataIndex=function(r){this.option.scrollDataIndex=r},e.prototype.init=function(r,i,n){var a=Dr(r);t.prototype.init.call(this,r,i,n),SE(this,r,a)},e.prototype.mergeOption=function(r,i){t.prototype.mergeOption.call(this,r,i),SE(this,this.option,r)},e.type="legend.scroll",e.defaultOption=xn(Eu.defaultOption,{scrollDataIndex:0,pageButtonItemGap:5,pageButtonGap:null,pageButtonPosition:"end",pageFormatter:"{current}/{total}",pageIcons:{horizontal:["M0,0L12,-10L12,10z","M0,0L-12,-10L-12,10z"],vertical:["M0,0L20,0L10,-20z","M0,0L20,0L10,20z"]},pageIconColor:K.color.accent50,pageIconInactiveColor:K.color.accent10,pageIconSize:15,pageTextStyle:{color:K.color.tertiary},animationDurationUpdate:800}),e})(Eu);function SE(t,e,r){var i=t.getOrient(),n=[1,1];n[i.index]=0,pr(e,r,{type:"box",ignoreSize:!!n})}var bE=nU;var TE=xe,i0=["width","height"],n0=["x","y"],aU=(function(t){L(e,t);function e(){var r=t!==null&&t.apply(this,arguments)||this;return r.type=e.type,r.newlineDisabled=!0,r._currentIndex=0,r}return e.prototype.init=function(){t.prototype.init.call(this),this.group.add(this._containerGroup=new TE),this._containerGroup.add(this.getContentGroup()),this.group.add(this._controllerGroup=new TE)},e.prototype.resetInner=function(){t.prototype.resetInner.call(this),this._controllerGroup.removeAll(),this._containerGroup.removeClipPath(),this._containerGroup.__rectSize=null},e.prototype.renderInner=function(r,i,n,a,o,s,l){var u=this;t.prototype.renderInner.call(this,r,i,n,a,o,s,l);var f=this._controllerGroup,c=i.get("pageIconSize",!0),p=k(c)?c:[c,c];v("pagePrev",0);var h=i.getModel("pageTextStyle");f.add(new De({name:"pageText",style:{text:"xx/xx",fill:h.getTextColor(),font:h.getFont(),verticalAlign:"middle",align:"center"},silent:!0})),v("pageNext",1);function v(d,m){var g=d+"DataIndex",y=Ia(i.get("pageIcons",!0)[i.getOrient().name][m],{onclick:J(u._pageGo,u,g,i,a)},{x:-p[0]/2,y:-p[1]/2,width:p[0],height:p[1]});y.name=d,f.add(y)}},e.prototype.layoutInner=function(r,i,n,a,o,s){var l=this.getSelectorGroup(),u=r.getOrient().index,f=i0[u],c=n0[u],p=i0[1-u],h=n0[1-u];o&&Ba("horizontal",l,r.get("selectorItemGap",!0));var v=r.get("selectorButtonGap",!0),d=l.getBoundingRect(),m=[-d.x,-d.y],g=$(n);o&&(g[f]=n[f]-d[f]-v);var y=this._layoutContentAndController(r,a,g,u,f,p,h,c);if(o){if(s==="end")m[u]+=y[f]+v;else{var _=d[f]+v;m[u]-=_,y[c]-=_}y[f]+=d[f]+v,m[1-u]+=y[h]+y[p]/2-d[p]/2,y[p]=Math.max(y[p],d[p]),y[h]=Math.min(y[h],d[h]+m[1-u]),l.x=m[0],l.y=m[1],l.markRedraw()}return y},e.prototype._layoutContentAndController=function(r,i,n,a,o,s,l,u){var f=this.getContentGroup(),c=this._containerGroup,p=this._controllerGroup;Ba(r.get("orient"),f,r.get("itemGap"),a?n.width:null,a?null:n.height),Ba("horizontal",p,r.get("pageButtonItemGap",!0));var h=f.getBoundingRect(),v=p.getBoundingRect(),d=this._showController=h[o]>n[o],m=[-h.x,-h.y];i||(m[a]=f[u]);var g=[0,0],y=[-v.x,-v.y],_=W(r.get("pageButtonGap",!0),r.get("itemGap",!0));if(d){var x=r.get("pageButtonPosition",!0);x==="end"?y[a]+=n[o]-v[o]:g[a]+=v[o]+_}y[1-a]+=h[s]/2-v[s]/2,f.setPosition(m),c.setPosition(g),p.setPosition(y);var S={x:0,y:0};if(S[o]=d?n[o]:h[o],S[s]=Math.max(h[s],v[s]),S[l]=Math.min(0,v[l]+y[1-a]),c.__rectSize=n[o],d){var b={x:0,y:0};b[o]=Math.max(n[o]-v[o]-_,0),b[s]=S[s],c.setClipPath(new ge({shape:b})),c.__rectSize=b[o]}else p.eachChild(function(A){A.attr({invisible:!0,silent:!0})});var w=this._getPageInfo(r);return w.pageIndex!=null&&Be(f,{x:w.contentPosition[0],y:w.contentPosition[1]},d?r:null),this._updatePageInfoView(r,w),S},e.prototype._pageGo=function(r,i,n){var a=this._getPageInfo(i)[r];a!=null&&n.dispatchAction({type:"legendScroll",scrollDataIndex:a,legendId:i.id})},e.prototype._updatePageInfoView=function(r,i){var n=this._controllerGroup;T(["pagePrev","pageNext"],function(f){var c=f+"DataIndex",p=i[c]!=null,h=n.childOfName(f);h&&(h.setStyle("fill",p?r.get("pageIconColor",!0):r.get("pageIconInactiveColor",!0)),h.cursor=p?"pointer":"default")});var a=n.childOfName("pageText"),o=r.get("pageFormatt Line truncated
`:"";function i(n){var a=n.children,o=n.tag,s=n.attrs,l=n.text;return IU(o,s)+(o!=="style"?Je(l):l||"")+(a?""+r+z(a,function(u){return i(u)}).join(r)+r:"")+OU(o)}return i(t)}function XE(t,e,r){r=r||{};var i=r.newline?`
`:"",n=" {"+i,a=i+"}",o=z(ne(t),function(l){return l+n+z(ne(t[l]),function(u){return u+":"+t[l][u]+";"}).join(i)+a}).join(i),s=z(ne(e),function(l){return"@keyframes "+l+n+z(ne(e[l]),function(u){return u+n+z(ne(e[l][u]),function(f){var c=e[l][u][f];return f==="d"&&(c='path("'+c+'")'),f+":"+c+";"}).join(i)+a}).join(i)+a}).join(i);return!o&&!s?"":["<![CDATA[",o,s,"]]>"].join(i)}function Nu(t){return{zrId:t,shadowCache:{},patternCache:{},gradientCache:{},clipPathCache:{},defs:{},cssNodes:{},cssAnims:{},cssStyleCache:{},cssAnimIdx:0,shadowIdx:0,gradientIdx:0,patternIdx:0,clipPathIdx:0}}function x0(t,e,r,i){return Ke("svg","root",{width:t,height:e,xmlns:HE,"xmlns:xlink":y0,version:"1.1",baseProfile:"full",viewBox:i?"0 0 "+t+" "+e:!1},r)}var LU=0;function Eh(){return LU++}var ZE={cubicIn:"0.32,0,0.67,0",cubicOut:"0.33,1,0.68,1",cubicInOut:"0.65,0,0.35,1",quadraticIn:"0.11,0,0.5,0",quadraticOut:"0.5,1,0.89,1",quadraticInOut:"0.45,0,0.55,1",quarticIn:"0.5,0,0.75,0",quarticOut:"0.25,1,0.5,1",quarticInOut:"0.76,0,0.24,1",quinticIn:"0.64,0,0.78,0",quinticOut:"0.22,1,0.36,1",quinticInOut:"0.83,0,0.17,1",sinusoidalIn:"0.12,0,0.39,0",sinusoidalOut:"0.61,1,0.88,1",sinusoidalInOut:"0.37,0,0.63,1",exponentialIn:"0.7,0,0.84,0",exponentialOut:"0.16,1,0.3,1",exponentialInOut:"0.87,0,0.13,1",circularIn:"0.55,0,1,0.45",circularOut:"0,0.55,0.45,1",circularInOut:"0.85,0,0.15,1"},uo="transform-origin";function PU(t,e,r){var i=P({},t.shape);P(i,e),t.buildPath(r,i);var n=new Ah;return n.reset(hf(t)),r.rebuildPath(n,1),n.generateStr(),n.getStr()}function NU(t,e){var r=e.originX,i=e.originY;(r||i)&&(t[uo]=r+"px "+i+"px")}var RU={fill:"fill",opacity:"opacity",lineWidth:"stroke-width",lineDashOffset:"stroke-dashoffset"};function KE(t,e){var r=e.zrId+"-ani-"+e.cssAnimIdx++;return e.cssAnims[r]=t,r}function kU(t,e,r){var i=t.shape.paths,n={},a,o;if(T(i,function(l){var u=Nu(r.zrId);u.animation=!0,Ru(l,{},u,!0);var f=u.cssAnims,c=u.cssNodes,p=ne(f),h=p.length;if(h){o=p[h-1];var v=f[o];for(var d in v){var m=v[d];n[d]=n[d]||{d:""},n[d].d+=m.d||""}for(var g in c){var y=c[g].animation;y.indexOf(o)>=0&&(a=y)}}}),!!a){e.d=!1;var s=KE(n,r);return a.replace(o,s)}}function qE(t){return U(t)?ZE[t]?"cubic-bezier("+ZE[t]+")":To(t)?t:"":""}function Ru(t,e,r,i){var n=t.animators,a=n.length,o=[];if(t instanceof Zo){var s=kU(t,e,r);if(s)o.push(s);else if(!a)return}else if(!a)return;for(var l={},u=0;u<a;u++){var f=n[u],c=[f.getMaxTime()/1e3+"s"],p=qE(f.getClip().easing),h=f.getDelay();p?c.push(p):c.push("linear"),h&&c.push(h/1e3+"s"),f.getLoop()&&c.push("infinite");var v=c.join(" ");l[v]=l[v]||[v,[]],l[v][1].push(f)}function d(y){var _=y[1],x=_.length,S={},b={},w={},A="animation-timing-function";function C(Fe,Te,Pe){for(var fe=Fe.getTracks(),_e=Fe.getMaxTime(),Ut=0;Ut<fe.length;Ut++){var Ye=fe[Ut];if(Ye.needsAnimate()){var Lr=Ye.keyframes,Ft=Ye.propName;if(Pe&&(Ft=Pe(Ft)),Ft)for(var Pr=0;Pr<Lr.length;Pr++){var fo=Lr[Pr],Nr=Math.round(fo.time/_e*100)+"%",Uu=qE(fo.easing),co=fo.rawValue;(U(co)||se(co))&&(Te[Nr]=Te[Nr]||{},Te[Nr][Ft]=fo.rawValue,Uu&&(Te[Nr][A]=Uu))}}}}for(var D=0;D<x;D++){var E=_[D],I=E.targetName;I?I==="shape"&&C(E,b):!i&&C(E,S)}for(var M in S){var O={};en(O,t),P(O,S[M]);var N=vf(O),R=S[M][A];w[M]=N?{transform:N}:{},NU(w[M],O),R&&(w[M][A]=R)}var F,B=!0;for(var M in b){w[M]=w[M]||{};var V=!F,R=b[M][A];V&&(F=new bt);var H=F.len();F.reset(),w[M].d=PU(t,b[M],F);var Z=F.len();if(!V&&H!==Z){B=!1;break}R&&(w[M][A]=R)}if(!B)for(var M in w)delete w[M].d;if(!i)for(var D=0;D<x;D++){var E=_[D],I=E.targetName;I==="style"&&C(E,w,function(fe){return RU[fe]})}for(var Q=ne(w),ee=!0,j,D=1;D<Q.length;D++){var ce=Q[D-1],ye=Q[D];if(w[ce][uo]!==w[ye][uo]){ee=!1;break}j=w[ce][uo]}if(ee&&j){for(var M in w)w[M][uo]&&delete w[M][uo];e[uo]=j}if(Ae(Q,function(Fe){return ne(w[Fe]).length>0}).length){var He=KE(w,r);return He+" "+y[0]+" both"}}for(var m in l){var s=d(l[m]);s&&o.push(s)}if(o.length){var g=r.zrId+"-cls-"+Eh();r.cssNodes["."+g]={animation:o.join(",")},e.class=g}}function QE(t,e,r){if(!t.ignore)if(t.isSilent()){var i={"pointer-events":"none"};jE(i,e,r,!0)}else{var n=t.states.emphasis&&t.states.emphasis.style?t.states.emphasis.style:{},a=n.fill;if(!a){var o=t.style&&t.style.fill,s=t.states.select&&t.states.select.style&&t.states.select.style.fill,l=t.currentStates.indexOf("select")>=0&&s||o;l&&(a=Ao(l))}var u=n.lineWidth;if(u){var f=!n.strokeNoScale&&t.transform?t.transform[0]:1;u=u/f}var i={cursor:"pointer"};a&&(i.fill=a),n.stroke&&(i.stroke=n.stroke),u&&(i["stroke-width"]=u),jE(i,e,r,!0)}}function jE(t,e,r,i){var n=JSON.stringify(t),a=r.cssStyleCache[n];a||(a=r.zrId+"-cls-"+Eh(),r.cssStyleCache[n]=a,r.cssNodes["."+a+(i?":hover":"")]=t),e.class=e.class?e.class+" "+a:a}var ku=Math.round;function tM(t){return t&&U(t.src)}function rM(t){return t&&X(t.toDataURL)}function S0(t,e,r,i){g0(function(n,a){var o=n==="fill"||n==="stroke";o&&pf(a)?C0(e,t,n,i):o&&tl(a)?A0(r,t,n,i):t[n]=a,o&&i.ssr&&a==="none"&&(t["pointer-events"]="visible")},e,r,!1),HU(r,t,i)}function b0(t,e Line truncated
+4
View File
@@ -227,6 +227,10 @@ if (class_exists('AKSRT\\Payment\\CommissionLedger')) {
if (class_exists('AKSRT\\Payment\\InviteCode')) { if (class_exists('AKSRT\\Payment\\InviteCode')) {
\AKSRT\Payment\InviteCode::get_instance()->init(); \AKSRT\Payment\InviteCode::get_instance()->init();
} }
// 支付中心统计的按天缓存:订单状态变化 / 删除时失效(需前后台都挂载,回调不走后台)
if (class_exists('AKSRT\\Payment\\OrderAnalyticsDaily')) {
\AKSRT\Payment\OrderAnalyticsDaily::get_instance()->init();
}
if (class_exists('\AKSRT\Content\Pay')) { if (class_exists('\AKSRT\Content\Pay')) {
\AKSRT\Content\Pay::get_instance()->init(); \AKSRT\Content\Pay::get_instance()->init();
} }
+1
View File
@@ -185,6 +185,7 @@ class Admin {
PermissionsController::class, PermissionsController::class,
UserController::class, UserController::class,
PaymentController::class, PaymentController::class,
PayOverviewController::class,
ToolsController::class, ToolsController::class,
ImportExportController::class, ImportExportController::class,
ExtensionController::class, ExtensionController::class,
+29 -63
View File
@@ -27,23 +27,28 @@ class PayMenus {
} }
NativeUi::boot(); NativeUi::boot();
PayOverview::get_instance()->init();
add_action('admin_menu', [$this, 'register_menus']); add_action('admin_menu', [$this, 'register_menus']);
add_action('admin_notices', [$this, 'pending_notices']); add_action('admin_notices', [$this, 'pending_notices']);
// Domain admin_post handlers are registered by dedicated modules. // Domain admin_post handlers are registered by dedicated modules.
} }
/** /**
* @return array<string, array{slug:string,title:string,callback:callable}> * @return array<string, array{slug:string,title:string,icon:string,desc:string,callback:callable}>
*/ */
private function pages(): array { private function pages(): array {
return [ return [
'overview' => [ 'overview' => [
'slug' => self::MENU_SLUG, 'slug' => self::MENU_SLUG,
'icon' => 'dashicons-cart',
'desc' => '',
'title' => __('概览', 'aksrt'), 'title' => __('概览', 'aksrt'),
'callback' => [$this, 'render_overview_page'], 'callback' => [$this, 'render_overview_page'],
], ],
'orders' => [ 'orders' => [
'slug' => 'aksrt_orders', 'slug' => 'aksrt_orders',
'icon' => 'dashicons-list-view',
'desc' => __('收款确认、关闭与退款标记', 'aksrt'),
'title' => __('订单明细', 'aksrt'), 'title' => __('订单明细', 'aksrt'),
'callback' => static function (): void { 'callback' => static function (): void {
if (class_exists('AKSRT\Admin\Native\PayOrders')) { if (class_exists('AKSRT\Admin\Native\PayOrders')) {
@@ -55,6 +60,8 @@ class PayMenus {
], ],
'products' => [ 'products' => [
'slug' => 'aksrt_pay_products', 'slug' => 'aksrt_pay_products',
'icon' => 'dashicons-media-document',
'desc' => __('付费文章定价与销量', 'aksrt'),
'title' => __('付费文章', 'aksrt'), 'title' => __('付费文章', 'aksrt'),
'callback' => static function (): void { 'callback' => static function (): void {
if (class_exists('AKSRT\\Admin\\Native\\PayProducts')) { if (class_exists('AKSRT\\Admin\\Native\\PayProducts')) {
@@ -66,6 +73,8 @@ class PayMenus {
], ],
'coupons' => [ 'coupons' => [
'slug' => 'aksrt_coupons', 'slug' => 'aksrt_coupons',
'icon' => 'dashicons-tickets-alt',
'desc' => __('创建与批量生成优惠码', 'aksrt'),
'title' => __('优惠码', 'aksrt'), 'title' => __('优惠码', 'aksrt'),
'callback' => static function (): void { 'callback' => static function (): void {
if (class_exists('AKSRT\\Admin\\Native\\PayCoupons')) { if (class_exists('AKSRT\\Admin\\Native\\PayCoupons')) {
@@ -77,6 +86,8 @@ class PayMenus {
], ],
'charge_cards' => [ 'charge_cards' => [
'slug' => 'aksrt_charge_cards', 'slug' => 'aksrt_charge_cards',
'icon' => 'dashicons-id-alt',
'desc' => __('充值卡密生成与导出', 'aksrt'),
'title' => __('卡密管理', 'aksrt'), 'title' => __('卡密管理', 'aksrt'),
'callback' => static function (): void { 'callback' => static function (): void {
if (class_exists('AKSRT\\Admin\\Native\\PayChargeCards')) { if (class_exists('AKSRT\\Admin\\Native\\PayChargeCards')) {
@@ -88,6 +99,8 @@ class PayMenus {
], ],
'income' => [ 'income' => [
'slug' => 'aksrt_income', 'slug' => 'aksrt_income',
'icon' => 'dashicons-chart-pie',
'desc' => __('创作者分成结算', 'aksrt'),
'title' => __('创作分成', 'aksrt'), 'title' => __('创作分成', 'aksrt'),
'callback' => static function (): void { 'callback' => static function (): void {
if (class_exists('AKSRT\\Admin\\Native\\PayIncome')) { if (class_exists('AKSRT\\Admin\\Native\\PayIncome')) {
@@ -99,6 +112,8 @@ class PayMenus {
], ],
'withdraw' => [ 'withdraw' => [
'slug' => 'aksrt_withdraw', 'slug' => 'aksrt_withdraw',
'icon' => 'dashicons-money-alt',
'desc' => __('提现申请审核与打款', 'aksrt'),
'title' => __('提现审核', 'aksrt'), 'title' => __('提现审核', 'aksrt'),
'callback' => static function (): void { 'callback' => static function (): void {
if (class_exists('AKSRT\\Admin\\Native\\PayWithdrawals')) { if (class_exists('AKSRT\\Admin\\Native\\PayWithdrawals')) {
@@ -164,72 +179,23 @@ class PayMenus {
} }
} }
private function ensure_cap(): void { /**
if (!current_user_can(self::CAPABILITY)) { * 概览页快捷入口:除概览外的子页面。
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt')); *
} * @return array<int, array{slug:string,title:string,icon:string,desc:string}>
} */
public function shortcuts(): array {
private function page_url(string $slug, array $args = []): string { $items = [];
return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php')); foreach ($this->pages() as $page) {
}
/** 快捷入口图标,键同 pages()。 */
private const PAGE_ICONS = [
'orders' => 'dashicons-list-view',
'products' => 'dashicons-media-document',
'coupons' => 'dashicons-tickets-alt',
'charge_cards' => 'dashicons-id-alt',
'income' => 'dashicons-chart-pie',
'withdraw' => 'dashicons-money-alt',
];
public function render_overview_page(): void {
$this->ensure_cap();
$order_stats = class_exists('\AKSRT\Payment\OrderRepository')
? \AKSRT\Payment\OrderRepository::get_instance()->get_stats()
: [];
$withdraw_stats = class_exists('\AKSRT\Payment\Withdrawal')
? \AKSRT\Payment\Withdrawal::get_instance()->get_stats()
: [];
$card_stats = class_exists('\AKSRT\Payment\ChargeCard')
? \AKSRT\Payment\ChargeCard::get_instance()->get_stats()
: [];
NativeUi::page_open();
NativeUi::header(
__('支付中心', 'aksrt'),
__('运营数据入口。配置项仍在 AKSRT 主题设置 SPA。', 'aksrt'),
'dashicons-cart',
[[
'label' => __('打开支付配置(SPA)', 'aksrt'),
'url' => admin_url('admin.php?page=aksrt-settings#/payment/gateway'),
'icon' => 'dashicons-admin-generic',
]]
);
NativeUi::stats([
['label' => __('订单总数', 'aksrt'), 'value' => (int) ($order_stats['total'] ?? 0), 'url' => $this->page_url('aksrt_orders'), 'icon' => 'dashicons-list-view', 'tone' => 'info'],
['label' => __('待支付', 'aksrt'), 'value' => (int) ($order_stats['pending'] ?? 0), 'url' => $this->page_url('aksrt_orders', ['status' => 'pending']), 'icon' => 'dashicons-clock', 'tone' => 'warning'],
['label' => __('已支付', 'aksrt'), 'value' => (int) ($order_stats['paid'] ?? 0), 'url' => $this->page_url('aksrt_orders', ['status' => 'paid']), 'icon' => 'dashicons-yes-alt', 'tone' => 'success'],
['label' => __('营收', 'aksrt'), 'value' => (string) ($order_stats['revenue'] ?? '0.00'), 'url' => $this->page_url('aksrt_orders'), 'icon' => 'dashicons-chart-line', 'tone' => 'success'],
['label' => __('待审提现', 'aksrt'), 'value' => (int) ($withdraw_stats['pending'] ?? 0), 'url' => $this->page_url('aksrt_withdraw', ['status' => 'pending']), 'icon' => 'dashicons-money-alt', 'tone' => 'danger'],
['label' => __('可用卡密', 'aksrt'), 'value' => (int) ($card_stats['active'] ?? 0), 'url' => $this->page_url('aksrt_charge_cards', ['status' => 'active']), 'icon' => 'dashicons-id-alt', 'tone' => 'info'],
]);
NativeUi::card_open(__('快捷入口', 'aksrt'));
echo '<ul class="aksrt-native-links">';
foreach ($this->pages() as $key => $page) {
if ($page['slug'] === self::MENU_SLUG) { if ($page['slug'] === self::MENU_SLUG) {
continue; continue;
} }
$icon = self::PAGE_ICONS[$key] ?? 'dashicons-admin-links'; $items[] = ['slug' => $page['slug'], 'title' => $page['title'], 'icon' => $page['icon'], 'desc' => $page['desc']];
echo '<li><a href="' . esc_url($this->page_url($page['slug'])) . '"><span class="dashicons ' . esc_attr($icon) . '" aria-hidden="true"></span>' . esc_html($page['title']) . '</a></li>'; }
return $items;
} }
echo '</ul>';
NativeUi::card_close();
NativeUi::page_close(); public function render_overview_page(): void {
PayOverview::get_instance()->render_page();
} }
} }
+325
View File
@@ -0,0 +1,325 @@
<?php
/**
* 支付中心「概览」:今日实时、区间指标卡、成交趋势、分类成交、支付方式、转化与退款、热销排行、待办与快捷入口。
*
* 统计口径见 \AKSRT\Payment\OrderAnalytics。首屏报表(近 30 天)随页面输出为 JSON,
* 之后切换区间 / 分类走 REST(aksrt/v1/admin/pay-overview,见 \AKSRT\Admin\PayOverviewController)。
* 图表用按需打包的 ECharts(assets/js/runtime/libs/echarts-pay.min.js,tools/build-pay-charts.mjs 生成),
* 绘制逻辑在 assets/js/admin-pay-overview.js,样式在 assets/css/admin/pay-overview.css。
*
* @package AKSRT
*/
namespace AKSRT\Admin\Native;
use AKSRT\Payment\OrderAnalytics;
use AKSRT\Payment\OrderAnalyticsDaily;
defined('ABSPATH') || exit;
final class PayOverview {
private const DEFAULT_DAYS = 30;
private const DATA_ID = 'aksrt-pay-overview-data';
private static $instance = null;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
add_action('admin_enqueue_scripts', [$this, 'enqueue_assets']);
}
public function enqueue_assets(): void {
$page = isset($_GET['page']) ? sanitize_key((string) wp_unslash($_GET['page'])) : '';
if ($page !== PayMenus::MENU_SLUG) {
return;
}
$lib = '/assets/js/runtime/libs/echarts-pay.min.js';
$css = '/assets/css/admin/pay-overview.css';
$js = '/assets/js/admin-pay-overview.js';
wp_enqueue_style('aksrt-admin-pay-overview', AKSRT_URI . $css, [NativeUi::STYLE_HANDLE], (string) filemtime(AKSRT_DIR . $css));
wp_enqueue_script('aksrt-echarts-pay', AKSRT_URI . $lib, [], (string) filemtime(AKSRT_DIR . $lib), true);
wp_enqueue_script('aksrt-admin-pay-overview', AKSRT_URI . $js, ['aksrt-echarts-pay'], (string) filemtime(AKSRT_DIR . $js), true);
}
public function render_page(): void {
if (!current_user_can(PayMenus::CAPABILITY)) {
wp_die(esc_html__('您没有权限访问此页面。', 'aksrt'));
}
$analytics = OrderAnalytics::get_instance();
$today = current_time('Y-m-d');
$from = OrderAnalyticsDaily::shift($today, -(self::DEFAULT_DAYS - 1));
$report = $analytics->report($from, $today, '');
$realtime = $analytics->today_vs_yesterday();
$symbol = $this->currency_symbol();
NativeUi::page_open('aksrt-po-page');
NativeUi::header(
__('支付中心', 'aksrt'),
__('成交走势、收入结构与待办一览。收款渠道、价格策略等配置在主题设置中修改。', 'aksrt'),
'dashicons-cart',
[
['label' => __('订单明细', 'aksrt'), 'url' => $this->page_url('aksrt_orders'), 'icon' => 'dashicons-list-view'],
['label' => __('支付配置', 'aksrt'), 'url' => admin_url('admin.php?page=aksrt-settings#/payment/gateway'), 'icon' => 'dashicons-admin-generic', 'primary' => true],
]
);
$this->render_realtime($realtime, $symbol);
$this->render_toolbar($from, $today);
echo '<div class="aksrt-po-notice" data-aksrt-po-excluded hidden></div>';
echo '<div class="aksrt-po-error notice notice-error" data-aksrt-po-error hidden><p></p></div>';
$this->render_kpis();
echo '<div class="aksrt-po-body" data-aksrt-po-body>';
echo '<div class="aksrt-po-grid aksrt-po-grid--main">';
$this->chart_card('trend', __('成交趋势', 'aksrt'), __('支付金额、净收入、退款金额曲线与支付订单数折线', 'aksrt'), 'lg');
$this->render_funnel_card();
echo '</div>';
echo '<div class="aksrt-po-grid aksrt-po-grid--main">';
$this->chart_card('groups', __('分类成交', 'aksrt'), __('按商品分类的支付金额曲线', 'aksrt'), 'md');
$this->chart_card('methods', __('支付方式', 'aksrt'), __('按支付金额的支付方式占比', 'aksrt'), 'md');
echo '</div>';
echo '<div class="aksrt-po-grid aksrt-po-grid--main">';
$this->render_top_card();
$this->render_todo_card();
echo '</div>';
echo '</div>';
$this->render_shortcuts_card();
$payload = [
'currency' => $symbol,
'today' => $today,
'maxDays' => OrderAnalytics::MAX_RANGE_DAYS,
'rest' => ['url' => rest_url('aksrt/v1/admin/pay-overview'), 'nonce' => wp_create_nonce('wp_rest')],
'initial' => ['from' => $from, 'to' => $today, 'group' => '', 'report' => $report],
'i18n' => [
'paid' => __('支付金额', 'aksrt'),
'net' => __('净收入', 'aksrt'),
'refund' => __('退款金额', 'aksrt'),
'count' => __('支付订单数', 'aksrt'),
'unit' => __('笔', 'aksrt'),
'people' => __('人', 'aksrt'),
'empty' => __('所选区间暂无成交', 'aksrt'),
'loadError' => __('统计数据加载失败,请稍后重试。', 'aksrt'),
'basis' => __('较上一周期', 'aksrt'),
'new' => __('新增', 'aksrt'),
'total' => __('合计', 'aksrt'),
'excluded' => __('以下外币订单未计入统计:', 'aksrt'),
'noTop' => __('所选区间暂无付费内容成交', 'aksrt'),
'badRange' => __('请选择有效的日期区间。', 'aksrt'),
],
];
echo '<script type="application/json" id="' . esc_attr(self::DATA_ID) . '">'
. wp_json_encode($payload, JSON_HEX_TAG | JSON_HEX_AMP | JSON_UNESCAPED_UNICODE)
. '</script>';
NativeUi::page_close();
}
/* ------------------------------------------------------------------
* 今日实时(同时段对比,不随区间变化)
* ----------------------------------------------------------------*/
/** @param array{today:array{amount:float,count:int},yesterday:array{amount:float,count:int},until:string} $data */
private function render_realtime(array $data, string $symbol): void {
$amount = (float) $data['today']['amount'];
$prev = (float) $data['yesterday']['amount'];
echo '<section class="aksrt-po-live" aria-label="' . esc_attr__('今日实时', 'aksrt') . '">';
echo '<div class="aksrt-po-live__main"><span class="aksrt-po-live__dot" aria-hidden="true"></span>'
. '<span class="aksrt-po-live__label">' . esc_html__('今日支付金额', 'aksrt') . '</span>'
. '<strong class="aksrt-po-live__value">' . esc_html($symbol . number_format_i18n($amount, 2)) . '</strong>'
. $this->delta_html($amount, $prev) . '</div>';
echo '<div class="aksrt-po-live__meta">';
/* translators: %d order count */
echo '<span>' . esc_html(sprintf(__('今日 %d 笔', 'aksrt'), (int) $data['today']['count'])) . '</span>';
echo '<span>' . esc_html(sprintf(
/* translators: 1 time 2 amount 3 order count */
__('昨日 00:00–%1$s:%2$s,%3$d 笔', 'aksrt'),
$data['until'],
$symbol . number_format_i18n($prev, 2),
(int) $data['yesterday']['count']
)) . '</span>';
echo '</div></section>';
}
/** 同时段环比徽标(服务端渲染,仅今日实时使用;区间指标的环比由前端计算)。 */
private function delta_html(float $current, float $previous): string {
$basis = __('较昨日同时段', 'aksrt');
if ($previous <= 0) {
$text = $current > 0 ? __('新增', 'aksrt') : '—';
$class = $current > 0 ? 'is-up' : 'is-flat';
} else {
$delta = round(($current - $previous) / $previous * 100, 1);
$class = $delta > 0 ? 'is-up' : ($delta < 0 ? 'is-down' : 'is-flat');
$text = ($delta > 0 ? '↑' : ($delta < 0 ? '↓' : '')) . number_format_i18n(abs($delta), 1) . '%';
}
return '<span class="aksrt-po-delta ' . esc_attr($class) . '">' . esc_html($basis . ' ' . $text) . '</span>';
}
/* ------------------------------------------------------------------
* 筛选条 / 指标卡
* ----------------------------------------------------------------*/
private function render_toolbar(string $from, string $to): void {
$presets = [
'd7' => __('近 7 天', 'aksrt'),
'd30' => __('近 30 天', 'aksrt'),
'd90' => __('近 90 天', 'aksrt'),
'this_month' => __('本月', 'aksrt'),
'last_month' => __('上月', 'aksrt'),
'd365' => __('近一年', 'aksrt'),
];
echo '<div class="aksrt-po-toolbar">';
echo '<div class="aksrt-po-seg" role="group" data-aksrt-po-presets aria-label="' . esc_attr__('统计区间', 'aksrt') . '">';
foreach ($presets as $key => $label) {
echo '<button type="button" data-preset="' . esc_attr($key) . '" aria-pressed="' . ($key === 'd30' ? 'true' : 'false') . '">' . esc_html($label) . '</button>';
}
echo '</div>';
echo '<form class="aksrt-po-range" data-aksrt-po-range>';
echo '<label><span class="screen-reader-text">' . esc_html__('开始日期', 'aksrt') . '</span>'
. '<input type="date" name="from" value="' . esc_attr($from) . '" max="' . esc_attr($to) . '" required></label>';
echo '<span aria-hidden="true">–</span>';
echo '<label><span class="screen-reader-text">' . esc_html__('结束日期', 'aksrt') . '</span>'
. '<input type="date" name="to" value="' . esc_attr($to) . '" max="' . esc_attr($to) . '" required></label>';
echo '<label><span class="screen-reader-text">' . esc_html__('商品分类', 'aksrt') . '</span><select name="group">'
. '<option value="">' . esc_html__('全部分类', 'aksrt') . '</option>';
foreach (OrderAnalytics::group_labels() as $key => $label) {
echo '<option value="' . esc_attr($key) . '">' . esc_html($label) . '</option>';
}
echo '</select></label>';
echo '<button type="submit" class="button">' . esc_html__('查询', 'aksrt') . '</button>';
echo '</form>';
echo '<span class="aksrt-po-range-label" data-aksrt-po-range-label aria-live="polite"></span>';
echo '</div>';
}
private function render_kpis(): void {
$cards = [
['paid_amount', __('支付金额', 'aksrt'), 'success'],
['net_amount', __('净收入', 'aksrt'), 'info'],
['paid_count', __('支付订单数', 'aksrt'), 'primary'],
['buyers', __('支付人数', 'aksrt'), 'primary'],
['avg_amount', __('客单价', 'aksrt'), 'warning'],
['refund_amount', __('退款金额', 'aksrt'), 'danger'],
];
echo '<div class="aksrt-po-kpis" data-aksrt-po-kpis>';
foreach ($cards as [$key, $label, $tone]) {
echo '<div class="aksrt-po-kpi aksrt-po-kpi--' . esc_attr($tone) . '" data-kpi="' . esc_attr($key) . '">'
. '<span class="aksrt-po-kpi__label">' . esc_html($label) . '</span>'
. '<span class="aksrt-po-kpi__value" data-kpi-value>—</span>'
. '<span class="aksrt-po-kpi__foot"><span class="aksrt-po-delta is-flat" data-kpi-delta>—</span>'
. '<span class="aksrt-po-kpi__meta" data-kpi-meta></span></span>'
. '</div>';
}
echo '</div>';
}
/* ------------------------------------------------------------------
* 卡片
* ----------------------------------------------------------------*/
private function chart_card(string $key, string $title, string $label, string $size): void {
NativeUi::card_open($title, '', false, 'aksrt-po-card');
echo '<div class="aksrt-po-chart aksrt-po-chart--' . esc_attr($size) . '" data-aksrt-po-chart="' . esc_attr($key) . '" role="img" aria-label="' . esc_attr($label) . '"></div>';
NativeUi::card_close();
}
private function render_funnel_card(): void {
$rows = [
['created_count', __('下单数', 'aksrt'), __('区间内创建的订单', 'aksrt')],
['conversion', __('支付转化率', 'aksrt'), __('下单后完成支付的比例', 'aksrt')],
['close_rate', __('超时关闭率', 'aksrt'), __('未支付而关闭的比例', 'aksrt')],
['refund_rate', __('退款率', 'aksrt'), __('退款金额 ÷ 支付金额', 'aksrt')],
];
NativeUi::card_open(__('转化与退款', 'aksrt'), '', false, 'aksrt-po-card');
echo '<ul class="aksrt-po-funnel" data-aksrt-po-funnel>';
foreach ($rows as [$key, $label, $hint]) {
echo '<li data-metric="' . esc_attr($key) . '"><div class="aksrt-po-funnel__head">'
. '<span class="aksrt-po-funnel__label">' . esc_html($label) . '</span>'
. '<strong class="aksrt-po-funnel__value" data-value>—</strong></div>'
. '<span class="aksrt-po-funnel__bar" aria-hidden="true"><i data-bar></i></span>'
. '<span class="aksrt-po-funnel__hint">' . esc_html($hint) . ' <span data-delta></span></span></li>';
}
echo '</ul>';
NativeUi::card_close();
}
private function render_top_card(): void {
NativeUi::card_open(__('付费内容热销 TOP 10', 'aksrt'), '', true, 'aksrt-po-card');
echo '<div class="aksrt-native-table-wrap"><table class="widefat aksrt-po-top">'
. '<caption class="screen-reader-text">' . esc_html__('付费内容热销排行', 'aksrt') . '</caption>'
. '<thead><tr><th scope="col" class="column-rank">#</th><th scope="col">' . esc_html__('文章', 'aksrt') . '</th>'
. '<th scope="col" class="column-num">' . esc_html__('销量', 'aksrt') . '</th>'
. '<th scope="col" class="column-num">' . esc_html__('支付金额', 'aksrt') . '</th>'
. '<th scope="col" class="column-share">' . esc_html__('占比', 'aksrt') . '</th></tr></thead>'
. '<tbody data-aksrt-po-top></tbody></table></div>';
NativeUi::card_close();
}
private function render_todo_card(): void {
$orders = class_exists('\AKSRT\Payment\OrderRepository') ? \AKSRT\Payment\OrderRepository::get_instance()->get_orders_status_counts() : [];
$withdraw = class_exists('\AKSRT\Payment\Withdrawal') ? \AKSRT\Payment\Withdrawal::get_instance()->get_stats() : [];
$income = class_exists('\AKSRT\Payment\Revenue') ? \AKSRT\Payment\Revenue::get_instance()->get_stats() : [];
$cards = class_exists('\AKSRT\Payment\ChargeCard') ? \AKSRT\Payment\ChargeCard::get_instance()->get_stats() : [];
$items = [
[__('待审提现', 'aksrt'), (int) ($withdraw['pending'] ?? 0), $this->page_url('aksrt_withdraw', ['status' => 'pending']), 'dashicons-money-alt', 'danger'],
[__('打款中提现', 'aksrt'), (int) ($withdraw['processing'] ?? 0), $this->page_url('aksrt_withdraw', ['status' => 'processing']), 'dashicons-update', 'warning'],
[__('待结算分成', 'aksrt'), (string) ($income['cash']['pending'] ?? '0.00'), $this->page_url('aksrt_income', ['status' => 'pending']), 'dashicons-chart-pie', 'warning'],
[__('待支付订单', 'aksrt'), (int) ($orders['pending'] ?? 0), $this->page_url('aksrt_orders', ['status' => 'pending']), 'dashicons-clock', 'info'],
[__('可用卡密', 'aksrt'), (int) ($cards['active'] ?? 0), $this->page_url('aksrt_charge_cards', ['status' => 'active']), 'dashicons-id-alt', 'neutral'],
];
NativeUi::card_open(__('待办事项', 'aksrt'), '', false, 'aksrt-po-card');
echo '<ul class="aksrt-po-todo">';
foreach ($items as [$label, $value, $url, $icon, $tone]) {
$idle = (string) $value === '0' || (string) $value === '0.00';
echo '<li><a class="aksrt-po-todo__item aksrt-po-todo__item--' . esc_attr($idle ? 'neutral' : $tone) . '" href="' . esc_url($url) . '">'
. '<span class="aksrt-po-todo__icon dashicons ' . esc_attr($icon) . '" aria-hidden="true"></span>'
. '<span class="aksrt-po-todo__label">' . esc_html($label) . '</span>'
. '<span class="aksrt-po-todo__value">' . esc_html(is_int($value) ? number_format_i18n($value) : $this->currency_symbol() . $value) . '</span>'
. '</a></li>';
}
echo '</ul>';
NativeUi::card_close();
}
private function render_shortcuts_card(): void {
NativeUi::card_open(__('快捷入口', 'aksrt'), '', false, 'aksrt-po-card');
echo '<ul class="aksrt-po-links">';
foreach (PayMenus::get_instance()->shortcuts() as $item) {
echo '<li><a href="' . esc_url($this->page_url($item['slug'])) . '">'
. '<span class="aksrt-po-links__icon dashicons ' . esc_attr($item['icon']) . '" aria-hidden="true"></span>'
. '<span class="aksrt-po-links__text"><strong>' . esc_html($item['title']) . '</strong>'
. ($item['desc'] !== '' ? '<span>' . esc_html($item['desc']) . '</span>' : '')
. '</span></a></li>';
}
echo '</ul>';
NativeUi::card_close();
}
/* ------------------------------------------------------------------
* 助手
* ----------------------------------------------------------------*/
private function currency_symbol(): string {
$paid = \AKSRT\Settings\Settings::get_paid_content_settings();
return (string) ($paid['display']['currency_symbol'] ?? '¥');
}
/** @param array<string, string> $args */
private function page_url(string $slug, array $args = []): string {
return add_query_arg(array_merge(['page' => $slug], $args), admin_url('admin.php'));
}
}
+75
View File
@@ -0,0 +1,75 @@
<?php
/**
* 支付中心概览的数据接口:GET aksrt/v1/admin/pay-overview?from=Y-m-d&to=Y-m-d&group=
*
* 仅管理员(manage_options + wp_rest nonce,见 ControllerBase::check_permission)。
* 统计口径见 \AKSRT\Payment\OrderAnalytics。
*
* @package AKSRT
*/
namespace AKSRT\Admin;
use AKSRT\Payment\OrderAnalytics;
use AKSRT\Payment\OrderAnalyticsDaily;
defined('ABSPATH') || exit;
class PayOverviewController extends ControllerBase {
private static $instance = null;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function register_routes(): void {
register_rest_route(self::API_NAMESPACE, '/admin/pay-overview', [
'methods' => 'GET',
'callback' => [$this, 'get_overview'],
'permission_callback' => [$this, 'check_permission'],
'args' => [
'from' => ['type' => 'string', 'required' => true, 'sanitize_callback' => 'sanitize_text_field'],
'to' => ['type' => 'string', 'required' => true, 'sanitize_callback' => 'sanitize_text_field'],
'group' => ['type' => 'string', 'default' => '', 'sanitize_callback' => 'sanitize_key'],
],
]);
}
public function get_overview(\WP_REST_Request $request): \WP_REST_Response {
$from = (string) $request->get_param('from');
$to = (string) $request->get_param('to');
$group = (string) $request->get_param('group');
$error = self::validate_range($from, $to);
if ($error !== '') {
return $this->error($error, 400);
}
if ($group !== '' && !isset(OrderAnalytics::group_labels()[$group])) {
return $this->error(__('商品分类无效。', 'aksrt'), 400);
}
return $this->success(['data' => OrderAnalytics::get_instance()->report($from, $to, $group)]);
}
/** 区间校验:合法日期、起止有序、不晚于今天、不超过上限天数。返回空串表示通过。 */
public static function validate_range(string $from, string $to): string {
if (!OrderAnalyticsDaily::is_date($from) || !OrderAnalyticsDaily::is_date($to)) {
return __('日期格式无效。', 'aksrt');
}
if ($from > $to) {
return __('开始日期不能晚于结束日期。', 'aksrt');
}
if ($to > current_time('Y-m-d')) {
return __('结束日期不能晚于今天。', 'aksrt');
}
if (count(OrderAnalyticsDaily::dates($from, $to)) > OrderAnalytics::MAX_RANGE_DAYS) {
/* translators: %d max days */
return sprintf(__('统计区间最长 %d 天。', 'aksrt'), OrderAnalytics::MAX_RANGE_DAYS);
}
return '';
}
}
-4
View File
@@ -60,10 +60,6 @@ class Order {
return $this->repo()->update_status($order_number, $new_status, $extra); return $this->repo()->update_status($order_number, $new_status, $extra);
} }
public function get_stats(int $user_id = 0): array {
return $this->repo()->get_stats($user_id);
}
public function has_paid_access(int $user_id, string $product_type, int $product_id): bool { public function has_paid_access(int $user_id, string $product_type, int $product_id): bool {
return $this->repo()->has_paid_access($user_id, $product_type, $product_id); return $this->repo()->has_paid_access($user_id, $product_type, $product_id);
} }
+379
View File
@@ -0,0 +1,379 @@
<?php
/**
* 支付中心概览报表:在 OrderAnalyticsDaily 的按天记录上汇总区间指标、趋势序列与结构分析。
*
* 口径(详见 OrderAnalyticsDaily):
* - 支付金额按支付时间归日,包含后来退款的订单;退款金额按退款时间归日;净收入 = 支付 − 退款;
* - 只统计站点主币种现金订单,不含积分订单与余额充值;外币订单在 excluded 中单独列出;
* - 支付转化率 / 超时关闭率按下单时间归日:区间内下单的订单里已支付 / 已关闭的占比。
*
* 金额在内部一律用「分」(整数)累加,输出时才转成元。
*
* @package AKSRT
*/
namespace AKSRT\Payment;
use AKSRT\Core\DomainModel;
defined('ABSPATH') || exit;
final class OrderAnalytics {
public const GROUP_CONTENT = 'content';
public const GROUP_VIP = 'vip';
public const GROUP_POINTS = 'points';
public const GROUP_OTHER = 'other';
public const MAX_RANGE_DAYS = 366;
/** 区间超过该天数时趋势按月聚合。 */
private const MONTHLY_THRESHOLD = 92;
private const TOP_LIMIT = 10;
private static $instance = null;
/** @var array<string,string>|null */
private ?array $method_labels = null;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
/** @return array<string, string> 分组键 => 名称(顺序即图例顺序) */
public static function group_labels(): array {
return [
self::GROUP_CONTENT => __('付费内容', 'aksrt'),
self::GROUP_VIP => __('会员', 'aksrt'),
self::GROUP_POINTS => __('购买积分', 'aksrt'),
self::GROUP_OTHER => __('其他', 'aksrt'),
];
}
public static function group_of(string $product_type): string {
if (DomainModel::is_resource_product_type($product_type)) {
return self::GROUP_CONTENT;
}
return match ($product_type) {
DomainModel::ORDER_PRODUCT_VIP => self::GROUP_VIP,
DomainModel::ORDER_PRODUCT_POINTS => self::GROUP_POINTS,
default => self::GROUP_OTHER,
};
}
/**
* 区间报表。$from / $to 为站点时区 Y-m-d(调用方已校验),$group 为空表示全部分类。
*
* @return array<string, mixed>
*/
public function report(string $from, string $to, string $group = ''): array {
$daily = OrderAnalyticsDaily::get_instance();
$records = $daily->records($from, $to);
$days = count($records);
$prev_to = OrderAnalyticsDaily::shift($from, -1);
$prev_from = OrderAnalyticsDaily::shift($from, -$days);
$previous = $daily->records($prev_from, $prev_to);
$granularity = $days > self::MONTHLY_THRESHOLD ? 'month' : 'day';
return [
'range' => ['from' => $from, 'to' => $to, 'days' => $days, 'granularity' => $granularity, 'group' => $group],
'previous_range' => ['from' => $prev_from, 'to' => $prev_to],
'totals' => $this->totals($records, $group) + ['buyers' => $this->buyers($from, $to, $group)],
'previous' => $this->totals($previous, $group) + ['buyers' => $this->buyers($prev_from, $prev_to, $group)],
'series' => $this->series($records, $group, $granularity),
'methods' => $this->methods($records, $group),
'top' => in_array($group, ['', self::GROUP_CONTENT], true) ? $this->top_products($from, $to) : [],
'excluded' => $this->excluded($from, $to),
];
}
/**
* 今日截至此刻 vs 昨日同一时段(支付侧)。
*
* @return array{today:array{amount:float,count:int},yesterday:array{amount:float,count:int},until:string}
*/
public function today_vs_yesterday(): array {
$today = current_time('Y-m-d');
$clock = current_time('H:i:s');
$yday = OrderAnalyticsDaily::shift($today, -1);
return [
'today' => $this->paid_window($today . ' 00:00:00', $today . ' ' . $clock),
'yesterday' => $this->paid_window($yday . ' 00:00:00', $yday . ' ' . $clock),
'until' => substr($clock, 0, 5),
];
}
/* ------------------------------------------------------------------
* 基于按天记录的汇总
* ----------------------------------------------------------------*/
/** @param array<string, array<string, array>> $records */
private function totals(array $records, string $group): array {
$paid = $paid_count = $refund = $refund_count = $created = $created_paid = $closed = 0;
foreach ($records as $record) {
foreach ($record['paid'] ?? [] as $type => $methods) {
if ($this->in_group((string) $type, $group)) {
foreach ($methods as $cell) {
$paid += (int) $cell[0];
$paid_count += (int) $cell[1];
}
}
}
foreach ($record['refund'] ?? [] as $type => $cell) {
if ($this->in_group((string) $type, $group)) {
$refund += (int) $cell[0];
$refund_count += (int) $cell[1];
}
}
foreach ($record['created'] ?? [] as $type => $cell) {
if ($this->in_group((string) $type, $group)) {
$created += (int) $cell[0];
$created_paid += (int) $cell[1];
$closed += (int) $cell[2];
}
}
}
return [
'paid_amount' => self::yuan($paid),
'paid_count' => $paid_count,
'refund_amount' => self::yuan($refund),
'refund_count' => $refund_count,
'net_amount' => self::yuan($paid - $refund),
'avg_amount' => $paid_count > 0 ? self::yuan((int) round($paid / $paid_count)) : 0.0,
'refund_rate' => $paid > 0 ? round($refund / $paid * 100, 1) : null,
'created_count' => $created,
'conversion' => $created > 0 ? round($created_paid / $created * 100, 1) : null,
'close_rate' => $created > 0 ? round($closed / $created * 100, 1) : null,
];
}
/**
* 趋势序列:支付金额、退款金额、净收入、支付订单数,以及按分类的支付金额。
*
* @param array<string, array<string, array>> $records
*/
private function series(array $records, string $group, string $granularity): array {
$buckets = [];
foreach ($records as $date => $record) {
$bucket = $granularity === 'month' ? substr((string) $date, 0, 7) : (string) $date;
$buckets[$bucket] ??= ['paid' => 0, 'refund' => 0, 'count' => 0, 'groups' => array_fill_keys(array_keys(self::group_labels()), 0)];
foreach ($record['paid'] ?? [] as $type => $methods) {
$type_group = self::group_of((string) $type);
foreach ($methods as $cell) {
$buckets[$bucket]['groups'][$type_group] += (int) $cell[0];
if ($this->in_group((string) $type, $group)) {
$buckets[$bucket]['paid'] += (int) $cell[0];
$buckets[$bucket]['count'] += (int) $cell[1];
}
}
}
foreach ($record['refund'] ?? [] as $type => $cell) {
if ($this->in_group((string) $type, $group)) {
$buckets[$bucket]['refund'] += (int) $cell[0];
}
}
}
$out = ['labels' => array_keys($buckets), 'paid' => [], 'refund' => [], 'net' => [], 'count' => [], 'groups' => []];
$group_data = array_fill_keys(array_keys(self::group_labels()), []);
foreach ($buckets as $bucket) {
$out['paid'][] = self::yuan($bucket['paid']);
$out['refund'][] = self::yuan($bucket['refund']);
$out['net'][] = self::yuan($bucket['paid'] - $bucket['refund']);
$out['count'][] = $bucket['count'];
foreach ($bucket['groups'] as $key => $minor) {
$group_data[$key][] = self::yuan($minor);
}
}
foreach (self::group_labels() as $key => $name) {
if (array_sum($group_data[$key]) > 0) {
$out['groups'][] = ['key' => $key, 'name' => $name, 'data' => $group_data[$key]];
}
}
return $out;
}
/**
* 支付方式占比(按支付金额降序)。
*
* @param array<string, array<string, array>> $records
* @return array<int, array{key:string,name:string,amount:float,count:int}>
*/
private function methods(array $records, string $group): array {
$sum = [];
foreach ($records as $record) {
foreach ($record['paid'] ?? [] as $type => $methods) {
if (!$this->in_group((string) $type, $group)) {
continue;
}
foreach ($methods as $method => $cell) {
$sum[$method] ??= [0, 0];
$sum[$method][0] += (int) $cell[0];
$sum[$method][1] += (int) $cell[1];
}
}
}
uasort($sum, static fn(array $a, array $b): int => $b[0] <=> $a[0]);
$out = [];
foreach ($sum as $method => $cell) {
$out[] = ['key' => (string) $method, 'name' => $this->method_label((string) $method), 'amount' => self::yuan($cell[0]), 'count' => $cell[1]];
}
return $out;
}
/* ------------------------------------------------------------------
* 需要直接查询的指标(跨天去重、明细排行、时段对比)
* ----------------------------------------------------------------*/
/** 区间内的支付人数(按用户去重;游客订单 user_id=0 不计)。 */
private function buyers(string $from, string $to, string $group): int {
global $wpdb;
[$where, $params] = $this->paid_window_where($from . ' 00:00:00', $to . ' 23:59:59', $group);
return (int) $wpdb->get_var($wpdb->prepare(
'SELECT COUNT(DISTINCT user_id) FROM ' . OrderAnalyticsDaily::table() . " WHERE {$where} AND user_id > 0",
$params
));
}
/** @return array{amount:float,count:int} */
private function paid_window(string $start, string $end): array {
global $wpdb;
[$where, $params] = $this->paid_window_where($start, $end, '');
$row = $wpdb->get_row($wpdb->prepare(
'SELECT COUNT(*) AS c, COALESCE(SUM(amount), 0) AS a FROM ' . OrderAnalyticsDaily::table() . " WHERE {$where}",
$params
), ARRAY_A);
return [
'amount' => self::yuan(OrderAnalyticsDaily::minor($row['a'] ?? 0)),
'count' => (int) ($row['c'] ?? 0),
];
}
/**
* 付费内容热销排行(按支付金额)。
*
* @return array<int, array{id:int,title:string,url:string,amount:float,count:int}>
*/
private function top_products(string $from, string $to): array {
global $wpdb;
[$where, $params] = $this->paid_window_where($from . ' 00:00:00', $to . ' 23:59:59', self::GROUP_CONTENT);
$params[] = self::TOP_LIMIT;
$rows = (array) $wpdb->get_results($wpdb->prepare(
'SELECT product_id AS id, COUNT(*) AS c, COALESCE(SUM(amount), 0) AS a FROM ' . OrderAnalyticsDaily::table()
. " WHERE {$where} AND product_id > 0 GROUP BY product_id ORDER BY a DESC, c DESC LIMIT %d",
$params
), ARRAY_A);
$ids = array_map(static fn(array $row): int => (int) $row['id'], $rows);
if ($ids !== []) {
_prime_post_caches($ids, false, false);
}
$out = [];
foreach ($rows as $row) {
$id = (int) $row['id'];
$title = (string) get_the_title($id);
$out[] = [
'id' => $id,
'title' => $title !== '' ? $title : sprintf(/* translators: %d post ID */ __('文章 #%d', 'aksrt'), $id),
'url' => (string) get_edit_post_link($id, 'raw'),
'amount' => self::yuan(OrderAnalyticsDaily::minor($row['a'] ?? 0)),
'count' => (int) ($row['c'] ?? 0),
];
}
return $out;
}
/**
* 未计入主口径的外币现金订单(按币种),供页面提示。
*
* @return array<int, array{currency:string,amount:float,count:int}>
*/
private function excluded(string $from, string $to): array {
global $wpdb;
$paid = OrderAnalyticsDaily::paid_statuses();
$in = implode(', ', array_fill(0, count($paid), '%s'));
$start = $from . ' 00:00:00';
$end = $to . ' 23:59:59';
$guard = OrderAnalyticsDaily::shift($from, -OrderAnalyticsDaily::CREATED_SLACK_DAYS) . ' 00:00:00';
$rows = (array) $wpdb->get_results($wpdb->prepare(
'SELECT UPPER(currency) AS cur, COUNT(*) AS c, COALESCE(SUM(amount), 0) AS a FROM ' . OrderAnalyticsDaily::table()
. " WHERE currency NOT IN (%s, 'points') AND payment_method <> 'points' AND product_type <> %s AND status IN ({$in})
AND created_at >= %s AND created_at <= %s AND COALESCE(paid_at, created_at) BETWEEN %s AND %s
GROUP BY cur ORDER BY c DESC",
array_merge([OrderAnalyticsDaily::CURRENCY, DomainModel::ORDER_PRODUCT_RECHARGE], $paid, [$guard, $end, $start, $end])
), ARRAY_A);
return array_map(static fn(array $row): array => [
'currency' => (string) $row['cur'],
'amount' => self::yuan(OrderAnalyticsDaily::minor($row['a'] ?? 0)),
'count' => (int) ($row['c'] ?? 0),
], $rows);
}
/**
* 按支付时间窗口的主口径 WHERE(含 created_at 索引下界)。
*
* @return array{0:string,1:array<int,mixed>}
*/
private function paid_window_where(string $start, string $end, string $group): array {
[$base, $params] = OrderAnalyticsDaily::base_where();
$paid = OrderAnalyticsDaily::paid_statuses();
$guard = OrderAnalyticsDaily::shift(substr($start, 0, 10), -OrderAnalyticsDaily::CREATED_SLACK_DAYS) . ' 00:00:00';
$where = $base . ' AND status IN (' . implode(', ', array_fill(0, count($paid), '%s')) . ')'
. ' AND created_at >= %s AND created_at <= %s AND COALESCE(paid_at, created_at) BETWEEN %s AND %s';
$params = array_merge($params, $paid, [$guard, $end, $start, $end]);
if ($group !== '') {
[$group_sql, $group_params] = $this->group_where($group);
$where .= ' AND ' . $group_sql;
$params = array_merge($params, $group_params);
}
return [$where, $params];
}
/** @return array{0:string,1:array<int,string>} */
private function group_where(string $group): array {
$content = DomainModel::resource_order_product_types();
$known = array_merge($content, [DomainModel::ORDER_PRODUCT_VIP, DomainModel::ORDER_PRODUCT_POINTS]);
$list = static fn(array $types): string => implode(', ', array_fill(0, count($types), '%s'));
return match ($group) {
self::GROUP_CONTENT => ['product_type IN (' . $list($content) . ')', $content],
self::GROUP_VIP => ['product_type = %s', [DomainModel::ORDER_PRODUCT_VIP]],
self::GROUP_POINTS => ['product_type = %s', [DomainModel::ORDER_PRODUCT_POINTS]],
default => ['product_type NOT IN (' . $list($known) . ')', $known],
};
}
/* ------------------------------------------------------------------
* 助手
* ----------------------------------------------------------------*/
private function in_group(string $product_type, string $group): bool {
return $group === '' || self::group_of($product_type) === $group;
}
private function method_label(string $method): string {
if ($this->method_labels === null) {
$this->method_labels = [
'card_pass' => __('卡密', 'aksrt'),
'free' => __('免费', 'aksrt'),
'' => __('未知', 'aksrt'),
];
if (class_exists('\AKSRT\Payment\Gateway')) {
foreach (Gateway::get_instance()->get_all() as $id => $gateway) {
$this->method_labels[(string) $id] = $gateway->get_name();
}
}
}
return $this->method_labels[$method] ?? $method;
}
private static function yuan(int $minor): float {
return round($minor / 100, 2);
}
}
+281
View File
@@ -0,0 +1,281 @@
<?php
/**
* 支付中心统计的按天明细与缓存。
*
* 每一天一条记录,金额一律用「分」(整数)保存,避免浮点累加误差:
* paid [product_type][payment_method] => [金额分, 笔数] 按支付时间归日
* refund [product_type] => [金额分, 笔数] 按退款时间归日
* created [product_type] => [下单数, 已支付数, 已关闭数] 按下单时间归日
*
* 统计口径(与 OrderAnalytics 一致):
* - 只统计站点主币种(CNY)的现金订单;积分订单与外币订单不进入主口径;
* - 不统计余额充值(预收款,消费时已计入对应订单);
* - 支付侧包含后来退款的订单(paid / refunding / refunded),退款单独记在退款当天,
* 所以历史支付金额不会因为后来的退款而改变。
*
* 缓存:今天和昨天始终实时查询;更早的日期按月存进 transient(一个月一条),
* 订单状态变化时删除受影响月份;清理 / 删除订单时整体换代。
*
* 性能:支付时间没有索引,查询额外加 created_at 下界(待支付订单最长 24 小时过期,
* 支付时间不会晚于下单 2 天),让 MySQL 走 idx_created;退款查询走 idx_status。
*
* @package AKSRT
*/
namespace AKSRT\Payment;
use AKSRT\Core\DomainModel;
defined('ABSPATH') || exit;
final class OrderAnalyticsDaily {
/** 主口径币种(订单表 currency 经 sanitize_key 存为小写)。 */
public const CURRENCY = 'cny';
/** 最近 N 天(含今天)不缓存:仍可能有回调、过期关闭等变化。 */
private const LIVE_DAYS = 2;
/** 支付时间与下单时间的最大间隔(天),用于 created_at 索引下界。 */
public const CREATED_SLACK_DAYS = 2;
private const GEN_OPTION = 'aksrt_pay_analytics_gen';
private const CACHE_TTL = 30 * DAY_IN_SECONDS;
private static $instance = null;
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
}
return self::$instance;
}
public function init(): void {
add_action('aksrt_order_status_changed', [$this, 'on_status_changed'], 10, 3);
add_action('aksrt_orders_purged', [$this, 'flush']);
}
/**
* 订单状态变化:删除该订单下单日 / 支付日 / 退款日所在月份的缓存。
*
* @param mixed $order_number
* @param mixed $status
* @param mixed $order
*/
public function on_status_changed($order_number = '', $status = '', $order = []): void {
if (!is_array($order)) {
return;
}
$cutoff = $this->cache_cutoff();
$months = [];
foreach (['created_at', 'paid_at', 'refunded_at'] as $field) {
$date = substr((string) ($order[$field] ?? ''), 0, 10);
if (self::is_date($date) && $date <= $cutoff) {
$months[substr($date, 0, 7)] = true;
}
}
foreach (array_keys($months) as $month) {
delete_transient($this->key($month));
}
}
/** 整体失效:换代后旧 key 不再被读取,随 TTL 自然过期。 */
public function flush(): void {
update_option(self::GEN_OPTION, $this->generation() + 1, false);
}
public function generation(): int {
return (int) get_option(self::GEN_OPTION, 0);
}
/** 可缓存的最后一天(Y-m-d)。 */
public function cache_cutoff(): string {
return self::shift(current_time('Y-m-d'), -self::LIVE_DAYS);
}
/**
* 区间内每天的记录(含首尾),键为 Y-m-d,按日期升序。
*
* @return array<string, array<string, array>>
*/
public function records(string $from, string $to): array {
$cutoff = $this->cache_cutoff();
$out = [];
$missing = [];
$by_month = [];
foreach (self::dates($from, $to) as $date) {
$by_month[substr($date, 0, 7)][] = $date;
}
foreach ($by_month as $month => $dates) {
$cached = $dates[0] <= $cutoff ? get_transient($this->key($month)) : false;
$cached = is_array($cached) ? $cached : [];
foreach ($dates as $date) {
if ($date <= $cutoff && isset($cached[$date])) {
$out[$date] = $cached[$date];
} else {
$missing[] = $date;
}
}
}
if ($missing !== []) {
$fresh = $this->query(min($missing), max($missing));
$store = [];
foreach ($missing as $date) {
$out[$date] = $fresh[$date] ?? self::empty_record();
if ($date <= $cutoff) {
$store[substr($date, 0, 7)][$date] = $out[$date];
}
}
foreach ($store as $month => $days) {
$cached = get_transient($this->key($month));
set_transient($this->key($month), array_merge(is_array($cached) ? $cached : [], $days), self::CACHE_TTL);
}
}
ksort($out);
return $out;
}
/* ------------------------------------------------------------------
* 查询
* ----------------------------------------------------------------*/
/** @return array<string, array<string, array>> */
private function query(string $from, string $to): array {
global $wpdb;
$table = self::table();
$start = $from . ' 00:00:00';
$end = $to . ' 23:59:59';
$guard = self::shift($from, -self::CREATED_SLACK_DAYS) . ' 00:00:00';
[$base, $base_params] = self::base_where();
$paid = self::paid_statuses();
$paid_in = implode(', ', array_fill(0, count($paid), '%s'));
$records = [];
// 支付:按支付时间归日。
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT DATE(COALESCE(paid_at, created_at)) AS d, product_type AS t, payment_method AS m,
COUNT(*) AS c, COALESCE(SUM(amount), 0) AS a
FROM {$table}
WHERE {$base} AND status IN ({$paid_in})
AND created_at >= %s AND created_at <= %s
AND COALESCE(paid_at, created_at) BETWEEN %s AND %s
GROUP BY d, t, m",
array_merge($base_params, $paid, [$guard, $end, $start, $end])
), ARRAY_A);
foreach ((array) $rows as $row) {
$date = (string) ($row['d'] ?? '');
$type = sanitize_key((string) ($row['t'] ?? ''));
$method = sanitize_key((string) ($row['m'] ?? ''));
$records[$date] ??= self::empty_record();
$cell = $records[$date]['paid'][$type][$method] ?? [0, 0];
$records[$date]['paid'][$type][$method] = [$cell[0] + self::minor($row['a'] ?? 0), $cell[1] + (int) ($row['c'] ?? 0)];
}
// 退款:按退款时间归日(缺失时回落支付 / 下单时间)。
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT DATE(COALESCE(refunded_at, paid_at, created_at)) AS d, product_type AS t,
COUNT(*) AS c, COALESCE(SUM(amount), 0) AS a
FROM {$table}
WHERE {$base} AND status = %s
AND COALESCE(refunded_at, paid_at, created_at) BETWEEN %s AND %s
GROUP BY d, t",
array_merge($base_params, [DomainModel::ORDER_STATUS_REFUNDED, $start, $end])
), ARRAY_A);
foreach ((array) $rows as $row) {
$date = (string) ($row['d'] ?? '');
$type = sanitize_key((string) ($row['t'] ?? ''));
$records[$date] ??= self::empty_record();
$cell = $records[$date]['refund'][$type] ?? [0, 0];
$records[$date]['refund'][$type] = [$cell[0] + self::minor($row['a'] ?? 0), $cell[1] + (int) ($row['c'] ?? 0)];
}
// 下单:按下单时间归日,用于支付转化率与关闭率。
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT DATE(created_at) AS d, product_type AS t, COUNT(*) AS c,
SUM(CASE WHEN status IN ({$paid_in}) THEN 1 ELSE 0 END) AS p,
SUM(CASE WHEN status = %s THEN 1 ELSE 0 END) AS x
FROM {$table}
WHERE {$base} AND created_at BETWEEN %s AND %s
GROUP BY d, t",
array_merge($paid, [DomainModel::ORDER_STATUS_CLOSED], $base_params, [$start, $end])
), ARRAY_A);
foreach ((array) $rows as $row) {
$date = (string) ($row['d'] ?? '');
$type = sanitize_key((string) ($row['t'] ?? ''));
$records[$date] ??= self::empty_record();
$cell = $records[$date]['created'][$type] ?? [0, 0, 0];
$records[$date]['created'][$type] = [
$cell[0] + (int) ($row['c'] ?? 0),
$cell[1] + (int) ($row['p'] ?? 0),
$cell[2] + (int) ($row['x'] ?? 0),
];
}
return $records;
}
/* ------------------------------------------------------------------
* 共享口径(OrderAnalytics 的区间查询也用这些)
* ----------------------------------------------------------------*/
public static function table(): string {
return \AKSRT\Core\DBMigrator::table('order');
}
/** @return array{0:string,1:array<int,string>} 主口径 WHERE 片段(主币种、非积分、非充值)。 */
public static function base_where(): array {
return [
'currency = %s AND payment_method <> %s AND product_type <> %s',
[self::CURRENCY, 'points', DomainModel::ORDER_PRODUCT_RECHARGE],
];
}
/** @return string[] 曾经支付成功的状态:支付侧统计包含后来退款的订单。 */
public static function paid_statuses(): array {
return array_values(array_unique(array_merge(
DomainModel::paid_order_statuses(),
[DomainModel::ORDER_STATUS_REFUNDED]
)));
}
/** 数据库 DECIMAL 字符串 → 分。 */
public static function minor($value): int {
return (int) round(((float) $value) * 100);
}
/** @return array{paid:array,refund:array,created:array} */
public static function empty_record(): array {
return ['paid' => [], 'refund' => [], 'created' => []];
}
public static function is_date(string $date): bool {
if (!preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $date, $m)) {
return false;
}
return checkdate((int) $m[2], (int) $m[3], (int) $m[1]);
}
/** 纯日期加减(UTC 计算,不受夏令时影响)。 */
public static function shift(string $date, int $days): string {
$base = new \DateTimeImmutable($date, new \DateTimeZone('UTC'));
return $base->modify(($days >= 0 ? '+' : '') . $days . ' days')->format('Y-m-d');
}
/** @return string[] 从 $from 到 $to 的连续日期。 */
public static function dates(string $from, string $to): array {
$dates = [];
for ($d = $from; $d <= $to; $d = self::shift($d, 1)) {
$dates[] = $d;
}
return $dates;
}
private function key(string $month): string {
return 'aksrt_pa_' . $this->generation() . '_' . str_replace('-', '', $month);
}
}
+1
View File
@@ -10,6 +10,7 @@ defined('ABSPATH') || exit;
class OrderRepository { class OrderRepository {
use OrderRepositoryCore; use OrderRepositoryCore;
use OrderRepositoryAdmin; use OrderRepositoryAdmin;
use OrderRepositoryAttempts;
private static $instance = null; private static $instance = null;
private int $status_transaction_depth = 0; private int $status_transaction_depth = 0;
+182
View File
@@ -0,0 +1,182 @@
<?php
/**
* 订单与支付渠道交易号(out_trade_no)的映射:meta 合并、按交易号反查订单、重复支付时分配新交易号。
*
* 作为 flat sibling trait 被 OrderRepository 使用(PSR-4:trait 必须与宿主类同目录)。
*
* @package AKSRT
*/
namespace AKSRT\Payment;
defined('ABSPATH') || exit;
trait OrderRepositoryAttempts {
/**
* Merge keys into the order note JSON payload (meta_payload).
*
* @param array<string,mixed> $patch
*/
public function merge_meta(string $order_number, array $patch): bool {
global $wpdb;
$order = $this->get($order_number);
if (!$order) {
return false;
}
$meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : [];
foreach ($patch as $key => $value) {
$meta[(string) $key] = $value;
}
$encoded = wp_json_encode($meta, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($encoded)) {
return false;
}
$updated = $wpdb->update(
$this->table(),
['note' => $encoded],
['order_number' => $order_number],
['%s'],
['%s']
);
return $updated !== false;
}
/**
* Resolve a local order from a provider out_trade_no (base or attempt-suffixed).
*
* The authoritative mapping is the order's own meta (provider_out_trade_no /
* provider_out_trade_nos); the prefix heuristic is only a fallback and must
* never be the sole resolver, otherwise a truncated attempt number would
* strand a real payment.
*/
public function resolve_by_out_trade_no(string $out_trade_no): ?array {
global $wpdb;
$out_trade_no = sanitize_text_field($out_trade_no);
if ($out_trade_no === '') {
return null;
}
$direct = $this->get($out_trade_no);
if ($direct) {
return $direct;
}
// Authoritative: the attempt number was recorded in the order note JSON.
$needle = '"provider_out_trade_no":"' . $out_trade_no . '"';
$row = $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM {$this->table()} WHERE note LIKE %s ORDER BY id DESC LIMIT 1",
'%' . $wpdb->esc_like($needle) . '%'
),
ARRAY_A
);
if (is_array($row)) {
return $this->expire_order_if_due($this->normalize_row($row));
}
// Superseded attempts stay in the history array.
$history_needle = '"' . $out_trade_no . '"';
$row = $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM {$this->table()}
WHERE note LIKE %s AND note LIKE %s
ORDER BY id DESC LIMIT 1",
'%' . $wpdb->esc_like('"provider_out_trade_nos"') . '%',
'%' . $wpdb->esc_like($history_needle) . '%'
),
ARRAY_A
);
if (is_array($row)) {
return $this->expire_order_if_due($this->normalize_row($row));
}
// Last resort: {order_number}P{n} prefix heuristic.
if (preg_match('/^(.+)P([0-9]{1,3})$/', $out_trade_no, $m)) {
$base = $this->get($m[1]);
if ($base) {
return $base;
}
}
return null;
}
/**
* Provider out_trade_no currently bound to this order (falls back to order_number).
*
* @param array<string,mixed> $order
*/
public static function provider_out_trade_no(array $order): string {
$meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : [];
$out = sanitize_text_field((string) ($meta['provider_out_trade_no'] ?? ''));
if ($out !== '') {
return $out;
}
return sanitize_text_field((string) ($order['order_number'] ?? ''));
}
/**
* Allocate a new provider out_trade_no for a payment attempt.
* First attempt uses bare order_number; later attempts append Pn (WeChat limit 32).
*
* @return array{out_trade_no:string,attempt:int,previous:string}|null
*/
public function allocate_payment_attempt(string $order_number): ?array {
$order = $this->get($order_number);
if (!$order) {
return null;
}
$meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : [];
$previous = sanitize_text_field((string) ($meta['provider_out_trade_no'] ?? ''));
$history = is_array($meta['provider_out_trade_nos'] ?? null) ? $meta['provider_out_trade_nos'] : [];
// First provider submission keeps the bare order number.
// Re-pay (provider_out_trade_no already set) always mints a new suffix so
// WeChat/Alipay/Epay accept the order when amount/subject/notify changed.
if ($previous === '') {
$attempt = 1;
$out = $order_number;
} else {
$attempt = max(1, (int) ($meta['pay_attempt'] ?? 1)) + 1;
$suffix = 'P' . $attempt;
$base = $order_number;
if (strlen($base) + strlen($suffix) > 32) {
$base = substr($base, 0, 32 - strlen($suffix));
}
$out = $base . $suffix;
if ($previous !== '' && !in_array($previous, $history, true)) {
$history[] = $previous;
}
}
if (!in_array($out, $history, true)) {
$history[] = $out;
}
if (count($history) > 20) {
$history = array_slice($history, -20);
}
$ok = $this->merge_meta($order_number, [
'pay_attempt' => $attempt,
'provider_out_trade_no' => $out,
'provider_out_trade_nos' => array_values($history),
]);
if (!$ok) {
return null;
}
return [
'out_trade_no' => $out,
'attempt' => $attempt,
'previous' => $previous,
];
}
}
+11 -219
View File
@@ -641,7 +641,12 @@ trait OrderRepositoryCore {
AND cancelled_at < %s", AND cancelled_at < %s",
$cutoff $cutoff
)); ));
return $deleted === false ? 0 : (int) $deleted; $deleted = $deleted === false ? 0 : (int) $deleted;
if ($deleted > 0) {
/** 订单被物理删除:统计缓存需整体失效。 */
do_action('aksrt_orders_purged');
}
return $deleted;
} }
@@ -656,51 +661,15 @@ trait OrderRepositoryCore {
return false; return false;
} }
$ok = $wpdb->delete($this->table(), ['order_number' => $order_number], ['%s']); $ok = $wpdb->delete($this->table(), ['order_number' => $order_number], ['%s']);
return $ok !== false && $ok > 0; $ok = $ok !== false && $ok > 0;
if ($ok) {
do_action('aksrt_orders_purged');
}
return $ok;
} }
/**
* 获取订单统计
*/
public function get_stats(int $user_id = 0): array {
global $wpdb;
$this->expire_pending_orders($user_id > 0 ? $user_id : null);
$where = "WHERE status IN ('pending', 'paid', 'refunding', 'refunded', 'closed')";
$params = [];
if ($user_id > 0) {
$where .= ' AND user_id = %d';
$params[] = $user_id;
}
$sql = "SELECT
COUNT(*) as total,
SUM(CASE WHEN status = 'pending' THEN 1 ELSE 0 END) as pending,
SUM(CASE WHEN status IN ('paid','refunding') THEN 1 ELSE 0 END) as paid,
SUM(CASE WHEN status = 'refunded' THEN 1 ELSE 0 END) as refunded,
SUM(CASE WHEN status = 'closed' THEN 1 ELSE 0 END) as closed,
SUM(CASE WHEN status IN ('paid','refunding') THEN amount ELSE 0 END) as revenue
FROM {$this->table()} {$where}";
if (!empty($params)) {
$row = $wpdb->get_row($wpdb->prepare($sql, $params), ARRAY_A);
} else {
$row = $wpdb->get_row($sql, ARRAY_A);
}
return [
'total' => (int) ($row['total'] ?? 0),
'pending' => (int) ($row['pending'] ?? 0),
'paid' => (int) ($row['paid'] ?? 0),
'refunded' => (int) ($row['refunded'] ?? 0),
'closed' => (int) ($row['closed'] ?? 0),
'revenue' => $this->cash_decimal((string) ($row['revenue'] ?? '0')),
];
}
public function dispatch_managed_status_events(): void { public function dispatch_managed_status_events(): void {
$events = $this->status_after_commit_events; $events = $this->status_after_commit_events;
$this->status_after_commit_events = []; $this->status_after_commit_events = [];
@@ -850,16 +819,6 @@ trait OrderRepositoryCore {
} }
} }
private function cash_decimal(string $value): string {
try {
return \AKSRT\Ledger\AssetAmount::balance($value)->decimal();
} catch (\InvalidArgumentException $e) {
return '0.00';
}
}
private function find_latest_success_order(int $user_id, string $product_type, int $product_id = 0, bool $ignore_product_id = false): ?array { private function find_latest_success_order(int $user_id, string $product_type, int $product_id = 0, bool $ignore_product_id = false): ?array {
global $wpdb; global $wpdb;
@@ -913,171 +872,4 @@ trait OrderRepositoryCore {
return \AKSRT\Finance\BusinessNumber::allocate(); return \AKSRT\Finance\BusinessNumber::allocate();
} }
/**
* Merge keys into the order note JSON payload (meta_payload).
*
* @param array<string,mixed> $patch
*/
public function merge_meta(string $order_number, array $patch): bool {
global $wpdb;
$order = $this->get($order_number);
if (!$order) {
return false;
}
$meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : [];
foreach ($patch as $key => $value) {
$meta[(string) $key] = $value;
}
$encoded = wp_json_encode($meta, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if (!is_string($encoded)) {
return false;
}
$updated = $wpdb->update(
$this->table(),
['note' => $encoded],
['order_number' => $order_number],
['%s'],
['%s']
);
return $updated !== false;
}
/**
* Resolve a local order from a provider out_trade_no (base or attempt-suffixed).
*
* The authoritative mapping is the order's own meta (provider_out_trade_no /
* provider_out_trade_nos); the prefix heuristic is only a fallback and must
* never be the sole resolver, otherwise a truncated attempt number would
* strand a real payment.
*/
public function resolve_by_out_trade_no(string $out_trade_no): ?array {
global $wpdb;
$out_trade_no = sanitize_text_field($out_trade_no);
if ($out_trade_no === '') {
return null;
}
$direct = $this->get($out_trade_no);
if ($direct) {
return $direct;
}
// Authoritative: the attempt number was recorded in the order note JSON.
$needle = '"provider_out_trade_no":"' . $out_trade_no . '"';
$row = $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM {$this->table()} WHERE note LIKE %s ORDER BY id DESC LIMIT 1",
'%' . $wpdb->esc_like($needle) . '%'
),
ARRAY_A
);
if (is_array($row)) {
return $this->expire_order_if_due($this->normalize_row($row));
}
// Superseded attempts stay in the history array.
$history_needle = '"' . $out_trade_no . '"';
$row = $wpdb->get_row(
$wpdb->prepare(
"SELECT * FROM {$this->table()}
WHERE note LIKE %s AND note LIKE %s
ORDER BY id DESC LIMIT 1",
'%' . $wpdb->esc_like('"provider_out_trade_nos"') . '%',
'%' . $wpdb->esc_like($history_needle) . '%'
),
ARRAY_A
);
if (is_array($row)) {
return $this->expire_order_if_due($this->normalize_row($row));
}
// Last resort: {order_number}P{n} prefix heuristic.
if (preg_match('/^(.+)P([0-9]{1,3})$/', $out_trade_no, $m)) {
$base = $this->get($m[1]);
if ($base) {
return $base;
}
}
return null;
}
/**
* Provider out_trade_no currently bound to this order (falls back to order_number).
*
* @param array<string,mixed> $order
*/
public static function provider_out_trade_no(array $order): string {
$meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : [];
$out = sanitize_text_field((string) ($meta['provider_out_trade_no'] ?? ''));
if ($out !== '') {
return $out;
}
return sanitize_text_field((string) ($order['order_number'] ?? ''));
}
/**
* Allocate a new provider out_trade_no for a payment attempt.
* First attempt uses bare order_number; later attempts append Pn (WeChat limit 32).
*
* @return array{out_trade_no:string,attempt:int,previous:string}|null
*/
public function allocate_payment_attempt(string $order_number): ?array {
$order = $this->get($order_number);
if (!$order) {
return null;
}
$meta = is_array($order['meta_payload'] ?? null) ? $order['meta_payload'] : [];
$previous = sanitize_text_field((string) ($meta['provider_out_trade_no'] ?? ''));
$history = is_array($meta['provider_out_trade_nos'] ?? null) ? $meta['provider_out_trade_nos'] : [];
// First provider submission keeps the bare order number.
// Re-pay (provider_out_trade_no already set) always mints a new suffix so
// WeChat/Alipay/Epay accept the order when amount/subject/notify changed.
if ($previous === '') {
$attempt = 1;
$out = $order_number;
} else {
$attempt = max(1, (int) ($meta['pay_attempt'] ?? 1)) + 1;
$suffix = 'P' . $attempt;
$base = $order_number;
if (strlen($base) + strlen($suffix) > 32) {
$base = substr($base, 0, 32 - strlen($suffix));
}
$out = $base . $suffix;
if ($previous !== '' && !in_array($previous, $history, true)) {
$history[] = $previous;
}
}
if (!in_array($out, $history, true)) {
$history[] = $out;
}
if (count($history) > 20) {
$history = array_slice($history, -20);
}
$ok = $this->merge_meta($order_number, [
'pay_attempt' => $attempt,
'provider_out_trade_no' => $out,
'provider_out_trade_nos' => array_values($history),
]);
if (!$ok) {
return null;
}
return [
'out_trade_no' => $out,
'attempt' => $attempt,
'previous' => $previous,
];
}
} }
+29
View File
@@ -0,0 +1,29 @@
/**
* 打包支付中心概览用的 ECharts 子集。
*
* cd assets/admin && npm run build:pay-charts
*
* echarts 是 assets/admin 的 devDependency;产物入库,部署时无需安装 node 依赖。
*/
import { createRequire } from 'node:module';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const adminDir = path.join(repoRoot, 'assets/admin');
const require = createRequire(path.join(adminDir, 'package.json'));
const { build } = require('esbuild');
const outfile = path.join(repoRoot, 'assets/js/runtime/libs/echarts-pay.min.js');
await build({
entryPoints: [path.join(repoRoot, 'tools/echarts-pay-overview.entry.mjs')],
outfile,
bundle: true,
format: 'iife',
minify: true,
target: ['es2018'],
legalComments: 'inline',
nodePaths: [path.join(adminDir, 'node_modules')],
logLevel: 'info',
});
+32
View File
@@ -0,0 +1,32 @@
/**
* 支付中心概览专用的 ECharts 按需入口(由 tools/build-pay-charts.mjs 打包)。
*
* 只注册概览页用到的图表与组件,打包为 IIFE 并挂到 window.echarts。
* 产物:assets/js/runtime/libs/echarts-pay.min.js(入库,后台只在 aksrt_pay 页入队)。
*/
import * as echarts from 'echarts/core';
import { LineChart, BarChart, PieChart } from 'echarts/charts';
import {
GridComponent,
TooltipComponent,
LegendComponent,
DataZoomComponent,
AxisPointerComponent,
AriaComponent,
} from 'echarts/components';
import { SVGRenderer } from 'echarts/renderers';
echarts.use([
LineChart,
BarChart,
PieChart,
GridComponent,
TooltipComponent,
LegendComponent,
DataZoomComponent,
AxisPointerComponent,
AriaComponent,
SVGRenderer,
]);
window.echarts = echarts;