chore: 同步最新改动(admin/superadmin、api、小程序页面与配置)

Made-with: Cursor
This commit is contained in:
卡若
2026-03-17 13:50:22 +08:00
parent a0aeb844d1
commit b394691c91
19 changed files with 5260 additions and 4925 deletions

450
admin/package-lock.json generated
View File

@@ -59,9 +59,9 @@
}
},
"node_modules/@babel/parser": {
"version": "7.29.0",
"resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.0.tgz",
"integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
"version": "7.29.2",
"resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.2.tgz",
"integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
"license": "MIT",
"dependencies": {
"@babel/types": "^7.29.0"
@@ -87,12 +87,12 @@
}
},
"node_modules/@ctrl/tinycolor": {
"version": "3.6.1",
"resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
"integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==",
"version": "4.2.0",
"resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz",
"integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==",
"license": "MIT",
"engines": {
"node": ">=10"
"node": ">=14"
}
},
"node_modules/@element-plus/icons-vue": {
@@ -496,28 +496,28 @@
}
},
"node_modules/@floating-ui/core": {
"version": "1.7.4",
"resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.4.tgz",
"integrity": "sha512-C3HlIdsBxszvm5McXlB8PeOEWfBhcGBTZGkGlWc2U0KFY5IwG5OQEuQ8rq52DZmcHDlPLd+YFBK+cZcytwIFWg==",
"version": "1.7.5",
"resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.5.tgz",
"integrity": "sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.10"
"@floating-ui/utils": "^0.2.11"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.7.5",
"resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.5.tgz",
"integrity": "sha512-N0bD2kIPInNHUHehXhMke1rBGs1dwqvC9O9KYMyyjK7iXt7GAhnro7UlcuYcGdS/yYOlq0MAVgrow8IbWJwyqg==",
"version": "1.7.6",
"resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.6.tgz",
"integrity": "sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.7.4",
"@floating-ui/utils": "^0.2.10"
"@floating-ui/core": "^1.7.5",
"@floating-ui/utils": "^0.2.11"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.10",
"resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.10.tgz",
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
"version": "0.2.11",
"resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.11.tgz",
"integrity": "sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==",
"license": "MIT"
},
"node_modules/@jridgewell/sourcemap-codec": {
@@ -909,9 +909,9 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.57.1",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz",
"integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==",
"version": "4.59.0",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
"integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
"cpu": [
"arm"
],
@@ -923,9 +923,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.57.1",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz",
"integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==",
"version": "4.59.0",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
"integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
"cpu": [
"arm64"
],
@@ -937,9 +937,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.57.1",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz",
"integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==",
"version": "4.59.0",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
"integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
"cpu": [
"arm64"
],
@@ -951,9 +951,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.57.1",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz",
"integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==",
"version": "4.59.0",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
"integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
"cpu": [
"x64"
],
@@ -965,9 +965,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.57.1",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz",
"integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==",
"version": "4.59.0",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
"integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
"cpu": [
"arm64"
],
@@ -979,9 +979,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.57.1",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz",
"integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==",
"version": "4.59.0",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
"integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
"cpu": [
"x64"
],
@@ -993,9 +993,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.57.1",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz",
"integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==",
"version": "4.59.0",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
"integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
"cpu": [
"arm"
],
@@ -1007,9 +1007,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.57.1",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz",
"integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==",
"version": "4.59.0",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
"integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
"cpu": [
"arm"
],
@@ -1021,9 +1021,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.57.1",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz",
"integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==",
"version": "4.59.0",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
"integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
"cpu": [
"arm64"
],
@@ -1035,9 +1035,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.57.1",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz",
"integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==",
"version": "4.59.0",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
"integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
"cpu": [
"arm64"
],
@@ -1049,9 +1049,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.57.1",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz",
"integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==",
"version": "4.59.0",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
"integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
"cpu": [
"loong64"
],
@@ -1063,9 +1063,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
"version": "4.57.1",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz",
"integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==",
"version": "4.59.0",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
"integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
"cpu": [
"loong64"
],
@@ -1077,9 +1077,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.57.1",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz",
"integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==",
"version": "4.59.0",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
"integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
"cpu": [
"ppc64"
],
@@ -1091,9 +1091,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
"version": "4.57.1",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz",
"integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==",
"version": "4.59.0",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
"integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
"cpu": [
"ppc64"
],
@@ -1105,9 +1105,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.57.1",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz",
"integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==",
"version": "4.59.0",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
"integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
"cpu": [
"riscv64"
],
@@ -1119,9 +1119,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.57.1",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz",
"integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==",
"version": "4.59.0",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
"integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
"cpu": [
"riscv64"
],
@@ -1133,9 +1133,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.57.1",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz",
"integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==",
"version": "4.59.0",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
"integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
"cpu": [
"s390x"
],
@@ -1147,9 +1147,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.57.1",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz",
"integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==",
"version": "4.59.0",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
"integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
"cpu": [
"x64"
],
@@ -1161,9 +1161,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.57.1",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz",
"integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==",
"version": "4.59.0",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
"integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
"cpu": [
"x64"
],
@@ -1175,9 +1175,9 @@
]
},
"node_modules/@rollup/rollup-openbsd-x64": {
"version": "4.57.1",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz",
"integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==",
"version": "4.59.0",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
"integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
"cpu": [
"x64"
],
@@ -1189,9 +1189,9 @@
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.57.1",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz",
"integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==",
"version": "4.59.0",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
"integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
"cpu": [
"arm64"
],
@@ -1203,9 +1203,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.57.1",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz",
"integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==",
"version": "4.59.0",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
"integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
"cpu": [
"arm64"
],
@@ -1217,9 +1217,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.57.1",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz",
"integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==",
"version": "4.59.0",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
"integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
"cpu": [
"ia32"
],
@@ -1231,9 +1231,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.57.1",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz",
"integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==",
"version": "4.59.0",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
"integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
"cpu": [
"x64"
],
@@ -1245,9 +1245,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.57.1",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz",
"integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==",
"version": "4.59.0",
"resolved": "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
"integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
"cpu": [
"x64"
],
@@ -1266,9 +1266,9 @@
"license": "MIT"
},
"node_modules/@types/lodash": {
"version": "4.17.23",
"resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.23.tgz",
"integrity": "sha512-RDvF6wTulMPjrNdCoYRC8gNR880JNGT8uB+REUpC2Ns4pRqQJhGz90wh7rgdXDPpCczF3VGktDuFGVnz8zP7HA==",
"version": "4.17.24",
"resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.24.tgz",
"integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==",
"license": "MIT"
},
"node_modules/@types/lodash-es": {
@@ -1330,53 +1330,53 @@
}
},
"node_modules/@vue/compiler-core": {
"version": "3.5.28",
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.28.tgz",
"integrity": "sha512-kviccYxTgoE8n6OCw96BNdYlBg2GOWfBuOW4Vqwrt7mSKWKwFVvI8egdTltqRgITGPsTFYtKYfxIG8ptX2PJHQ==",
"version": "3.5.30",
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.30.tgz",
"integrity": "sha512-s3DfdZkcu/qExZ+td75015ljzHc6vE+30cFMGRPROYjqkroYI5NV2X1yAMX9UeyBNWB9MxCfPcsjpLS11nzkkw==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.29.0",
"@vue/shared": "3.5.28",
"@vue/shared": "3.5.30",
"entities": "^7.0.1",
"estree-walker": "^2.0.2",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-dom": {
"version": "3.5.28",
"resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.28.tgz",
"integrity": "sha512-/1ZepxAb159jKR1btkefDP+J2xuWL5V3WtleRmxaT+K2Aqiek/Ab/+Ebrw2pPj0sdHO8ViAyyJWfhXXOP/+LQA==",
"version": "3.5.30",
"resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.30.tgz",
"integrity": "sha512-eCFYESUEVYHhiMuK4SQTldO3RYxyMR/UQL4KdGD1Yrkfdx4m/HYuZ9jSfPdA+nWJY34VWndiYdW/wZXyiPEB9g==",
"license": "MIT",
"dependencies": {
"@vue/compiler-core": "3.5.28",
"@vue/shared": "3.5.28"
"@vue/compiler-core": "3.5.30",
"@vue/shared": "3.5.30"
}
},
"node_modules/@vue/compiler-sfc": {
"version": "3.5.28",
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.28.tgz",
"integrity": "sha512-6TnKMiNkd6u6VeVDhZn/07KhEZuBSn43Wd2No5zaP5s3xm8IqFTHBj84HJah4UepSUJTro5SoqqlOY22FKY96g==",
"version": "3.5.30",
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.30.tgz",
"integrity": "sha512-LqmFPDn89dtU9vI3wHJnwaV6GfTRD87AjWpTWpyrdVOObVtjIuSeZr181z5C4PmVx/V3j2p+0f7edFKGRMpQ5A==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.29.0",
"@vue/compiler-core": "3.5.28",
"@vue/compiler-dom": "3.5.28",
"@vue/compiler-ssr": "3.5.28",
"@vue/shared": "3.5.28",
"@vue/compiler-core": "3.5.30",
"@vue/compiler-dom": "3.5.30",
"@vue/compiler-ssr": "3.5.30",
"@vue/shared": "3.5.30",
"estree-walker": "^2.0.2",
"magic-string": "^0.30.21",
"postcss": "^8.5.6",
"postcss": "^8.5.8",
"source-map-js": "^1.2.1"
}
},
"node_modules/@vue/compiler-ssr": {
"version": "3.5.28",
"resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.28.tgz",
"integrity": "sha512-JCq//9w1qmC6UGLWJX7RXzrGpKkroubey/ZFqTpvEIDJEKGgntuDMqkuWiZvzTzTA5h2qZvFBFHY7fAAa9475g==",
"version": "3.5.30",
"resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.30.tgz",
"integrity": "sha512-NsYK6OMTnx109PSL2IAyf62JP6EUdk4Dmj6AkWcJGBvN0dQoMYtVekAmdqgTtWQgEJo+Okstbf/1p7qZr5H+bA==",
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.5.28",
"@vue/shared": "3.5.28"
"@vue/compiler-dom": "3.5.30",
"@vue/shared": "3.5.30"
}
},
"node_modules/@vue/compiler-vue2": {
@@ -1422,53 +1422,53 @@
}
},
"node_modules/@vue/reactivity": {
"version": "3.5.28",
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.28.tgz",
"integrity": "sha512-gr5hEsxvn+RNyu9/9o1WtdYdwDjg5FgjUSBEkZWqgTKlo/fvwZ2+8W6AfKsc9YN2k/+iHYdS9vZYAhpi10kNaw==",
"version": "3.5.30",
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.30.tgz",
"integrity": "sha512-179YNgKATuwj9gB+66snskRDOitDiuOZqkYia7mHKJaidOMo/WJxHKF8DuGc4V4XbYTJANlfEKb0yxTQotnx4Q==",
"license": "MIT",
"dependencies": {
"@vue/shared": "3.5.28"
"@vue/shared": "3.5.30"
}
},
"node_modules/@vue/runtime-core": {
"version": "3.5.28",
"resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.28.tgz",
"integrity": "sha512-POVHTdbgnrBBIpnbYU4y7pOMNlPn2QVxVzkvEA2pEgvzbelQq4ZOUxbp2oiyo+BOtiYlm8Q44wShHJoBvDPAjQ==",
"version": "3.5.30",
"resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.30.tgz",
"integrity": "sha512-e0Z+8PQsUTdwV8TtEsLzUM7SzC7lQwYKePydb7K2ZnmS6jjND+WJXkmmfh/swYzRyfP1EY3fpdesyYoymCzYfg==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.5.28",
"@vue/shared": "3.5.28"
"@vue/reactivity": "3.5.30",
"@vue/shared": "3.5.30"
}
},
"node_modules/@vue/runtime-dom": {
"version": "3.5.28",
"resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.28.tgz",
"integrity": "sha512-4SXxSF8SXYMuhAIkT+eBRqOkWEfPu6nhccrzrkioA6l0boiq7sp18HCOov9qWJA5HML61kW8p/cB4MmBiG9dSA==",
"version": "3.5.30",
"resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.30.tgz",
"integrity": "sha512-2UIGakjU4WSQ0T4iwDEW0W7vQj6n7AFn7taqZ9Cvm0Q/RA2FFOziLESrDL4GmtI1wV3jXg5nMoJSYO66egDUBw==",
"license": "MIT",
"dependencies": {
"@vue/reactivity": "3.5.28",
"@vue/runtime-core": "3.5.28",
"@vue/shared": "3.5.28",
"@vue/reactivity": "3.5.30",
"@vue/runtime-core": "3.5.30",
"@vue/shared": "3.5.30",
"csstype": "^3.2.3"
}
},
"node_modules/@vue/server-renderer": {
"version": "3.5.28",
"resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.28.tgz",
"integrity": "sha512-pf+5ECKGj8fX95bNincbzJ6yp6nyzuLDhYZCeFxUNp8EBrQpPpQaLX3nNCp49+UbgbPun3CeVE+5CXVV1Xydfg==",
"version": "3.5.30",
"resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.30.tgz",
"integrity": "sha512-v+R34icapydRwbZRD0sXwtHqrQJv38JuMB4JxbOxd8NEpGLny7cncMp53W9UH/zo4j8eDHjQ1dEJXwzFQknjtQ==",
"license": "MIT",
"dependencies": {
"@vue/compiler-ssr": "3.5.28",
"@vue/shared": "3.5.28"
"@vue/compiler-ssr": "3.5.30",
"@vue/shared": "3.5.30"
},
"peerDependencies": {
"vue": "3.5.28"
"vue": "3.5.30"
}
},
"node_modules/@vue/shared": {
"version": "3.5.28",
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.28.tgz",
"integrity": "sha512-cfWa1fCGBxrvaHRhvV3Is0MgmrbSCxYTXCSCau2I0a1Xw1N1pHAvkWCiXPRAqjvToILvguNyEwjevUqAuBQWvQ==",
"version": "3.5.30",
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.30.tgz",
"integrity": "sha512-YXgQ7JjaO18NeK2K9VTbDHaFy62WrObMa6XERNfNOkAhD1F1oDSf3ZJ7K6GqabZ0BvSDHajp8qfS5Sa2I9n8uQ==",
"license": "MIT"
},
"node_modules/@vue/tsconfig": {
@@ -1479,45 +1479,45 @@
"license": "MIT"
},
"node_modules/@vueuse/core": {
"version": "10.11.1",
"resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-10.11.1.tgz",
"integrity": "sha512-guoy26JQktXPcz+0n3GukWIy/JDNKti9v6VEMu6kV2sYBsWuGiTU8OWdg+ADfUbHg3/3DlqySDe7JmdHrktiww==",
"version": "12.0.0",
"resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-12.0.0.tgz",
"integrity": "sha512-C12RukhXiJCbx4MGhjmd/gH52TjJsc3G0E0kQj/kb19H3Nt6n1CA4DRWuTdWWcaFRdlTe0npWDS942mvacvNBw==",
"license": "MIT",
"dependencies": {
"@types/web-bluetooth": "^0.0.20",
"@vueuse/metadata": "10.11.1",
"@vueuse/shared": "10.11.1",
"vue-demi": ">=0.14.8"
"@vueuse/metadata": "12.0.0",
"@vueuse/shared": "12.0.0",
"vue": "^3.5.13"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/metadata": {
"version": "10.11.1",
"resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-10.11.1.tgz",
"integrity": "sha512-IGa5FXd003Ug1qAZmyE8wF3sJ81xGLSqTqtQ6jaVfkeZ4i5kS2mwQF61yhVqojRnenVew5PldLyRgvdl4YYuSw==",
"version": "12.0.0",
"resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-12.0.0.tgz",
"integrity": "sha512-Yzimd1D3sjxTDOlF05HekU5aSGdKjxhuhRFHA7gDWLn57PRbBIh+SF5NmjhJ0WRgF3my7T8LBucyxdFJjIfRJQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared": {
"version": "10.11.1",
"resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-10.11.1.tgz",
"integrity": "sha512-LHpC8711VFZlDaYUXEBbFBCQ7GS3dVU9mjOhhMhXP6txTV4EhYQg/KGnQuvt/sPAtoUKq7VVUnL6mVtFoL42sA==",
"version": "12.0.0",
"resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-12.0.0.tgz",
"integrity": "sha512-3i6qtcq2PIio5i/vVYidkkcgvmTjCqrf26u+Fd4LhnbBmIT6FN8y6q/GJERp8lfcB9zVEfjdV0Br0443qZuJpw==",
"license": "MIT",
"dependencies": {
"vue-demi": ">=0.14.8"
"vue": "^3.5.13"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"version": "8.16.0",
"resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.16.0.tgz",
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"dev": true,
"license": "MIT",
"bin": {
@@ -1574,9 +1574,9 @@
"license": "MIT"
},
"node_modules/axios": {
"version": "1.13.5",
"resolved": "https://registry.npmmirror.com/axios/-/axios-1.13.5.tgz",
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
"version": "1.13.6",
"resolved": "https://registry.npmmirror.com/axios/-/axios-1.13.6.tgz",
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
@@ -1682,9 +1682,9 @@
"license": "MIT"
},
"node_modules/dayjs": {
"version": "1.11.19",
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.19.tgz",
"integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
"version": "1.11.20",
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.20.tgz",
"integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==",
"license": "MIT"
},
"node_modules/de-indent": {
@@ -1757,18 +1757,18 @@
}
},
"node_modules/element-plus": {
"version": "2.13.2",
"resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.13.2.tgz",
"integrity": "sha512-Zjzm1NnFXGhV4LYZ6Ze9skPlYi2B4KAmN18FL63A3PZcjhDfroHwhtM6RE8BonlOPHXUnPQynH0BgaoEfvhrGw==",
"version": "2.13.5",
"resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.13.5.tgz",
"integrity": "sha512-dmY24fhSREfZN/PuUt0YZigMso7wWzl+B5o+YKNN15kQIn/0hzamsPU+ebj9SES0IbUqsLX1wkrzYmzU8VrVOQ==",
"license": "MIT",
"dependencies": {
"@ctrl/tinycolor": "^3.4.1",
"@ctrl/tinycolor": "^4.2.0",
"@element-plus/icons-vue": "^2.3.2",
"@floating-ui/dom": "^1.0.1",
"@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7",
"@types/lodash": "^4.17.20",
"@types/lodash-es": "^4.17.12",
"@vueuse/core": "^10.11.0",
"@vueuse/core": "12.0.0",
"async-validator": "^4.2.5",
"dayjs": "^1.11.19",
"lodash": "^4.17.23",
@@ -2115,9 +2115,9 @@
}
},
"node_modules/immutable": {
"version": "5.1.4",
"resolved": "https://registry.npmmirror.com/immutable/-/immutable-5.1.4.tgz",
"integrity": "sha512-p6u1bG3YSnINT5RQmx/yRZBpenIl30kVxkTLDyHLIMk0gict704Q9n+thfDI7lTRm9vXdDYutVzXhzcThxTnXA==",
"version": "5.1.5",
"resolved": "https://registry.npmmirror.com/immutable/-/immutable-5.1.5.tgz",
"integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==",
"dev": true,
"license": "MIT"
},
@@ -2313,13 +2313,13 @@
}
},
"node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"version": "9.0.9",
"resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz",
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
"brace-expansion": "^2.0.2"
},
"engines": {
"node": ">=16 || 14 >=14.17"
@@ -2329,16 +2329,16 @@
}
},
"node_modules/mlly": {
"version": "1.8.0",
"resolved": "https://registry.npmmirror.com/mlly/-/mlly-1.8.0.tgz",
"integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==",
"version": "1.8.1",
"resolved": "https://registry.npmmirror.com/mlly/-/mlly-1.8.1.tgz",
"integrity": "sha512-SnL6sNutTwRWWR/vcmCYHSADjiEesp5TGQQ0pXyLhW5IoeibRlF/CbSLailbB3CNqJUk9cVJ9dUDnbD7GrcHBQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"acorn": "^8.15.0",
"acorn": "^8.16.0",
"pathe": "^2.0.3",
"pkg-types": "^1.3.1",
"ufo": "^1.6.1"
"ufo": "^1.6.3"
}
},
"node_modules/ms": {
@@ -2472,9 +2472,9 @@
}
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"version": "8.5.8",
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.8.tgz",
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
"funding": [
{
"type": "opencollective",
@@ -2596,9 +2596,9 @@
}
},
"node_modules/rollup": {
"version": "4.57.1",
"resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.57.1.tgz",
"integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==",
"version": "4.59.0",
"resolved": "https://registry.npmmirror.com/rollup/-/rollup-4.59.0.tgz",
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2612,31 +2612,31 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.57.1",
"@rollup/rollup-android-arm64": "4.57.1",
"@rollup/rollup-darwin-arm64": "4.57.1",
"@rollup/rollup-darwin-x64": "4.57.1",
"@rollup/rollup-freebsd-arm64": "4.57.1",
"@rollup/rollup-freebsd-x64": "4.57.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.57.1",
"@rollup/rollup-linux-arm-musleabihf": "4.57.1",
"@rollup/rollup-linux-arm64-gnu": "4.57.1",
"@rollup/rollup-linux-arm64-musl": "4.57.1",
"@rollup/rollup-linux-loong64-gnu": "4.57.1",
"@rollup/rollup-linux-loong64-musl": "4.57.1",
"@rollup/rollup-linux-ppc64-gnu": "4.57.1",
"@rollup/rollup-linux-ppc64-musl": "4.57.1",
"@rollup/rollup-linux-riscv64-gnu": "4.57.1",
"@rollup/rollup-linux-riscv64-musl": "4.57.1",
"@rollup/rollup-linux-s390x-gnu": "4.57.1",
"@rollup/rollup-linux-x64-gnu": "4.57.1",
"@rollup/rollup-linux-x64-musl": "4.57.1",
"@rollup/rollup-openbsd-x64": "4.57.1",
"@rollup/rollup-openharmony-arm64": "4.57.1",
"@rollup/rollup-win32-arm64-msvc": "4.57.1",
"@rollup/rollup-win32-ia32-msvc": "4.57.1",
"@rollup/rollup-win32-x64-gnu": "4.57.1",
"@rollup/rollup-win32-x64-msvc": "4.57.1",
"@rollup/rollup-android-arm-eabi": "4.59.0",
"@rollup/rollup-android-arm64": "4.59.0",
"@rollup/rollup-darwin-arm64": "4.59.0",
"@rollup/rollup-darwin-x64": "4.59.0",
"@rollup/rollup-freebsd-arm64": "4.59.0",
"@rollup/rollup-freebsd-x64": "4.59.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
"@rollup/rollup-linux-arm-musleabihf": "4.59.0",
"@rollup/rollup-linux-arm64-gnu": "4.59.0",
"@rollup/rollup-linux-arm64-musl": "4.59.0",
"@rollup/rollup-linux-loong64-gnu": "4.59.0",
"@rollup/rollup-linux-loong64-musl": "4.59.0",
"@rollup/rollup-linux-ppc64-gnu": "4.59.0",
"@rollup/rollup-linux-ppc64-musl": "4.59.0",
"@rollup/rollup-linux-riscv64-gnu": "4.59.0",
"@rollup/rollup-linux-riscv64-musl": "4.59.0",
"@rollup/rollup-linux-s390x-gnu": "4.59.0",
"@rollup/rollup-linux-x64-gnu": "4.59.0",
"@rollup/rollup-linux-x64-musl": "4.59.0",
"@rollup/rollup-openbsd-x64": "4.59.0",
"@rollup/rollup-openharmony-arm64": "4.59.0",
"@rollup/rollup-win32-arm64-msvc": "4.59.0",
"@rollup/rollup-win32-ia32-msvc": "4.59.0",
"@rollup/rollup-win32-x64-gnu": "4.59.0",
"@rollup/rollup-win32-x64-msvc": "4.59.0",
"fsevents": "~2.3.2"
}
},
@@ -2665,14 +2665,14 @@
}
},
"node_modules/sass": {
"version": "1.97.3",
"resolved": "https://registry.npmmirror.com/sass/-/sass-1.97.3.tgz",
"integrity": "sha512-fDz1zJpd5GycprAbu4Q2PV/RprsRtKC/0z82z0JLgdytmcq0+ujJbJ/09bPGDxCLkKY3Np5cRAOcWiVkLXJURg==",
"version": "1.98.0",
"resolved": "https://registry.npmmirror.com/sass/-/sass-1.98.0.tgz",
"integrity": "sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A==",
"dev": true,
"license": "MIT",
"dependencies": {
"chokidar": "^4.0.0",
"immutable": "^5.0.2",
"immutable": "^5.1.5",
"source-map-js": ">=0.6.2 <2.0.0"
},
"bin": {
@@ -3056,16 +3056,16 @@
"license": "MIT"
},
"node_modules/vue": {
"version": "3.5.28",
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.28.tgz",
"integrity": "sha512-BRdrNfeoccSoIZeIhyPBfvWSLFP4q8J3u8Ju8Ug5vu3LdD+yTM13Sg4sKtljxozbnuMu1NB1X5HBHRYUzFocKg==",
"version": "3.5.30",
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.30.tgz",
"integrity": "sha512-hTHLc6VNZyzzEH/l7PFGjpcTvUgiaPK5mdLkbjrTeWSRcEfxFrv56g/XckIYlE9ckuobsdwqd5mk2g1sBkMewg==",
"license": "MIT",
"dependencies": {
"@vue/compiler-dom": "3.5.28",
"@vue/compiler-sfc": "3.5.28",
"@vue/runtime-dom": "3.5.28",
"@vue/server-renderer": "3.5.28",
"@vue/shared": "3.5.28"
"@vue/compiler-dom": "3.5.30",
"@vue/compiler-sfc": "3.5.30",
"@vue/runtime-dom": "3.5.30",
"@vue/server-renderer": "3.5.30",
"@vue/shared": "3.5.30"
},
"peerDependencies": {
"typescript": "*"

File diff suppressed because it is too large Load Diff

View File

@@ -1,201 +1,212 @@
<?php
namespace app\controller\api;
use app\BaseController;
use app\model\PricingConfig as PricingConfigModel;
use app\model\AiProvider as AiProviderModel;
use app\common\service\JwtService;
use think\facade\Db;
/**
* 小程序运行配置(定价 + AI 服务商)
* 个人用户读超管个人定价企业用户读超管企业定价AI 一律读超管配置,默认使用第一个启用的服务商。
*/
class AppConfig extends BaseController
{
/**
* GET api/config/runtime
* 可选 Header Authorization有 token 且用户属于企业则返回企业定价,否则个人定价。
* 返回pricingType(personal|enterprise), pricing(config), aiProviderId, aiProviderName
*/
public function runtime()
{
$pricingType = 'personal';
$enterpriseId = null;
$pricing = [];
$scope = (string) ($this->request->param('scope', '') ?? '');
$user = $this->request->user ?? null;
if (!$user) {
$token = JwtService::getTokenFromRequest($this->request);
if ($token) {
$payload = JwtService::verifyToken($token);
if ($payload) {
$user = [
'source' => $payload['source'] ?? '',
'user_id' => $payload['user_id'] ?? $payload['userId'] ?? null,
'userId' => $payload['user_id'] ?? $payload['userId'] ?? null,
];
}
}
}
if ($scope !== 'personal' && $user && ($user['source'] ?? '') === 'wechat') {
$userId = (int) ($user['user_id'] ?? $user['userId'] ?? 0);
if ($userId > 0) {
$row = Db::name('wechat_users')->where('id', $userId)->field('enterpriseId')->find();
if (!empty($row['enterpriseId'])) {
$enterpriseId = (int) $row['enterpriseId'];
$pricingType = 'enterprise';
}
}
}
$config = PricingConfigModel::getByTypeAndEnterprise($pricingType, $enterpriseId);
if ($config && !empty($config->config)) {
// PricingConfig 模型已对 config 做 JSON 转换,这里直接当数组/对象用即可
$rawConfig = $config->config;
$pricing = is_array($rawConfig) ? $rawConfig : (array) $rawConfig;
}
// 超管 AI 配置:第一个 enabled=1、visible=1显示且 apiKey 非空;隐藏的服务商不参与选用
$firstProvider = AiProviderModel::where('enabled', 1)
->whereRaw('(visible IS NULL OR visible = 1)')
->whereRaw('(apiKey IS NOT NULL AND LENGTH(TRIM(apiKey)) > 0)')
->order('id', 'asc')
->find();
$aiProviderId = null;
$aiProviderName = null;
if ($firstProvider) {
$aiProviderId = $firstProvider->providerId;
$aiProviderName = $firstProvider->name ?? $firstProvider->providerId;
}
// 报告付费开关:完全根据定价配置判断(价格 > 0 视为需要付费)
$reportRequiresPayment = ['face' => 0, 'mbti' => 0, 'disc' => 0, 'pdp' => 0];
foreach (['face', 'mbti', 'disc', 'pdp'] as $k) {
$key = $k === 'team_analysis' ? 'teamAnalysis' : $k;
if (isset($pricing[$key]) && (float) $pricing[$key] > 0) {
$reportRequiresPayment[$k] = 1;
}
}
// 站点信息(网站名称、小程序名称):供小程序导航栏等展示
$siteName = '';
$miniprogramName = '';
$siteInfo = Db::name('system_config')->where('key', 'site_info')->find();
if ($siteInfo && !empty($siteInfo['value'])) {
$val = is_string($siteInfo['value']) ? json_decode($siteInfo['value'], true) : $siteInfo['value'];
if (is_array($val)) {
$siteName = (string) ($val['siteName'] ?? '');
$miniprogramName = (string) ($val['miniprogramName'] ?? '');
}
}
if ($siteName === '' || $miniprogramName === '') {
$system = Db::name('system_config')->where('key', 'system')->find();
if ($system && !empty($system['value'])) {
$val = is_string($system['value']) ? json_decode($system['value'], true) : $system['value'];
if (is_array($val)) {
if ($siteName === '') $siteName = (string) ($val['siteName'] ?? '');
if ($miniprogramName === '') $miniprogramName = (string) ($val['miniprogramName'] ?? $val['siteName'] ?? '');
}
}
}
$siteTitle = $miniprogramName !== '' ? $miniprogramName : ($siteName !== '' ? $siteName : '神仙团队AI性格测试');
// 小程序文案配置(分析中提示、按钮、报告标题等)
$textConfig = [
'analyzingTitle' => '正在分析中',
'startButtonText' => '开始面相测试',
'startButtonEnterprise' => '开始面部测试',
'reportTitle' => '分析报告',
'aiAnalysisText' => '智能分析'
];
// 先读全局 text_configenterprise_id=0
$tcRow = Db::name('system_config')->where('key', 'text_config')->where('enterprise_id', 0)->find();
if ($tcRow && !empty($tcRow['value'])) {
$tcVal = is_string($tcRow['value']) ? json_decode($tcRow['value'], true) : $tcRow['value'];
if (is_array($tcVal)) {
$textConfig = array_merge($textConfig, array_intersect_key($tcVal, $textConfig));
}
}
// 企业专属文案 + 小程序名称 覆盖全局enterprise_id={eid}
if ($enterpriseId > 0) {
$eidTc = Db::name('system_config')->where('key', 'text_config')->where('enterprise_id', $enterpriseId)->find();
if ($eidTc && !empty($eidTc['value'])) {
$eidVal = is_string($eidTc['value']) ? json_decode($eidTc['value'], true) : $eidTc['value'];
if (is_array($eidVal)) {
$textConfig = array_merge($textConfig, array_intersect_key($eidVal, $textConfig));
if (!empty($eidVal['miniprogramName'])) {
$miniprogramName = (string) $eidVal['miniprogramName'];
$siteTitle = $miniprogramName;
}
}
}
}
return success([
'pricingType' => $pricingType,
'pricing' => $pricing,
'aiProviderId' => $aiProviderId,
'aiProviderName' => $aiProviderName,
'reportRequiresPayment' => $reportRequiresPayment,
'siteName' => $siteName,
'miniprogramName' => $miniprogramName,
'siteTitle' => $siteTitle,
'textConfig' => $textConfig,
]);
}
/**
* GET api/config/deep-pricing?scope=personal|enterprise
* 深度服务价格(开通会员页):个人版与企业版分别返回可配置的类目列表,支持后台新增类目
*/
public function deepPricing()
{
$scope = (string) ($this->request->param('scope', 'personal') ?? 'personal');
$type = $scope === 'enterprise' ? 'deep_enterprise' : 'deep_personal';
$config = PricingConfigModel::where('type', $type)->whereNull('enterpriseId')->find();
$categories = [];
if ($config && !empty($config->config)) {
$raw = $config->config;
$data = is_array($raw) ? $raw : (array) $raw;
$categories = isset($data['categories']) && is_array($data['categories']) ? $data['categories'] : [];
// 兼容旧数据:补全可能缺失的字段,确保前端始终能读到完整结构
foreach ($categories as &$cat) {
// features旧数据只存 featuresText动态拆成数组
if (!isset($cat['features']) || !is_array($cat['features'])) {
if (!empty($cat['featuresText']) && is_string($cat['featuresText'])) {
$lines = preg_split('/\r?\n/', $cat['featuresText']);
$cat['features'] = array_values(array_filter(array_map('trim', $lines), static function ($s) {
return $s !== '';
}));
} else {
$cat['features'] = [];
}
}
// serviceWechat客服微信展示给用户的微信号
if (!isset($cat['serviceWechat'])) {
$cat['serviceWechat'] = '';
}
// consultWechat存客宝KEY旧类目可能没有该字段补空字符串
if (!isset($cat['consultWechat'])) {
$cat['consultWechat'] = '';
}
// promptText同样补全
if (!isset($cat['promptText'])) {
$cat['promptText'] = '';
}
// successMessage成功提示词补全
if (!isset($cat['successMessage'])) {
$cat['successMessage'] = '';
}
}
unset($cat);
}
return success(['scope' => $scope, 'categories' => $categories]);
}
}
<?php
namespace app\controller\api;
use app\BaseController;
use app\model\PricingConfig as PricingConfigModel;
use app\model\AiProvider as AiProviderModel;
use app\common\service\JwtService;
use think\facade\Db;
/**
* 小程序运行配置(定价 + AI 服务商)
* 个人用户读超管个人定价企业用户读超管企业定价AI 一律读超管配置,默认使用第一个启用的服务商。
*/
class AppConfig extends BaseController
{
/**
* GET api/config/runtime
* 可选 Header Authorization有 token 且用户属于企业则返回企业定价,否则个人定价。
* 返回pricingType(personal|enterprise), pricing(config), aiProviderId, aiProviderName
*/
public function runtime()
{
$pricingType = 'personal';
$enterpriseId = null;
$pricing = [];
$scope = (string) ($this->request->param('scope', '') ?? '');
$user = $this->request->user ?? null;
if (!$user) {
$token = JwtService::getTokenFromRequest($this->request);
if ($token) {
$payload = JwtService::verifyToken($token);
if ($payload) {
$user = [
'source' => $payload['source'] ?? '',
'user_id' => $payload['user_id'] ?? $payload['userId'] ?? null,
'userId' => $payload['user_id'] ?? $payload['userId'] ?? null,
];
}
}
}
if ($scope !== 'personal' && $user && ($user['source'] ?? '') === 'wechat') {
$userId = (int) ($user['user_id'] ?? $user['userId'] ?? 0);
if ($userId > 0) {
$row = Db::name('wechat_users')->where('id', $userId)->field('enterpriseId')->find();
if (!empty($row['enterpriseId'])) {
$enterpriseId = (int) $row['enterpriseId'];
$pricingType = 'enterprise';
}
}
}
$config = PricingConfigModel::getByTypeAndEnterprise($pricingType, $enterpriseId);
if ($config && !empty($config->config)) {
// PricingConfig 模型已对 config 做 JSON 转换,这里直接当数组/对象用即可
$rawConfig = $config->config;
$pricing = is_array($rawConfig) ? $rawConfig : (array) $rawConfig;
}
// 超管 AI 配置:第一个 enabled=1、visible=1显示且 apiKey 非空;隐藏的服务商不参与选用
$firstProvider = AiProviderModel::where('enabled', 1)
->whereRaw('(visible IS NULL OR visible = 1)')
->whereRaw('(apiKey IS NOT NULL AND LENGTH(TRIM(apiKey)) > 0)')
->order('id', 'asc')
->find();
$aiProviderId = null;
$aiProviderName = null;
if ($firstProvider) {
$aiProviderId = $firstProvider->providerId;
$aiProviderName = $firstProvider->name ?? $firstProvider->providerId;
}
// 报告付费开关:完全根据定价配置判断(价格 > 0 视为需要付费)
$reportRequiresPayment = ['face' => 0, 'mbti' => 0, 'disc' => 0, 'pdp' => 0];
foreach (['face', 'mbti', 'disc', 'pdp'] as $k) {
$key = $k === 'team_analysis' ? 'teamAnalysis' : $k;
if (isset($pricing[$key]) && (float) $pricing[$key] > 0) {
$reportRequiresPayment[$k] = 1;
}
}
// 站点信息(网站名称、小程序名称):供小程序导航栏等展示
$siteName = '';
$miniprogramName = '';
$siteInfo = Db::name('system_config')->where('key', 'site_info')->find();
if ($siteInfo && !empty($siteInfo['value'])) {
$val = is_string($siteInfo['value']) ? json_decode($siteInfo['value'], true) : $siteInfo['value'];
if (is_array($val)) {
$siteName = (string) ($val['siteName'] ?? '');
$miniprogramName = (string) ($val['miniprogramName'] ?? '');
}
}
if ($siteName === '' || $miniprogramName === '') {
$system = Db::name('system_config')->where('key', 'system')->find();
if ($system && !empty($system['value'])) {
$val = is_string($system['value']) ? json_decode($system['value'], true) : $system['value'];
if (is_array($val)) {
if ($siteName === '') $siteName = (string) ($val['siteName'] ?? '');
if ($miniprogramName === '') $miniprogramName = (string) ($val['miniprogramName'] ?? $val['siteName'] ?? '');
}
}
}
$siteTitle = $miniprogramName !== '' ? $miniprogramName : ($siteName !== '' ? $siteName : '神仙团队AI性格测试');
// 小程序文案配置(分析中提示、按钮、报告标题等)
$textConfig = [
'analyzingTitle' => '正在分析中',
'startButtonText' => '开始面相测试',
'startButtonEnterprise' => '开始面部测试',
'reportTitle' => '分析报告',
'aiAnalysisText' => '智能分析'
];
// 先读全局 text_configenterprise_id=0
$tcRow = Db::name('system_config')->where('key', 'text_config')->where('enterprise_id', 0)->find();
if ($tcRow && !empty($tcRow['value'])) {
$tcVal = is_string($tcRow['value']) ? json_decode($tcRow['value'], true) : $tcRow['value'];
if (is_array($tcVal)) {
$textConfig = array_merge($textConfig, array_intersect_key($tcVal, $textConfig));
}
}
// 企业专属文案 + 小程序名称 覆盖全局enterprise_id={eid}
if ($enterpriseId > 0) {
$eidTc = Db::name('system_config')->where('key', 'text_config')->where('enterprise_id', $enterpriseId)->find();
if ($eidTc && !empty($eidTc['value'])) {
$eidVal = is_string($eidTc['value']) ? json_decode($eidTc['value'], true) : $eidTc['value'];
if (is_array($eidVal)) {
$textConfig = array_merge($textConfig, array_intersect_key($eidVal, $textConfig));
if (!empty($eidVal['miniprogramName'])) {
$miniprogramName = (string) $eidVal['miniprogramName'];
$siteTitle = $miniprogramName;
}
}
}
}
// 审核模式开关
$reviewMode = false;
$rmRow = Db::name('system_config')->where('key', 'review_mode')->where('enterprise_id', 0)->find();
if ($rmRow && !empty($rmRow['value'])) {
$rmVal = is_string($rmRow['value']) ? json_decode($rmRow['value'], true) : $rmRow['value'];
if (is_array($rmVal) && !empty($rmVal['enabled'])) {
$reviewMode = true;
}
}
return success([
'pricingType' => $pricingType,
'pricing' => $pricing,
'aiProviderId' => $aiProviderId,
'aiProviderName' => $aiProviderName,
'reportRequiresPayment' => $reportRequiresPayment,
'siteName' => $siteName,
'miniprogramName' => $miniprogramName,
'siteTitle' => $siteTitle,
'textConfig' => $textConfig,
'reviewMode' => $reviewMode,
]);
}
/**
* GET api/config/deep-pricing?scope=personal|enterprise
* 深度服务价格(开通会员页):个人版与企业版分别返回可配置的类目列表,支持后台新增类目
*/
public function deepPricing()
{
$scope = (string) ($this->request->param('scope', 'personal') ?? 'personal');
$type = $scope === 'enterprise' ? 'deep_enterprise' : 'deep_personal';
$config = PricingConfigModel::where('type', $type)->whereNull('enterpriseId')->find();
$categories = [];
if ($config && !empty($config->config)) {
$raw = $config->config;
$data = is_array($raw) ? $raw : (array) $raw;
$categories = isset($data['categories']) && is_array($data['categories']) ? $data['categories'] : [];
// 兼容旧数据:补全可能缺失的字段,确保前端始终能读到完整结构
foreach ($categories as &$cat) {
// features旧数据只存 featuresText动态拆成数组
if (!isset($cat['features']) || !is_array($cat['features'])) {
if (!empty($cat['featuresText']) && is_string($cat['featuresText'])) {
$lines = preg_split('/\r?\n/', $cat['featuresText']);
$cat['features'] = array_values(array_filter(array_map('trim', $lines), static function ($s) {
return $s !== '';
}));
} else {
$cat['features'] = [];
}
}
// serviceWechat客服微信展示给用户的微信号
if (!isset($cat['serviceWechat'])) {
$cat['serviceWechat'] = '';
}
// consultWechat存客宝KEY旧类目可能没有该字段补空字符串
if (!isset($cat['consultWechat'])) {
$cat['consultWechat'] = '';
}
// promptText同样补全
if (!isset($cat['promptText'])) {
$cat['promptText'] = '';
}
// successMessage成功提示词补全
if (!isset($cat['successMessage'])) {
$cat['successMessage'] = '';
}
}
unset($cat);
}
return success(['scope' => $scope, 'categories' => $categories]);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,382 +1,383 @@
<?php
// API路由定义
use think\facade\Route;
// ==================== 前端用户API路由 ====================
// 为避免匹配混淆,手机号接口单独声明完整路径(优先级更高)
Route::post('api/auth/wechat/phone', 'api.Auth/wechatPhone')->middleware(['cors', 'auth']);
// 前端公开API路由不需要认证
Route::group('api', function () {
// 用户认证相关
Route::post('auth/login', 'api.Auth/login');
Route::post('auth/register', 'api.Auth/register');
Route::post('auth/refresh', 'api.Auth/refresh');
// 微信小程序登录
Route::post('auth/wechat', 'api.Auth/wechatLogin');
})->middleware('cors');
// 小程序/前端运行配置与面相分析(可选 token
Route::group('api', function () {
Route::get('config/runtime', 'api.AppConfig/runtime');
Route::get('config/deep-pricing', 'api.AppConfig/deepPricing');
Route::post('analyze', 'api.Analyze/index');
})->middleware('cors');
// 前端需要认证的API路由
Route::group('api', function () {
// 用户信息
Route::get('auth/me', 'api.Auth/me');
Route::post('auth/logout', 'api.Auth/logout');
// 小程序扫码企业邀请后绑定企业
Route::post('enterprise/bind', 'api.Auth/wechatBindEnterprise');
// 企业版简历上传记录(具体路径放前面,避免被 POST resume-uploads 吞掉)
Route::get('enterprise/resume-uploads', 'api.EnterpriseResume/list');
Route::post('enterprise/resume-uploads/set-default', 'api.EnterpriseResume/setDefault');
Route::post('enterprise/resume-uploads/delete', 'api.EnterpriseResume/delete');
Route::post('enterprise/resume-uploads', 'api.EnterpriseResume/add');
// 小程序用户更新资料
Route::put('auth/wechat/profile', 'api.Auth/updateWechatProfile');
// 小程序用户上传图片(头像等)
Route::post('upload/image', 'api.Upload/image');
// 小程序用户上传文件(候选人简历等)
Route::post('upload/file', 'api.Upload/file');
// 当前用户测试历史记录(小程序「测试历史」页)
Route::get('test/history', 'api.Test/history');
// 当前用户各类型最新一条记录(小程序「我的」页)
Route::get('test/recent', 'api.Test/recent');
// 单条测试详情
Route::get('test/detail', 'api.Test/detail');
// 提交测试结果MBTI/DISC/PDP 等)
Route::post('test/submit', 'api.Test/submit');
// 简历综合分析(基于人脸/MBTI/PDP/DISC 最近一次结果)
Route::post('resume/analyze', 'api.Analyze/resumeAnalysis');
// 支付与订单
Route::post('payment/create', 'api.Payment/create');
Route::post('payment/notify', 'api.Payment/notify');
Route::get('payment/query', 'api.Payment/query');
// 分销
Route::post('distribution/bind', 'api.Distribution/bind');
Route::get('distribution/stats', 'api.Distribution/stats');
Route::get('distribution/bindings', 'api.Distribution/bindings');
Route::get('distribution/commissions', 'api.Distribution/commissions');
Route::post('distribution/withdraw', 'api.Distribution/withdraw');
Route::get('distribution/withdrawals', 'api.Distribution/withdrawals');
Route::post('distribution/withdrawals/query-transfer', 'api.Distribution/queryTransfer');
Route::get('distribution/qrcode', 'api.Distribution/qrcode');
Route::get('distribution/poster', 'api.Distribution/poster');
// 微信商家转账结果回调(无需登录,但需配置到微信商户平台)
Route::post('wechat/transfer/notify', 'api.WechatTransferNotify/notify')->middleware('cors');
// 存客宝获客线索上报
Route::post('crm/report', 'api.CrmReport/report');
})->middleware(['cors', 'auth']);
// ==================== 小程序API路由匹配前端 /api 路径)====================
// 小程序公开API路由不需要认证- 与前端共用上面 api 组,此处可不再重复
// 小程序需要认证的API路由 - 与前端共用上面 api 组
// ==================== 普通管理员API路由匹配前端 /api/v1/admin 路径)====================
// 普通管理员认证路由(不需要认证)
Route::group('api/v1/admin', function () {
// 普通管理员登录
Route::post('auth/login', 'admin.Auth/adminLogin');
// 刷新Token
Route::post('auth/refresh', 'admin.Auth/refresh');
})->middleware('cors');
// 普通管理员路由(需要认证)
Route::group('api/v1/admin', function () {
// 管理员认证
Route::get('auth/me', 'admin.Auth/me');
Route::post('auth/logout', 'admin.Auth/logout');
// 仪表盘统计
Route::get('dashboard', 'admin.Dashboard/index');
// 邀请二维码
Route::get('invite/qrcode', 'admin.Invite/qrcode');
// 通用上传
Route::post('upload/image', 'admin.Upload/image');
// 测试用户(小程序用户,只读列表与详情)
Route::get('app-users/:id', 'admin.AppUser/detail');
Route::get('app-users', 'admin.AppUser/index');
// 订单列表(含用户与关联测试数据)
Route::get('orders', 'admin.Order/index');
// 用户管理(普通管理员和企业管理员,后台账号)
Route::get('users', 'admin.User/index');
Route::get('users/:id', 'admin.User/detail');
Route::post('users', 'admin.User/create');
Route::put('users/:id', 'admin.User/update');
Route::delete('users/:id', 'admin.User/delete');
Route::put('users/:id/status', 'admin.User/toggleStatus');
// 题库管理(企业管理员和普通管理员)
Route::get('questions/:id', 'admin.Question/detail');
Route::get('questions', 'admin.Question/index');
Route::post('questions', 'admin.Question/create');
Route::put('questions/:id', 'admin.Question/update');
Route::delete('questions/:id', 'admin.Question/delete');
Route::put('questions/:id/status', 'admin.Question/toggleStatus');
Route::post('questions/batch-import', 'admin.Question/batchImport');
// 定价管理(普通管理员)
Route::get('pricing', 'admin.Pricing/index');
Route::put('pricing', 'admin.Pricing/update');
// 系统设置(普通管理员,子路径放前面避免被 settings 吞掉)
Route::get('settings/miniprogram', 'admin.Settings/getMiniprogramConfig');
Route::put('settings/miniprogram', 'admin.Settings/updateMiniprogramConfig');
Route::get('settings/poster', 'admin.Settings/getPosterConfig');
Route::put('settings/poster', 'admin.Settings/updatePosterConfig');
Route::get('settings/fonts', 'admin.Settings/getFonts');
Route::put('settings/credentials', 'admin.Settings/updateCredentials');
Route::get('settings', 'admin.Settings/index');
// 分销管理(企业管理员)
Route::get('distribution/overview', 'admin.Distribution/overview');
Route::get('distribution/distributors', 'admin.Distribution/distributors');
Route::get('distribution/bindings', 'admin.Distribution/bindings');
Route::get('distribution/commissions', 'admin.Distribution/commissions');
Route::get('distribution/withdrawals', 'admin.Distribution/withdrawals');
Route::post('distribution/withdrawals/:id/approve', 'admin.Distribution/approveWithdrawal');
Route::post('distribution/withdrawals/:id/reject', 'admin.Distribution/rejectWithdrawal');
Route::get('distribution/settings', 'admin.Distribution/settings');
Route::put('distribution/settings', 'admin.Distribution/updateSettings');
// 企业财务(企业管理员)
Route::get('finance/overview', 'admin.Finance/overview');
Route::get('finance/records', 'admin.Finance/records');
Route::post('finance/recharge-qrcode', 'admin.Finance/rechargeQrcode');
Route::post('finance/recharge', 'admin.Finance/rechargeQrcode');
})->middleware(['cors', 'auth']);
// ==================== 超级管理员API路由匹配前端 /api/v1/superadmin 路径)====================
// 超级管理员认证路由(不需要认证)
Route::group('api/v1/superadmin', function () {
// 超级管理员登录
Route::post('auth/login', 'superadmin.Auth/login');
// 刷新Token
Route::post('auth/refresh', 'superadmin.Auth/refresh');
})->middleware('cors');
// 超级管理员路由(需要认证)
Route::group('api/v1/superadmin', function () {
// 超级管理员认证
Route::get('auth/me', 'superadmin.Auth/me');
Route::post('auth/logout', 'superadmin.Auth/logout');
// 企业管理(超管专用)
// 注意:带参数的路由要放在不带参数的路由之前,避免路由匹配冲突
Route::get('enterprises/:id/detail', 'superadmin.Enterprise/detail'); // 详细详情接口
Route::get('enterprises/:id', 'superadmin.Enterprise/detail');
Route::get('enterprises', 'superadmin.Enterprise/index');
Route::post('enterprises', 'superadmin.Enterprise/create');
Route::put('enterprises/:id', 'superadmin.Enterprise/update');
Route::delete('enterprises/:id', 'superadmin.Enterprise/delete');
Route::put('enterprises/:id/status', 'superadmin.Enterprise/toggleStatus');
// 题库管理(超管专用,管理超管题库)
Route::get('questions/:id', 'superadmin.Question/detail');
Route::get('questions', 'superadmin.Question/index');
Route::post('questions', 'superadmin.Question/create');
Route::put('questions/:id', 'superadmin.Question/update');
Route::delete('questions/:id', 'superadmin.Question/delete');
Route::put('questions/:id/status', 'superadmin.Question/toggleStatus');
Route::post('questions/batch-import', 'superadmin.Question/batchImport');
// 全局定价管理(超管专用)
Route::get('pricing', 'superadmin.Pricing/index');
Route::put('pricing', 'superadmin.Pricing/update');
Route::post('pricing/batch-update', 'superadmin.Pricing/batchUpdate');
// AI服务商配置管理超管专用
Route::get('ai-config', 'superadmin.AiConfig/index');
Route::put('ai-config', 'superadmin.AiConfig/update');
Route::post('ai-config/batch-update', 'superadmin.AiConfig/batchUpdate');
Route::post('ai-config/query-balance', 'superadmin.AiConfig/queryBalance');
Route::post('ai-config/query-all-balances', 'superadmin.AiConfig/queryAllBalances');
// 数据库管理(超管专用)
Route::get('database/info', 'superadmin.Database/info');
Route::get('database/tables', 'superadmin.Database/tables');
Route::get('database/view-table', 'superadmin.Database/viewTable');
Route::post('database/export-table', 'superadmin.Database/exportTable');
Route::post('database/clear-table', 'superadmin.Database/clearTable');
Route::post('database/backup', 'superadmin.Database/backup');
Route::get('database/backups', 'superadmin.Database/backups');
Route::delete('database/backups/:id', 'superadmin.Database/delete');
Route::post('database/backups/delete', 'superadmin.Database/delete');
Route::get('database/download', 'superadmin.Database/download');
Route::post('database/restore', 'superadmin.Database/restore');
// 通用上传(超管)
Route::post('upload/image', 'admin.Upload/image');
// 系统设置(超管专用,子路径放前面避免被 settings 吞掉)
Route::get('settings/fonts', 'superadmin.Settings/getFonts');
Route::get('settings/poster', 'superadmin.Settings/getPosterConfig');
Route::put('settings/poster', 'superadmin.Settings/updatePosterConfig');
Route::get('settings', 'superadmin.Settings/index');
Route::put('settings/system', 'superadmin.Settings/updateSystem');
Route::put('settings/report-requires-payment', 'superadmin.Settings/updateReportRequiresPayment');
Route::put('settings/notification', 'superadmin.Settings/updateNotification');
Route::put('settings/prompts', 'superadmin.Settings/updatePrompts');
Route::put('settings/credentials', 'superadmin.Settings/updateCredentials');
// 测试用户(超管专用)
Route::get('app-users/overview', 'superadmin.AppUser/overview');
Route::get('app-users/:id', 'superadmin.AppUser/detail');
Route::get('app-users', 'superadmin.AppUser/index');
// 数据概览(超管专用,子路径放前面避免被 overview 吞掉)
Route::get('overview/recent-dynamics', 'superadmin.Overview/recentDynamics');
Route::get('overview/enterprise-ranking', 'superadmin.Overview/enterpriseRanking');
Route::get('overview/test-trends', 'superadmin.Overview/testTrends');
Route::get('overview', 'superadmin.Overview/index');
// 财务管理(超管专用)
Route::get('finance/overview', 'superadmin.Finance/overview');
Route::get('finance/revenue-details', 'superadmin.Finance/revenueDetails');
Route::get('finance/cost-details', 'superadmin.Finance/costDetails');
Route::get('finance/recharge-records', 'superadmin.Finance/rechargeRecords');
Route::get('finance/payment-records', 'superadmin.Finance/paymentRecords');
Route::post('finance/export', 'superadmin.Finance/export');
// 分销管理(超管专用 - 个人版全平台视图)
Route::get('distribution/overview', 'superadmin.Distribution/overview');
Route::get('distribution/bindings', 'superadmin.Distribution/bindings');
Route::get('distribution/commissions', 'superadmin.Distribution/commissions');
Route::get('distribution/withdrawals', 'superadmin.Distribution/withdrawals');
Route::post('distribution/withdrawals/:id/approve', 'superadmin.Distribution/approveWithdrawal');
Route::post('distribution/withdrawals/:id/reject', 'superadmin.Distribution/rejectWithdrawal');
Route::get('distribution/settings', 'superadmin.Distribution/settings');
Route::put('distribution/settings', 'superadmin.Distribution/updateSettings');
})->middleware(['cors', 'auth', 'superadmin']);
// ==================== 兼容旧版路由(保留,逐步废弃)====================
// 管理后台认证路由(不需要认证)- 兼容旧版
Route::group('api/v1', function () {
// 管理员登录(兼容旧版)
Route::post('auth/admin/login', 'admin.Auth/adminLogin');
// 超级管理员登录(兼容旧版)
Route::post('auth/superadmin/login', 'superadmin.Auth/login');
// 刷新Token兼容旧版
Route::post('auth/refresh', 'admin.Auth/refresh');
})->middleware('cors');
// 管理后台路由(需要认证)- 兼容旧版
Route::group('api/v1', function () {
// 管理员认证(兼容旧版)
Route::get('auth/me', 'admin.Auth/me');
Route::post('auth/logout', 'admin.Auth/logout');
// 仪表盘统计(兼容旧版)
Route::get('dashboard', 'admin.Dashboard/index');
// 通用上传(兼容旧版)
Route::post('upload/image', 'admin.Upload/image');
// 用户管理(兼容旧版)
Route::get('users', 'admin.User/index');
Route::get('users/:id', 'admin.User/detail');
Route::post('users', 'admin.User/create');
Route::put('users/:id', 'admin.User/update');
Route::delete('users/:id', 'admin.User/delete');
Route::put('users/:id/status', 'admin.User/toggleStatus');
// 企业管理(超管,兼容旧版)
// 注意:带参数的路由要放在不带参数的路由之前,避免路由匹配冲突
Route::get('enterprises/:id/detail', 'superadmin.Enterprise/detail'); // 详细详情接口
Route::get('enterprises/:id', 'superadmin.Enterprise/detail');
Route::get('enterprises', 'superadmin.Enterprise/index');
Route::post('enterprises', 'superadmin.Enterprise/create');
Route::put('enterprises/:id', 'superadmin.Enterprise/update');
Route::delete('enterprises/:id', 'superadmin.Enterprise/delete');
Route::put('enterprises/:id/status', 'superadmin.Enterprise/toggleStatus');
// 题库管理(兼容旧版)
Route::get('questions/:id', 'admin.Question/detail');
Route::get('questions', 'admin.Question/index');
Route::post('questions', 'admin.Question/create');
Route::put('questions/:id', 'admin.Question/update');
Route::delete('questions/:id', 'admin.Question/delete');
Route::put('questions/:id/status', 'admin.Question/toggleStatus');
Route::post('questions/batch-import', 'admin.Question/batchImport');
// 企业财务(兼容旧版)
Route::get('finance/overview', 'admin.Finance/overview');
Route::get('finance/records', 'admin.Finance/records');
Route::post('finance/recharge-qrcode', 'admin.Finance/rechargeQrcode');
Route::post('finance/recharge', 'admin.Finance/rechargeQrcode');
})->middleware(['cors', 'auth']);
// ==================== 兼容旧版路由(保留)====================
// 后台管理认证路由(不需要认证)- 兼容旧版
Route::group('api/admin', function () {
// 管理员登录
Route::post('auth/login', 'admin.Auth/login');
Route::post('auth/refresh', 'admin.Auth/refresh');
})->middleware('cors');
// 后台管理路由(需要认证)- 兼容旧版
Route::group('api/admin', function () {
// 管理员认证
Route::get('auth/me', 'admin.Auth/me');
Route::post('auth/logout', 'admin.Auth/logout');
// 仪表盘统计
Route::get('dashboard', 'admin.Dashboard/index');
// 通用上传
Route::post('upload/image', 'admin.Upload/image');
// 用户管理
Route::get('users', 'admin.User/index');
Route::get('users/:id', 'admin.User/detail');
Route::post('users', 'admin.User/create');
Route::put('users/:id', 'admin.User/update');
Route::delete('users/:id', 'admin.User/delete');
Route::put('users/:id/status', 'admin.User/toggleStatus');
// 企业财务
Route::get('finance/overview', 'admin.Finance/overview');
Route::get('finance/records', 'admin.Finance/records');
Route::post('finance/recharge-qrcode', 'admin.Finance/rechargeQrcode');
Route::post('finance/recharge', 'admin.Finance/rechargeQrcode');
})->middleware(['cors', 'auth']);
// ==================== 兼容旧版路由(保留)====================
// 后台管理认证路由(不需要认证)- 兼容旧版
Route::group('api/admin', function () {
// 管理员登录
Route::post('auth/login', 'admin.Auth/login');
Route::post('auth/refresh', 'admin.Auth/refresh');
})->middleware('cors');
// 后台管理路由(需要认证)- 兼容旧版
Route::group('api/admin', function () {
// 管理员认证
Route::get('auth/me', 'admin.Auth/me');
Route::post('auth/logout', 'admin.Auth/logout');
// 仪表盘统计
Route::get('dashboard', 'admin.Dashboard/index');
// 通用上传
Route::post('upload/image', 'admin.Upload/image');
// 用户管理
Route::get('users', 'admin.User/index');
Route::get('users/:id', 'admin.User/detail');
Route::post('users', 'admin.User/create');
Route::put('users/:id', 'admin.User/update');
Route::delete('users/:id', 'admin.User/delete');
Route::put('users/:id/status', 'admin.User/toggleStatus');
// 企业财务
Route::get('finance/overview', 'admin.Finance/overview');
Route::get('finance/records', 'admin.Finance/records');
Route::post('finance/recharge-qrcode', 'admin.Finance/rechargeQrcode');
Route::post('finance/recharge', 'admin.Finance/rechargeQrcode');
})->middleware(['cors', 'auth']);
<?php
// API路由定义
use think\facade\Route;
// ==================== 前端用户API路由 ====================
// 为避免匹配混淆,手机号接口单独声明完整路径(优先级更高)
Route::post('api/auth/wechat/phone', 'api.Auth/wechatPhone')->middleware(['cors', 'auth']);
// 前端公开API路由不需要认证
Route::group('api', function () {
// 用户认证相关
Route::post('auth/login', 'api.Auth/login');
Route::post('auth/register', 'api.Auth/register');
Route::post('auth/refresh', 'api.Auth/refresh');
// 微信小程序登录
Route::post('auth/wechat', 'api.Auth/wechatLogin');
})->middleware('cors');
// 小程序/前端运行配置与面相分析(可选 token
Route::group('api', function () {
Route::get('config/runtime', 'api.AppConfig/runtime');
Route::get('config/deep-pricing', 'api.AppConfig/deepPricing');
Route::post('analyze', 'api.Analyze/index');
})->middleware('cors');
// 前端需要认证的API路由
Route::group('api', function () {
// 用户信息
Route::get('auth/me', 'api.Auth/me');
Route::post('auth/logout', 'api.Auth/logout');
// 小程序扫码企业邀请后绑定企业
Route::post('enterprise/bind', 'api.Auth/wechatBindEnterprise');
// 企业版简历上传记录(具体路径放前面,避免被 POST resume-uploads 吞掉)
Route::get('enterprise/resume-uploads', 'api.EnterpriseResume/list');
Route::post('enterprise/resume-uploads/set-default', 'api.EnterpriseResume/setDefault');
Route::post('enterprise/resume-uploads/delete', 'api.EnterpriseResume/delete');
Route::post('enterprise/resume-uploads', 'api.EnterpriseResume/add');
// 小程序用户更新资料
Route::put('auth/wechat/profile', 'api.Auth/updateWechatProfile');
// 小程序用户上传图片(头像等)
Route::post('upload/image', 'api.Upload/image');
// 小程序用户上传文件(候选人简历等)
Route::post('upload/file', 'api.Upload/file');
// 当前用户测试历史记录(小程序「测试历史」页)
Route::get('test/history', 'api.Test/history');
// 当前用户各类型最新一条记录(小程序「我的」页)
Route::get('test/recent', 'api.Test/recent');
// 单条测试详情
Route::get('test/detail', 'api.Test/detail');
// 提交测试结果MBTI/DISC/PDP 等)
Route::post('test/submit', 'api.Test/submit');
// 简历综合分析(基于人脸/MBTI/PDP/DISC 最近一次结果)
Route::post('resume/analyze', 'api.Analyze/resumeAnalysis');
// 支付与订单
Route::post('payment/create', 'api.Payment/create');
Route::post('payment/notify', 'api.Payment/notify');
Route::get('payment/query', 'api.Payment/query');
// 分销
Route::post('distribution/bind', 'api.Distribution/bind');
Route::get('distribution/stats', 'api.Distribution/stats');
Route::get('distribution/bindings', 'api.Distribution/bindings');
Route::get('distribution/commissions', 'api.Distribution/commissions');
Route::post('distribution/withdraw', 'api.Distribution/withdraw');
Route::get('distribution/withdrawals', 'api.Distribution/withdrawals');
Route::post('distribution/withdrawals/query-transfer', 'api.Distribution/queryTransfer');
Route::get('distribution/qrcode', 'api.Distribution/qrcode');
Route::get('distribution/poster', 'api.Distribution/poster');
// 微信商家转账结果回调(无需登录,但需配置到微信商户平台)
Route::post('wechat/transfer/notify', 'api.WechatTransferNotify/notify')->middleware('cors');
// 存客宝获客线索上报
Route::post('crm/report', 'api.CrmReport/report');
})->middleware(['cors', 'auth']);
// ==================== 小程序API路由匹配前端 /api 路径)====================
// 小程序公开API路由不需要认证- 与前端共用上面 api 组,此处可不再重复
// 小程序需要认证的API路由 - 与前端共用上面 api 组
// ==================== 普通管理员API路由匹配前端 /api/v1/admin 路径)====================
// 普通管理员认证路由(不需要认证)
Route::group('api/v1/admin', function () {
// 普通管理员登录
Route::post('auth/login', 'admin.Auth/adminLogin');
// 刷新Token
Route::post('auth/refresh', 'admin.Auth/refresh');
})->middleware('cors');
// 普通管理员路由(需要认证)
Route::group('api/v1/admin', function () {
// 管理员认证
Route::get('auth/me', 'admin.Auth/me');
Route::post('auth/logout', 'admin.Auth/logout');
// 仪表盘统计
Route::get('dashboard', 'admin.Dashboard/index');
// 邀请二维码
Route::get('invite/qrcode', 'admin.Invite/qrcode');
// 通用上传
Route::post('upload/image', 'admin.Upload/image');
// 测试用户(小程序用户,只读列表与详情)
Route::get('app-users/:id', 'admin.AppUser/detail');
Route::get('app-users', 'admin.AppUser/index');
// 订单列表(含用户与关联测试数据)
Route::get('orders', 'admin.Order/index');
// 用户管理(普通管理员和企业管理员,后台账号)
Route::get('users', 'admin.User/index');
Route::get('users/:id', 'admin.User/detail');
Route::post('users', 'admin.User/create');
Route::put('users/:id', 'admin.User/update');
Route::delete('users/:id', 'admin.User/delete');
Route::put('users/:id/status', 'admin.User/toggleStatus');
// 题库管理(企业管理员和普通管理员)
Route::get('questions/:id', 'admin.Question/detail');
Route::get('questions', 'admin.Question/index');
Route::post('questions', 'admin.Question/create');
Route::put('questions/:id', 'admin.Question/update');
Route::delete('questions/:id', 'admin.Question/delete');
Route::put('questions/:id/status', 'admin.Question/toggleStatus');
Route::post('questions/batch-import', 'admin.Question/batchImport');
// 定价管理(普通管理员)
Route::get('pricing', 'admin.Pricing/index');
Route::put('pricing', 'admin.Pricing/update');
// 系统设置(普通管理员,子路径放前面避免被 settings 吞掉)
Route::get('settings/miniprogram', 'admin.Settings/getMiniprogramConfig');
Route::put('settings/miniprogram', 'admin.Settings/updateMiniprogramConfig');
Route::get('settings/poster', 'admin.Settings/getPosterConfig');
Route::put('settings/poster', 'admin.Settings/updatePosterConfig');
Route::get('settings/fonts', 'admin.Settings/getFonts');
Route::put('settings/credentials', 'admin.Settings/updateCredentials');
Route::get('settings', 'admin.Settings/index');
// 分销管理(企业管理员)
Route::get('distribution/overview', 'admin.Distribution/overview');
Route::get('distribution/distributors', 'admin.Distribution/distributors');
Route::get('distribution/bindings', 'admin.Distribution/bindings');
Route::get('distribution/commissions', 'admin.Distribution/commissions');
Route::get('distribution/withdrawals', 'admin.Distribution/withdrawals');
Route::post('distribution/withdrawals/:id/approve', 'admin.Distribution/approveWithdrawal');
Route::post('distribution/withdrawals/:id/reject', 'admin.Distribution/rejectWithdrawal');
Route::get('distribution/settings', 'admin.Distribution/settings');
Route::put('distribution/settings', 'admin.Distribution/updateSettings');
// 企业财务(企业管理员)
Route::get('finance/overview', 'admin.Finance/overview');
Route::get('finance/records', 'admin.Finance/records');
Route::post('finance/recharge-qrcode', 'admin.Finance/rechargeQrcode');
Route::post('finance/recharge', 'admin.Finance/rechargeQrcode');
})->middleware(['cors', 'auth']);
// ==================== 超级管理员API路由匹配前端 /api/v1/superadmin 路径)====================
// 超级管理员认证路由(不需要认证)
Route::group('api/v1/superadmin', function () {
// 超级管理员登录
Route::post('auth/login', 'superadmin.Auth/login');
// 刷新Token
Route::post('auth/refresh', 'superadmin.Auth/refresh');
})->middleware('cors');
// 超级管理员路由(需要认证)
Route::group('api/v1/superadmin', function () {
// 超级管理员认证
Route::get('auth/me', 'superadmin.Auth/me');
Route::post('auth/logout', 'superadmin.Auth/logout');
// 企业管理(超管专用)
// 注意:带参数的路由要放在不带参数的路由之前,避免路由匹配冲突
Route::get('enterprises/:id/detail', 'superadmin.Enterprise/detail'); // 详细详情接口
Route::get('enterprises/:id', 'superadmin.Enterprise/detail');
Route::get('enterprises', 'superadmin.Enterprise/index');
Route::post('enterprises', 'superadmin.Enterprise/create');
Route::put('enterprises/:id', 'superadmin.Enterprise/update');
Route::delete('enterprises/:id', 'superadmin.Enterprise/delete');
Route::put('enterprises/:id/status', 'superadmin.Enterprise/toggleStatus');
// 题库管理(超管专用,管理超管题库)
Route::get('questions/:id', 'superadmin.Question/detail');
Route::get('questions', 'superadmin.Question/index');
Route::post('questions', 'superadmin.Question/create');
Route::put('questions/:id', 'superadmin.Question/update');
Route::delete('questions/:id', 'superadmin.Question/delete');
Route::put('questions/:id/status', 'superadmin.Question/toggleStatus');
Route::post('questions/batch-import', 'superadmin.Question/batchImport');
// 全局定价管理(超管专用)
Route::get('pricing', 'superadmin.Pricing/index');
Route::put('pricing', 'superadmin.Pricing/update');
Route::post('pricing/batch-update', 'superadmin.Pricing/batchUpdate');
// AI服务商配置管理超管专用
Route::get('ai-config', 'superadmin.AiConfig/index');
Route::put('ai-config', 'superadmin.AiConfig/update');
Route::post('ai-config/batch-update', 'superadmin.AiConfig/batchUpdate');
Route::post('ai-config/query-balance', 'superadmin.AiConfig/queryBalance');
Route::post('ai-config/query-all-balances', 'superadmin.AiConfig/queryAllBalances');
// 数据库管理(超管专用)
Route::get('database/info', 'superadmin.Database/info');
Route::get('database/tables', 'superadmin.Database/tables');
Route::get('database/view-table', 'superadmin.Database/viewTable');
Route::post('database/export-table', 'superadmin.Database/exportTable');
Route::post('database/clear-table', 'superadmin.Database/clearTable');
Route::post('database/backup', 'superadmin.Database/backup');
Route::get('database/backups', 'superadmin.Database/backups');
Route::delete('database/backups/:id', 'superadmin.Database/delete');
Route::post('database/backups/delete', 'superadmin.Database/delete');
Route::get('database/download', 'superadmin.Database/download');
Route::post('database/restore', 'superadmin.Database/restore');
// 通用上传(超管)
Route::post('upload/image', 'admin.Upload/image');
// 系统设置(超管专用,子路径放前面避免被 settings 吞掉)
Route::get('settings/fonts', 'superadmin.Settings/getFonts');
Route::get('settings/poster', 'superadmin.Settings/getPosterConfig');
Route::put('settings/poster', 'superadmin.Settings/updatePosterConfig');
Route::put('settings/review-mode', 'superadmin.Settings/updateReviewMode');
Route::get('settings', 'superadmin.Settings/index');
Route::put('settings/system', 'superadmin.Settings/updateSystem');
Route::put('settings/report-requires-payment', 'superadmin.Settings/updateReportRequiresPayment');
Route::put('settings/notification', 'superadmin.Settings/updateNotification');
Route::put('settings/prompts', 'superadmin.Settings/updatePrompts');
Route::put('settings/credentials', 'superadmin.Settings/updateCredentials');
// 测试用户(超管专用)
Route::get('app-users/overview', 'superadmin.AppUser/overview');
Route::get('app-users/:id', 'superadmin.AppUser/detail');
Route::get('app-users', 'superadmin.AppUser/index');
// 数据概览(超管专用,子路径放前面避免被 overview 吞掉)
Route::get('overview/recent-dynamics', 'superadmin.Overview/recentDynamics');
Route::get('overview/enterprise-ranking', 'superadmin.Overview/enterpriseRanking');
Route::get('overview/test-trends', 'superadmin.Overview/testTrends');
Route::get('overview', 'superadmin.Overview/index');
// 财务管理(超管专用)
Route::get('finance/overview', 'superadmin.Finance/overview');
Route::get('finance/revenue-details', 'superadmin.Finance/revenueDetails');
Route::get('finance/cost-details', 'superadmin.Finance/costDetails');
Route::get('finance/recharge-records', 'superadmin.Finance/rechargeRecords');
Route::get('finance/payment-records', 'superadmin.Finance/paymentRecords');
Route::post('finance/export', 'superadmin.Finance/export');
// 分销管理(超管专用 - 个人版全平台视图)
Route::get('distribution/overview', 'superadmin.Distribution/overview');
Route::get('distribution/bindings', 'superadmin.Distribution/bindings');
Route::get('distribution/commissions', 'superadmin.Distribution/commissions');
Route::get('distribution/withdrawals', 'superadmin.Distribution/withdrawals');
Route::post('distribution/withdrawals/:id/approve', 'superadmin.Distribution/approveWithdrawal');
Route::post('distribution/withdrawals/:id/reject', 'superadmin.Distribution/rejectWithdrawal');
Route::get('distribution/settings', 'superadmin.Distribution/settings');
Route::put('distribution/settings', 'superadmin.Distribution/updateSettings');
})->middleware(['cors', 'auth', 'superadmin']);
// ==================== 兼容旧版路由(保留,逐步废弃)====================
// 管理后台认证路由(不需要认证)- 兼容旧版
Route::group('api/v1', function () {
// 管理员登录(兼容旧版)
Route::post('auth/admin/login', 'admin.Auth/adminLogin');
// 超级管理员登录(兼容旧版)
Route::post('auth/superadmin/login', 'superadmin.Auth/login');
// 刷新Token兼容旧版
Route::post('auth/refresh', 'admin.Auth/refresh');
})->middleware('cors');
// 管理后台路由(需要认证)- 兼容旧版
Route::group('api/v1', function () {
// 管理员认证(兼容旧版)
Route::get('auth/me', 'admin.Auth/me');
Route::post('auth/logout', 'admin.Auth/logout');
// 仪表盘统计(兼容旧版)
Route::get('dashboard', 'admin.Dashboard/index');
// 通用上传(兼容旧版)
Route::post('upload/image', 'admin.Upload/image');
// 用户管理(兼容旧版)
Route::get('users', 'admin.User/index');
Route::get('users/:id', 'admin.User/detail');
Route::post('users', 'admin.User/create');
Route::put('users/:id', 'admin.User/update');
Route::delete('users/:id', 'admin.User/delete');
Route::put('users/:id/status', 'admin.User/toggleStatus');
// 企业管理(超管,兼容旧版)
// 注意:带参数的路由要放在不带参数的路由之前,避免路由匹配冲突
Route::get('enterprises/:id/detail', 'superadmin.Enterprise/detail'); // 详细详情接口
Route::get('enterprises/:id', 'superadmin.Enterprise/detail');
Route::get('enterprises', 'superadmin.Enterprise/index');
Route::post('enterprises', 'superadmin.Enterprise/create');
Route::put('enterprises/:id', 'superadmin.Enterprise/update');
Route::delete('enterprises/:id', 'superadmin.Enterprise/delete');
Route::put('enterprises/:id/status', 'superadmin.Enterprise/toggleStatus');
// 题库管理(兼容旧版)
Route::get('questions/:id', 'admin.Question/detail');
Route::get('questions', 'admin.Question/index');
Route::post('questions', 'admin.Question/create');
Route::put('questions/:id', 'admin.Question/update');
Route::delete('questions/:id', 'admin.Question/delete');
Route::put('questions/:id/status', 'admin.Question/toggleStatus');
Route::post('questions/batch-import', 'admin.Question/batchImport');
// 企业财务(兼容旧版)
Route::get('finance/overview', 'admin.Finance/overview');
Route::get('finance/records', 'admin.Finance/records');
Route::post('finance/recharge-qrcode', 'admin.Finance/rechargeQrcode');
Route::post('finance/recharge', 'admin.Finance/rechargeQrcode');
})->middleware(['cors', 'auth']);
// ==================== 兼容旧版路由(保留)====================
// 后台管理认证路由(不需要认证)- 兼容旧版
Route::group('api/admin', function () {
// 管理员登录
Route::post('auth/login', 'admin.Auth/login');
Route::post('auth/refresh', 'admin.Auth/refresh');
})->middleware('cors');
// 后台管理路由(需要认证)- 兼容旧版
Route::group('api/admin', function () {
// 管理员认证
Route::get('auth/me', 'admin.Auth/me');
Route::post('auth/logout', 'admin.Auth/logout');
// 仪表盘统计
Route::get('dashboard', 'admin.Dashboard/index');
// 通用上传
Route::post('upload/image', 'admin.Upload/image');
// 用户管理
Route::get('users', 'admin.User/index');
Route::get('users/:id', 'admin.User/detail');
Route::post('users', 'admin.User/create');
Route::put('users/:id', 'admin.User/update');
Route::delete('users/:id', 'admin.User/delete');
Route::put('users/:id/status', 'admin.User/toggleStatus');
// 企业财务
Route::get('finance/overview', 'admin.Finance/overview');
Route::get('finance/records', 'admin.Finance/records');
Route::post('finance/recharge-qrcode', 'admin.Finance/rechargeQrcode');
Route::post('finance/recharge', 'admin.Finance/rechargeQrcode');
})->middleware(['cors', 'auth']);
// ==================== 兼容旧版路由(保留)====================
// 后台管理认证路由(不需要认证)- 兼容旧版
Route::group('api/admin', function () {
// 管理员登录
Route::post('auth/login', 'admin.Auth/login');
Route::post('auth/refresh', 'admin.Auth/refresh');
})->middleware('cors');
// 后台管理路由(需要认证)- 兼容旧版
Route::group('api/admin', function () {
// 管理员认证
Route::get('auth/me', 'admin.Auth/me');
Route::post('auth/logout', 'admin.Auth/logout');
// 仪表盘统计
Route::get('dashboard', 'admin.Dashboard/index');
// 通用上传
Route::post('upload/image', 'admin.Upload/image');
// 用户管理
Route::get('users', 'admin.User/index');
Route::get('users/:id', 'admin.User/detail');
Route::post('users', 'admin.User/create');
Route::put('users/:id', 'admin.User/update');
Route::delete('users/:id', 'admin.User/delete');
Route::put('users/:id/status', 'admin.User/toggleStatus');
// 企业财务
Route::get('finance/overview', 'admin.Finance/overview');
Route::get('finance/records', 'admin.Finance/records');
Route::post('finance/recharge-qrcode', 'admin.Finance/rechargeQrcode');
Route::post('finance/recharge', 'admin.Finance/rechargeQrcode');
})->middleware(['cors', 'auth']);

View File

@@ -1,344 +1,348 @@
// app.js - MBTI小程序主入口
const { request } = require('./utils/request.js')
App({
globalData: {
userInfo: null,
openId: null,
token: null,
siteTitle: '神仙团队AI性格测试',
textConfig: null, // 从 /api/config/runtime 动态加载analyzingTitle, startButtonText, reportTitle, aiAnalysisText 等
// 当前使用范围personal 个人版 / enterprise 企业版(影响定价与 enterpriseId 写入)
appScope: 'personal',
// 扫码进入企业页时 scene 解析出的企业IDe_123提交测试/分析时优先使用
enterpriseIdFromScene: null,
// API基础地址开发时用本地生产环境替换为实际域名
apiBase: 'https://mbtiapi.quwanzhi.com',
//apiBase: 'http://mbti.com',
// VIP信息
vipInfo: null,
// 测试次数
testCount: 0,
// 已解锁的测试
unlockedTests: [],
// 测试结果缓存
mbtiResult: null,
discResult: null,
pdpResult: null,
aiResult: null
},
onLaunch() {
// 加载本地存储的数据
this.loadStoredData()
// 静默登录获取openId
this.silentLogin()
// 预加载站点/小程序名称(供导航栏展示)
this.getRuntimeConfig().then((cfg) => {
if (cfg && cfg.siteTitle) {
this.globalData.siteTitle = cfg.siteTitle
}
}).catch(() => {})
},
// 加载本地存储数据
loadStoredData() {
const token = wx.getStorageSync('token')
if (token) {
this.globalData.token = token
}
const userInfo = wx.getStorageSync('userInfo')
if (userInfo) {
this.globalData.userInfo = userInfo
// 优先使用后端返回的 openid 字段,而不是内部自增 id
this.globalData.openId = userInfo.openid || userInfo.openId || null
}
// VIP信息
const vipInfo = wx.getStorageSync('vipInfo')
if (vipInfo) {
this.globalData.vipInfo = vipInfo
}
// 测试次数
const testCount = wx.getStorageSync('testCount')
if (testCount) {
this.globalData.testCount = testCount
}
// 已解锁测试
const unlockedTests = wx.getStorageSync('unlockedTests')
if (unlockedTests) {
this.globalData.unlockedTests = unlockedTests
}
// 测试结果
this.globalData.mbtiResult = wx.getStorageSync('mbtiResult') || null
this.globalData.discResult = wx.getStorageSync('discResult') || null
this.globalData.pdpResult = wx.getStorageSync('pdpResult') || null
this.globalData.aiResult = wx.getStorageSync('aiResult') || null
},
/**
* 静默登录wx.login 取 code请求后端换 token 与用户信息
* @returns {Promise<boolean>} 是否登录成功(拿到 token
*/
silentLogin() {
return new Promise((resolve) => {
wx.login({
success: (res) => {
if (!res.code) {
resolve(false)
return
}
const url = `${this.globalData.apiBase}/api/auth/wechat`
wx.request({
url,
method: 'POST',
header: { 'Content-Type': 'application/json' },
data: { code: res.code },
success: (response) => {
if (response.statusCode === 200 && response.data && response.data.code === 200) {
const data = response.data.data || {}
const { token, user } = data
if (token) {
this.globalData.token = token
wx.setStorageSync('token', token)
}
if (user) {
this.globalData.userInfo = user
// 使用微信真实 openid而不是 wechat_users 表的 id
this.globalData.openId = user.openid || user.openId || null
wx.setStorageSync('userInfo', user)
}
// 登录成功后处理分销绑定(若进入时携带了推荐人参数)
if (token) {
this._tryDistributionBind()
}
resolve(!!token)
return
}
resolve(false)
},
fail: (err) => {
console.error('登录请求失败:', err)
const storedToken = wx.getStorageSync('token')
const storedUser = wx.getStorageSync('userInfo')
if (storedToken) {
this.globalData.token = storedToken
this.globalData.userInfo = storedUser || null
this.globalData.openId = storedUser ? storedUser.id : null
resolve(true)
} else {
resolve(false)
}
}
})
},
fail: () => resolve(false)
})
})
},
/**
* 确保已登录:有 token 直接 resolve否则先执行静默登录
* @returns {Promise<boolean>} 当前是否有有效登录态
*/
ensureLogin() {
if (this.globalData.token) return Promise.resolve(true)
return this.silentLogin()
},
/**
* 登录成功后尝试分销绑定(处理进入时携带的 uid 参数)
*/
_tryDistributionBind() {
const inviterId = this.globalData._pendingInviterId
const scope = this.globalData._pendingInviterScope || 'personal'
const eid = this.globalData._pendingInviterEid || null
if (!inviterId || inviterId <= 0) return
// 清除 pending 状态,避免重复绑定
this.globalData._pendingInviterId = null
this.globalData._pendingInviterScope = null
this.globalData._pendingInviterEid = null
const token = this.globalData.token
if (!token) return
const data = { inviterId, scope }
if (eid) data.eid = eid
request({
url: '/api/distribution/bind',
method: 'POST',
data,
success: () => {},
fail: () => {}
})
},
/** 清除登录态(退出登录时调用) */
logout() {
this.globalData.token = null
this.globalData.userInfo = null
this.globalData.openId = null
wx.removeStorageSync('token')
wx.removeStorageSync('userInfo')
},
// 获取用户信息(弹窗授权),并同步昵称/头像到后端
getUserInfo(callback) {
wx.getUserProfile({
desc: '用于展示用户头像和昵称',
success: (res) => {
const u = res.userInfo
this.globalData.userInfo = { ...this.globalData.userInfo, ...u, avatarUrl: u.avatarUrl || u.avatar }
wx.setStorageSync('userInfo', this.globalData.userInfo)
callback && callback(this.globalData.userInfo)
this.syncProfileToServer({ nickname: u.nickName, avatar: u.avatarUrl || u.avatar })
},
fail: () => {
callback && callback(this.globalData.userInfo || null)
}
})
},
// 同步昵称、头像到后端(需已登录)
syncProfileToServer(profile, callback) {
if (!this.globalData.token || !profile) {
callback && callback(false)
return
}
// 记录同步请求(调试用)
console.log('同步用户资料到服务器:', profile)
request({
url: '/api/auth/wechat/profile',
method: 'PUT',
data: profile,
success: (res) => {
console.log('同步用户资料响应:', res)
if (res.statusCode === 200 && res.data && res.data.code === 200) {
// 如果服务器返回了更新后的数据,使用服务器数据
if (res.data.data) {
const updatedUserInfo = { ...this.globalData.userInfo, ...res.data.data }
// 确保 nickname 和 nickName 字段都更新
if (profile.nickname) {
updatedUserInfo.nickname = profile.nickname
updatedUserInfo.nickName = profile.nickname
}
if (profile.avatar) {
updatedUserInfo.avatar = profile.avatar
updatedUserInfo.avatarUrl = profile.avatar
}
if (profile.birthday !== undefined) {
updatedUserInfo.birthday = profile.birthday
}
this.globalData.userInfo = updatedUserInfo
wx.setStorageSync('userInfo', updatedUserInfo)
console.log('用户资料已同步:', updatedUserInfo)
} else {
// 服务器没有返回数据,使用本地更新的数据
const updatedUserInfo = { ...this.globalData.userInfo, ...profile }
if (profile.nickname) {
updatedUserInfo.nickname = profile.nickname
updatedUserInfo.nickName = profile.nickname
}
if (profile.avatar) {
updatedUserInfo.avatar = profile.avatar
updatedUserInfo.avatarUrl = profile.avatar
}
if (profile.birthday !== undefined) {
updatedUserInfo.birthday = profile.birthday
}
this.globalData.userInfo = updatedUserInfo
wx.setStorageSync('userInfo', updatedUserInfo)
console.log('用户资料已更新(本地):', updatedUserInfo)
}
callback && callback(true)
} else {
console.error('更新用户资料失败:', res.data)
callback && callback(false)
}
},
fail: (err) => {
console.error('请求失败:', err)
callback && callback(false)
}
})
},
// 保存测试结果
saveTestResult(type, result) {
const key = `${type}Result`
wx.setStorageSync(key, result)
this.globalData[key] = result
// 同步到服务器(需携带 token后端从 JWT 解析 userId
if (this.globalData.token) {
const scope = this.globalData.appScope || 'personal'
const storedUser = wx.getStorageSync('userInfo') || null
const enterpriseId =
scope === 'enterprise'
? (this.globalData.enterpriseIdFromScene || (this.globalData.userInfo && this.globalData.userInfo.enterpriseId) || (storedUser && storedUser.enterpriseId) || null)
: null
wx.request({
url: `${this.globalData.apiBase}/api/test/submit`,
method: 'POST',
header: {
'Authorization': `Bearer ${this.globalData.token}`,
'Content-Type': 'application/json'
},
data: {
testType: type,
answers: result.answers || [],
result: result,
userId: this.globalData.userInfo?.id ?? this.globalData.openId,
enterpriseId: enterpriseId || undefined,
testDuration: result.testDuration || 0,
timestamp: new Date().toISOString()
}
})
}
},
// 获取测试结果
getTestResult(type) {
return this.globalData[`${type}Result`] || wx.getStorageSync(`${type}Result`) || null
},
/**
* 获取运行配置:个人/企业定价 + 当前 AI 服务商(超管配置,默认第一个启用的)
* 有 token 且用户属于企业则返回企业定价,否则个人定价
* @returns {Promise<{pricingType, pricing, aiProviderId, aiProviderName}>}
*/
getRuntimeConfig() {
return new Promise((resolve, reject) => {
const scope = this.globalData.appScope || 'personal'
const base = this.globalData.apiBase.replace(/\/$/, '')
const url = `${base}/api/config/runtime?scope=${encodeURIComponent(scope)}`
const token = this.globalData.token || wx.getStorageSync('token') || ''
wx.request({
url,
method: 'GET',
header: token ? { Authorization: 'Bearer ' + token } : {},
success: (res) => {
if (res.statusCode === 200 && res.data && res.data.code === 200) {
const data = res.data.data || {}
if (data.siteTitle) this.globalData.siteTitle = data.siteTitle
if (data.textConfig) this.globalData.textConfig = data.textConfig
resolve(data)
} else {
reject(new Error(res.data && res.data.message ? res.data.message : '获取配置失败'))
}
},
fail: reject
})
})
}
})
// app.js - MBTI小程序主入口
const { request } = require('./utils/request.js')
App({
globalData: {
userInfo: null,
openId: null,
token: null,
siteTitle: '神仙团队AI性格测试',
textConfig: null, // 从 /api/config/runtime 动态加载analyzingTitle, startButtonText, reportTitle, aiAnalysisText 等
// 当前使用范围personal 个人版 / enterprise 企业版(影响定价与 enterpriseId 写入)
appScope: 'personal',
// 扫码进入企业页时 scene 解析出的企业IDe_123提交测试/分析时优先使用
enterpriseIdFromScene: null,
// API基础地址开发时用本地生产环境替换为实际域名
apiBase: 'https://mbtiapi.quwanzhi.com',
//apiBase: 'http://mbti.com',
// VIP信息
vipInfo: null,
// 测试次数
testCount: 0,
// 已解锁的测试
unlockedTests: [],
// 测试结果缓存
mbtiResult: null,
discResult: null,
pdpResult: null,
aiResult: null,
// 审核模式true 时隐藏AI面相分析功能仅展示问卷测试
reviewMode: false
},
onLaunch() {
// 加载本地存储的数据
this.loadStoredData()
// 静默登录获取openId
this.silentLogin()
// 预加载站点/小程序名称 + 审核模式
this.getRuntimeConfig().then((cfg) => {
if (cfg) {
if (cfg.siteTitle) this.globalData.siteTitle = cfg.siteTitle
this.globalData.reviewMode = !!cfg.reviewMode
}
}).catch(() => {})
},
// 加载本地存储数据
loadStoredData() {
const token = wx.getStorageSync('token')
if (token) {
this.globalData.token = token
}
const userInfo = wx.getStorageSync('userInfo')
if (userInfo) {
this.globalData.userInfo = userInfo
// 优先使用后端返回的 openid 字段,而不是内部自增 id
this.globalData.openId = userInfo.openid || userInfo.openId || null
}
// VIP信息
const vipInfo = wx.getStorageSync('vipInfo')
if (vipInfo) {
this.globalData.vipInfo = vipInfo
}
// 测试次数
const testCount = wx.getStorageSync('testCount')
if (testCount) {
this.globalData.testCount = testCount
}
// 已解锁测试
const unlockedTests = wx.getStorageSync('unlockedTests')
if (unlockedTests) {
this.globalData.unlockedTests = unlockedTests
}
// 测试结果
this.globalData.mbtiResult = wx.getStorageSync('mbtiResult') || null
this.globalData.discResult = wx.getStorageSync('discResult') || null
this.globalData.pdpResult = wx.getStorageSync('pdpResult') || null
this.globalData.aiResult = wx.getStorageSync('aiResult') || null
},
/**
* 静默登录wx.login 取 code请求后端换 token 与用户信息
* @returns {Promise<boolean>} 是否登录成功(拿到 token
*/
silentLogin() {
return new Promise((resolve) => {
wx.login({
success: (res) => {
if (!res.code) {
resolve(false)
return
}
const url = `${this.globalData.apiBase}/api/auth/wechat`
wx.request({
url,
method: 'POST',
header: { 'Content-Type': 'application/json' },
data: { code: res.code },
success: (response) => {
if (response.statusCode === 200 && response.data && response.data.code === 200) {
const data = response.data.data || {}
const { token, user } = data
if (token) {
this.globalData.token = token
wx.setStorageSync('token', token)
}
if (user) {
this.globalData.userInfo = user
// 使用微信真实 openid而不是 wechat_users 表的 id
this.globalData.openId = user.openid || user.openId || null
wx.setStorageSync('userInfo', user)
}
// 登录成功后处理分销绑定(若进入时携带了推荐人参数)
if (token) {
this._tryDistributionBind()
}
resolve(!!token)
return
}
resolve(false)
},
fail: (err) => {
console.error('登录请求失败:', err)
const storedToken = wx.getStorageSync('token')
const storedUser = wx.getStorageSync('userInfo')
if (storedToken) {
this.globalData.token = storedToken
this.globalData.userInfo = storedUser || null
this.globalData.openId = storedUser ? storedUser.id : null
resolve(true)
} else {
resolve(false)
}
}
})
},
fail: () => resolve(false)
})
})
},
/**
* 确保已登录:有 token 直接 resolve否则先执行静默登录
* @returns {Promise<boolean>} 当前是否有有效登录态
*/
ensureLogin() {
if (this.globalData.token) return Promise.resolve(true)
return this.silentLogin()
},
/**
* 登录成功后尝试分销绑定(处理进入时携带的 uid 参数)
*/
_tryDistributionBind() {
const inviterId = this.globalData._pendingInviterId
const scope = this.globalData._pendingInviterScope || 'personal'
const eid = this.globalData._pendingInviterEid || null
if (!inviterId || inviterId <= 0) return
// 清除 pending 状态,避免重复绑定
this.globalData._pendingInviterId = null
this.globalData._pendingInviterScope = null
this.globalData._pendingInviterEid = null
const token = this.globalData.token
if (!token) return
const data = { inviterId, scope }
if (eid) data.eid = eid
request({
url: '/api/distribution/bind',
method: 'POST',
data,
success: () => {},
fail: () => {}
})
},
/** 清除登录态(退出登录时调用) */
logout() {
this.globalData.token = null
this.globalData.userInfo = null
this.globalData.openId = null
wx.removeStorageSync('token')
wx.removeStorageSync('userInfo')
},
// 获取用户信息(弹窗授权),并同步昵称/头像到后端
getUserInfo(callback) {
wx.getUserProfile({
desc: '用于展示用户头像和昵称',
success: (res) => {
const u = res.userInfo
this.globalData.userInfo = { ...this.globalData.userInfo, ...u, avatarUrl: u.avatarUrl || u.avatar }
wx.setStorageSync('userInfo', this.globalData.userInfo)
callback && callback(this.globalData.userInfo)
this.syncProfileToServer({ nickname: u.nickName, avatar: u.avatarUrl || u.avatar })
},
fail: () => {
callback && callback(this.globalData.userInfo || null)
}
})
},
// 同步昵称、头像到后端(需已登录)
syncProfileToServer(profile, callback) {
if (!this.globalData.token || !profile) {
callback && callback(false)
return
}
// 记录同步请求(调试用)
console.log('同步用户资料到服务器:', profile)
request({
url: '/api/auth/wechat/profile',
method: 'PUT',
data: profile,
success: (res) => {
console.log('同步用户资料响应:', res)
if (res.statusCode === 200 && res.data && res.data.code === 200) {
// 如果服务器返回了更新后的数据,使用服务器数据
if (res.data.data) {
const updatedUserInfo = { ...this.globalData.userInfo, ...res.data.data }
// 确保 nicknamenickName 字段都更新
if (profile.nickname) {
updatedUserInfo.nickname = profile.nickname
updatedUserInfo.nickName = profile.nickname
}
if (profile.avatar) {
updatedUserInfo.avatar = profile.avatar
updatedUserInfo.avatarUrl = profile.avatar
}
if (profile.birthday !== undefined) {
updatedUserInfo.birthday = profile.birthday
}
this.globalData.userInfo = updatedUserInfo
wx.setStorageSync('userInfo', updatedUserInfo)
console.log('用户资料已同步:', updatedUserInfo)
} else {
// 服务器没有返回数据,使用本地更新的数据
const updatedUserInfo = { ...this.globalData.userInfo, ...profile }
if (profile.nickname) {
updatedUserInfo.nickname = profile.nickname
updatedUserInfo.nickName = profile.nickname
}
if (profile.avatar) {
updatedUserInfo.avatar = profile.avatar
updatedUserInfo.avatarUrl = profile.avatar
}
if (profile.birthday !== undefined) {
updatedUserInfo.birthday = profile.birthday
}
this.globalData.userInfo = updatedUserInfo
wx.setStorageSync('userInfo', updatedUserInfo)
console.log('用户资料已更新(本地):', updatedUserInfo)
}
callback && callback(true)
} else {
console.error('更新用户资料失败:', res.data)
callback && callback(false)
}
},
fail: (err) => {
console.error('请求失败:', err)
callback && callback(false)
}
})
},
// 保存测试结果
saveTestResult(type, result) {
const key = `${type}Result`
wx.setStorageSync(key, result)
this.globalData[key] = result
// 同步到服务器(需携带 token后端从 JWT 解析 userId
if (this.globalData.token) {
const scope = this.globalData.appScope || 'personal'
const storedUser = wx.getStorageSync('userInfo') || null
const enterpriseId =
scope === 'enterprise'
? (this.globalData.enterpriseIdFromScene || (this.globalData.userInfo && this.globalData.userInfo.enterpriseId) || (storedUser && storedUser.enterpriseId) || null)
: null
wx.request({
url: `${this.globalData.apiBase}/api/test/submit`,
method: 'POST',
header: {
'Authorization': `Bearer ${this.globalData.token}`,
'Content-Type': 'application/json'
},
data: {
testType: type,
answers: result.answers || [],
result: result,
userId: this.globalData.userInfo?.id ?? this.globalData.openId,
enterpriseId: enterpriseId || undefined,
testDuration: result.testDuration || 0,
timestamp: new Date().toISOString()
}
})
}
},
// 获取测试结果
getTestResult(type) {
return this.globalData[`${type}Result`] || wx.getStorageSync(`${type}Result`) || null
},
/**
* 获取运行配置:个人/企业定价 + 当前 AI 服务商(超管配置,默认第一个启用的)
* 有 token 且用户属于企业则返回企业定价,否则个人定价
* @returns {Promise<{pricingType, pricing, aiProviderId, aiProviderName}>}
*/
getRuntimeConfig() {
return new Promise((resolve, reject) => {
const scope = this.globalData.appScope || 'personal'
const base = this.globalData.apiBase.replace(/\/$/, '')
const url = `${base}/api/config/runtime?scope=${encodeURIComponent(scope)}`
const token = this.globalData.token || wx.getStorageSync('token') || ''
wx.request({
url,
method: 'GET',
header: token ? { Authorization: 'Bearer ' + token } : {},
success: (res) => {
if (res.statusCode === 200 && res.data && res.data.code === 200) {
const data = res.data.data || {}
if (data.siteTitle) this.globalData.siteTitle = data.siteTitle
if (data.textConfig) this.globalData.textConfig = data.textConfig
this.globalData.reviewMode = !!data.reviewMode
resolve(data)
} else {
reject(new Error(res.data && res.data.message ? res.data.message : '获取配置失败'))
}
},
fail: reject
})
})
}
})

View File

@@ -1,64 +1,64 @@
{
"pages": [
"pages/index/index",
"pages/index/camera",
"pages/index/upload",
"pages/index/result",
"pages/test-select/index",
"pages/test/mbti",
"pages/test/disc",
"pages/test/pdp",
"pages/result/mbti",
"pages/result/disc",
"pages/result/pdp",
"pages/result/resume",
"pages/purchase/index",
"pages/recharge/index",
"pages/enterprise/index",
"pages/enterprise/resume-history",
"pages/profile/index",
"pages/user-profile/index",
"pages/history/index",
"pages/phone-auth/index",
"pages/promo/index",
"pages/promo/poster",
"pages/promo/withdrawals"
],
"tabBar": {
"custom": true,
"color": "#999999",
"selectedColor": "#7c3aed",
"backgroundColor": "#ffffff",
"borderStyle": "black",
"list": [
{
"pagePath": "pages/index/index",
"text": "首页",
"iconPath": "images/home.png",
"selectedIconPath": "images/home-active.png"
},
{
"pagePath": "pages/index/camera",
"text": "查看报告",
"iconPath": "images/camera.png",
"selectedIconPath": "images/camera-active.png"
},
{
"pagePath": "pages/profile/index",
"text": "我的",
"iconPath": "images/user.png",
"selectedIconPath": "images/user-active.png"
}
]
},
"window": {
"backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#FFFFFF",
"navigationBarTitleText": "神仙团队AI性格测试",
"navigationBarTextStyle": "black",
"backgroundColor": "#F5F5F5"
},
"style": "v2",
"sitemapLocation": "sitemap.json",
"lazyCodeLoading": "requiredComponents"
}
{
"pages": [
"pages/index/index",
"pages/index/camera",
"pages/index/upload",
"pages/index/result",
"pages/test-select/index",
"pages/test/mbti",
"pages/test/disc",
"pages/test/pdp",
"pages/result/mbti",
"pages/result/disc",
"pages/result/pdp",
"pages/result/resume",
"pages/purchase/index",
"pages/recharge/index",
"pages/enterprise/index",
"pages/enterprise/resume-history",
"pages/profile/index",
"pages/user-profile/index",
"pages/history/index",
"pages/phone-auth/index",
"pages/promo/index",
"pages/promo/poster",
"pages/promo/withdrawals"
],
"tabBar": {
"custom": true,
"color": "#999999",
"selectedColor": "#7c3aed",
"backgroundColor": "#ffffff",
"borderStyle": "black",
"list": [
{
"pagePath": "pages/index/index",
"text": "首页",
"iconPath": "images/home.png",
"selectedIconPath": "images/home-active.png"
},
{
"pagePath": "pages/index/camera",
"text": "查看报告",
"iconPath": "images/camera.png",
"selectedIconPath": "images/camera-active.png"
},
{
"pagePath": "pages/profile/index",
"text": "我的",
"iconPath": "images/user.png",
"selectedIconPath": "images/user-active.png"
}
]
},
"window": {
"backgroundTextStyle": "light",
"navigationBarBackgroundColor": "#FFFFFF",
"navigationBarTitleText": "神仙团队性格测试",
"navigationBarTextStyle": "black",
"backgroundColor": "#F5F5F5"
},
"style": "v2",
"sitemapLocation": "sitemap.json",
"lazyCodeLoading": "requiredComponents"
}

View File

@@ -1,74 +1,80 @@
// 自定义 tabBar不使用微信内置灰线 + 三栏,中间为浮起圆钮
Component({
data: {
selected: 0,
list: [
{ pagePath: '/pages/index/index', text: '首页', textKey: 'home', icon: 'home' },
{ pagePath: '/pages/index/camera', text: '查看报告', textKey: 'camera', icon: 'camera' },
{ pagePath: '/pages/profile/index', text: '我的', textKey: 'profile', icon: 'user' }
]
},
lifetimes: {
attached() {
this.updateSelected()
}
},
pageLifetimes: {
show() {
this.updateSelected()
}
},
methods: {
updateSelected() {
try {
const pages = getCurrentPages()
if (!pages || pages.length === 0) {
return
}
const currentPage = pages[pages.length - 1]
if (!currentPage || !currentPage.route) {
return
}
const url = currentPage.route
let selected = 0
if (url === 'pages/index/index' || url === 'pages/enterprise/index') {
selected = 0
} else if (url === 'pages/index/camera') {
selected = 1
} else if (url === 'pages/profile/index') {
selected = 2
}
this.setData({ selected })
} catch (error) {
console.error('updateSelected error:', error)
// 默认选中第一个
this.setData({ selected: 0 })
}
},
switchTab(e) {
const index = parseInt(e.currentTarget.dataset.index, 10)
let url = e.currentTarget.dataset.path
// 点击"首页"index=0根据当前 scope 跳到企业版或个人版首页
if (index === 0) {
try {
const app = getApp()
const scope = (app && app.globalData && app.globalData.appScope) || 'personal'
if (scope === 'enterprise') {
// 企业版:navigateTo(不是 tabBar 页面,不能用 switchTab
wx.navigateTo({ url: '/pages/enterprise/index' })
this.setData({ selected: index })
return
}
} catch (e) {}
}
wx.switchTab({ url })
this.setData({ selected: index })
}
}
})
// 自定义 tabBar不使用微信内置灰线 + 三栏,中间为浮起圆钮
Component({
data: {
selected: 0,
reviewMode: false,
list: [
{ pagePath: '/pages/index/index', text: '首页', textKey: 'home', icon: 'home' },
{ pagePath: '/pages/index/camera', text: '查看报告', textKey: 'camera', icon: 'camera' },
{ pagePath: '/pages/profile/index', text: '我的', textKey: 'profile', icon: 'user' }
]
},
lifetimes: {
attached() {
this.updateSelected()
this.checkReviewMode()
}
},
pageLifetimes: {
show() {
this.updateSelected()
this.checkReviewMode()
}
},
methods: {
checkReviewMode() {
try {
const app = getApp()
const rm = !!(app && app.globalData && app.globalData.reviewMode)
this.setData({ reviewMode: rm })
} catch (e) {}
},
updateSelected() {
try {
const pages = getCurrentPages()
if (!pages || pages.length === 0) return
const currentPage = pages[pages.length - 1]
if (!currentPage || !currentPage.route) return
const url = currentPage.route
let selected = 0
if (url === 'pages/index/index' || url === 'pages/enterprise/index') {
selected = 0
} else if (url === 'pages/index/camera') {
selected = 1
} else if (url === 'pages/profile/index') {
selected = 2
}
this.setData({ selected })
} catch (error) {
console.error('updateSelected error:', error)
this.setData({ selected: 0 })
}
},
switchTab(e) {
const index = parseInt(e.currentTarget.dataset.index, 10)
let url = e.currentTarget.dataset.path
if (index === 0) {
try {
const app = getApp()
const scope = (app && app.globalData && app.globalData.appScope) || 'personal'
if (scope === 'enterprise') {
wx.navigateTo({ url: '/pages/enterprise/index' })
this.setData({ selected: index })
return
}
} catch (e) {}
}
// 审核模式:中间按钮跳转到测试选择页而非相机
if (index === 1 && this.data.reviewMode) {
wx.navigateTo({ url: '/pages/test-select/index' })
this.setData({ selected: index })
return
}
wx.switchTab({ url })
this.setData({ selected: index })
}
}
})

View File

@@ -1,35 +1,48 @@
<!-- 自定义 tabBar顶部分割线 + 三栏,中间浮起圆钮 -->
<view class="tab-bar">
<view class="tab-bar-line"></view>
<view class="tab-bar-inner">
<view
class="tab-item {{selected === 0 ? 'active' : ''}}"
data-index="0"
data-path="/pages/index/index"
bindtap="switchTab"
>
<image class="tab-icon" src="{{selected === 0 ? '/images/home-active.png' : '/images/home.png'}}" mode="aspectFit" />
<text class="tab-text">首页</text>
</view>
<view
class="tab-item-center {{selected === 1 ? 'active' : ''}}"
data-index="1"
data-path="/pages/index/camera"
bindtap="switchTab"
>
<view class="center-circle">
<image class="center-icon" src="{{selected === 1 ? '/images/camera-active.png' : '/images/camera.png'}}" mode="aspectFit" />
</view>
<view class="center-text-placeholder"></view>
</view>
<view
class="tab-item {{selected === 2 ? 'active' : ''}}"
data-index="2"
data-path="/pages/profile/index"
bindtap="switchTab"
>
<image class="tab-icon" src="{{selected === 2 ? '/images/user-active.png' : '/images/user.png'}}" mode="aspectFit" />
<text class="tab-text">我的</text>
</view>
</view>
</view>
<!-- 自定义 tabBar顶部分割线 + 三栏,中间浮起圆钮 -->
<view class="tab-bar">
<view class="tab-bar-line"></view>
<view class="tab-bar-inner">
<view
class="tab-item {{selected === 0 ? 'active' : ''}}"
data-index="0"
data-path="/pages/index/index"
bindtap="switchTab"
>
<image class="tab-icon" src="{{selected === 0 ? '/images/home-active.png' : '/images/home.png'}}" mode="aspectFit" />
<text class="tab-text">首页</text>
</view>
<!-- 审核模式:中间按钮显示"测试"文字 -->
<view
wx:if="{{reviewMode}}"
class="tab-item {{selected === 1 ? 'active' : ''}}"
data-index="1"
data-path="/pages/test-select/index"
bindtap="switchTab"
>
<image class="tab-icon" src="{{selected === 1 ? '/images/home-active.png' : '/images/home.png'}}" mode="aspectFit" />
<text class="tab-text">测试</text>
</view>
<!-- 正常模式:中间浮起拍照按钮 -->
<view
wx:else
class="tab-item-center {{selected === 1 ? 'active' : ''}}"
data-index="1"
data-path="/pages/index/camera"
bindtap="switchTab"
>
<view class="center-circle">
<image class="center-icon" src="{{selected === 1 ? '/images/camera-active.png' : '/images/camera.png'}}" mode="aspectFit" />
</view>
<view class="center-text-placeholder"></view>
</view>
<view
class="tab-item {{selected === 2 ? 'active' : ''}}"
data-index="2"
data-path="/pages/profile/index"
bindtap="switchTab"
>
<image class="tab-icon" src="{{selected === 2 ? '/images/user-active.png' : '/images/user.png'}}" mode="aspectFit" />
<text class="tab-text">我的</text>
</view>
</view>
</view>

View File

@@ -1,161 +1,162 @@
// pages/history/index.js - 测试历史记录(一次拉取全部,时间行右侧展示企业名)
const app = getApp()
Page({
data: {
activeTab: 'all',
tabName: '',
list: [],
total: 0,
loading: false,
isEnterprise: false
},
_checkIsEnterprise() {
const gd = app.globalData || {}
const storedUser = wx.getStorageSync('userInfo') || null
const scope = gd.appScope || 'personal'
const enterpriseId = gd.enterpriseIdFromScene
|| (gd.userInfo && gd.userInfo.enterpriseId)
|| (storedUser && storedUser.enterpriseId)
|| null
return scope === 'enterprise' || !!enterpriseId
},
onLoad() {
this.setData({ isEnterprise: this._checkIsEnterprise() })
this.loadAll()
},
onShow() {
this.setData({ isEnterprise: this._checkIsEnterprise() })
this.loadAll()
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({ selected: 1 })
}
},
// 一次拉取全部历史pageSize=500
loadAll() {
if (this.data.loading) return
this.setData({ loading: true, list: [] })
const token = app.globalData.token || wx.getStorageSync('token')
const apiBase = app.globalData.apiBase
const { activeTab } = this.data
const typeParam = activeTab === 'all' ? '' : `&type=${activeTab}`
if (!token || !apiBase) {
this.setData({ loading: false })
this.loadFromStorage()
return
}
wx.request({
url: `${apiBase}/api/test/history?page=1&pageSize=500${typeParam}&scope=all`,
method: 'GET',
header: { Authorization: `Bearer ${token}` },
success: (res) => {
if (res.statusCode === 200 && res.data && res.data.data) {
const payload = res.data.data
const rawList = Array.isArray(payload) ? payload : (payload.list || [])
const total = Array.isArray(payload) ? rawList.length : (payload.total || 0)
const formatted = this.formatList(rawList)
this.setData({ list: formatted, total, loading: false })
} else {
this.setData({ loading: false })
this.loadFromStorage()
}
},
fail: () => {
this.setData({ loading: false })
this.loadFromStorage()
}
})
},
formatList(rawList) {
const typeNames = { mbti: 'MBTI性格测试', disc: 'DISC性格测试', pdp: 'PDP行为偏好测试', ai: '面相分析', resume: '简历综合分析' }
const emojis = { mbti: '🧠', disc: '📊', pdp: '🦁', ai: '👁️', resume: '📋' }
return rawList.map((item, idx) => {
if (item.typeName) {
return { ...item, enterpriseName: item.enterpriseName || '' }
}
const testType = (item.testType || item.type || 'mbti').toLowerCase()
const ts = item.createdAt || item.testTime || item.timestamp
return {
...item,
type: testType,
key: testType + '_' + (item.id || idx),
emoji: emojis[testType] || '📋',
typeName: typeNames[testType] || '测试',
testTime: ts ? this.formatTime(typeof ts === 'number' ? ts * 1000 : ts) : '',
enterpriseName: item.enterpriseName || ''
}
})
},
// 本地缓存回退
loadFromStorage() {
const mbtiResult = wx.getStorageSync('mbtiResult')
const discResult = wx.getStorageSync('discResult')
const pdpResult = wx.getStorageSync('pdpResult')
const aiResult = wx.getStorageSync('aiResult')
const list = []
if (mbtiResult) list.push({ type: 'mbti', key: 'mbti', emoji: '🧠', typeName: 'MBTI性格测试', resultText: mbtiResult.mbtiType || '未知', testTime: this.formatTime(mbtiResult.timestamp), data: mbtiResult })
if (pdpResult) list.push({ type: 'pdp', key: 'pdp', emoji: pdpResult.description?.emoji || '🦁', typeName: 'PDP行为偏好测试', resultText: pdpResult.description?.type || '未知', testTime: this.formatTime(pdpResult.timestamp || pdpResult.completedAt), data: pdpResult })
if (discResult) list.push({ type: 'disc', key: 'disc', emoji: '📊', typeName: 'DISC性格测试', resultText: (discResult.dominantType || '未知') + '型', testTime: this.formatTime(discResult.timestamp || discResult.completedAt), data: discResult })
if (aiResult) list.push({ type: 'ai', key: 'ai', emoji: '👁️', typeName: '面相分析', resultText: aiResult.mbti || '未知', testTime: this.formatTime(aiResult.timestamp || aiResult.completedAt), data: aiResult })
this.setData({ list, total: list.length, loading: false })
},
changeTab(e) {
const tab = e.currentTarget.dataset.tab
const names = { all: '', mbti: 'MBTI', pdp: 'PDP', disc: 'DISC', ai: '面相', resume: '简历' }
this.setData({ activeTab: tab, tabName: names[tab] || '' })
this.loadAll()
},
formatTime(timestamp) {
if (!timestamp) return '未知时间'
const date = new Date(timestamp)
const y = date.getFullYear()
const m = String(date.getMonth() + 1).padStart(2, '0')
const d = String(date.getDate()).padStart(2, '0')
const h = String(date.getHours()).padStart(2, '0')
const min = String(date.getMinutes()).padStart(2, '0')
return `${y}-${m}-${d} ${h}:${min}`
},
viewDetail(e) {
const type = e.currentTarget.dataset.type
const id = e.currentTarget.dataset.id
const routes = {
mbti: '/pages/result/mbti',
disc: '/pages/result/disc',
pdp: '/pages/result/pdp',
ai: '/pages/index/result',
resume: '/pages/result/resume'
}
const base = routes[type]
if (!base) return
if ((type === 'ai' || type === 'resume') && !id) return
const query = id ? `?id=${id}&type=${type}` : ''
wx.navigateTo({ url: query ? base + query : base })
},
goToTest() {
const routes = {
mbti: '/pages/test/mbti',
disc: '/pages/test/disc',
pdp: '/pages/test/pdp',
ai: '/pages/index/camera',
resume: '/pages/enterprise/index'
}
const url = routes[this.data.activeTab]
if (url) wx.navigateTo({ url })
else wx.switchTab({ url: '/pages/index/index' })
},
})
// pages/history/index.js - 测试历史记录(一次拉取全部,时间行右侧展示企业名)
const app = getApp()
Page({
data: {
activeTab: 'all',
tabName: '',
list: [],
total: 0,
loading: false,
isEnterprise: false,
reviewMode: false
},
_checkIsEnterprise() {
const gd = app.globalData || {}
const storedUser = wx.getStorageSync('userInfo') || null
const scope = gd.appScope || 'personal'
const enterpriseId = gd.enterpriseIdFromScene
|| (gd.userInfo && gd.userInfo.enterpriseId)
|| (storedUser && storedUser.enterpriseId)
|| null
return scope === 'enterprise' || !!enterpriseId
},
onLoad() {
this.setData({ isEnterprise: this._checkIsEnterprise() })
this.loadAll()
},
onShow() {
this.setData({ isEnterprise: this._checkIsEnterprise(), reviewMode: !!app.globalData.reviewMode })
this.loadAll()
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({ selected: 1 })
}
},
// 一次拉取全部历史pageSize=500
loadAll() {
if (this.data.loading) return
this.setData({ loading: true, list: [] })
const token = app.globalData.token || wx.getStorageSync('token')
const apiBase = app.globalData.apiBase
const { activeTab } = this.data
const typeParam = activeTab === 'all' ? '' : `&type=${activeTab}`
if (!token || !apiBase) {
this.setData({ loading: false })
this.loadFromStorage()
return
}
wx.request({
url: `${apiBase}/api/test/history?page=1&pageSize=500${typeParam}&scope=all`,
method: 'GET',
header: { Authorization: `Bearer ${token}` },
success: (res) => {
if (res.statusCode === 200 && res.data && res.data.data) {
const payload = res.data.data
const rawList = Array.isArray(payload) ? payload : (payload.list || [])
const total = Array.isArray(payload) ? rawList.length : (payload.total || 0)
const formatted = this.formatList(rawList)
this.setData({ list: formatted, total, loading: false })
} else {
this.setData({ loading: false })
this.loadFromStorage()
}
},
fail: () => {
this.setData({ loading: false })
this.loadFromStorage()
}
})
},
formatList(rawList) {
const typeNames = { mbti: 'MBTI性格测试', disc: 'DISC性格测试', pdp: 'PDP行为偏好测试', ai: '面相分析', resume: '简历综合分析' }
const emojis = { mbti: '🧠', disc: '📊', pdp: '🦁', ai: '👁️', resume: '📋' }
return rawList.map((item, idx) => {
if (item.typeName) {
return { ...item, enterpriseName: item.enterpriseName || '' }
}
const testType = (item.testType || item.type || 'mbti').toLowerCase()
const ts = item.createdAt || item.testTime || item.timestamp
return {
...item,
type: testType,
key: testType + '_' + (item.id || idx),
emoji: emojis[testType] || '📋',
typeName: typeNames[testType] || '测试',
testTime: ts ? this.formatTime(typeof ts === 'number' ? ts * 1000 : ts) : '',
enterpriseName: item.enterpriseName || ''
}
})
},
// 本地缓存回退
loadFromStorage() {
const mbtiResult = wx.getStorageSync('mbtiResult')
const discResult = wx.getStorageSync('discResult')
const pdpResult = wx.getStorageSync('pdpResult')
const aiResult = wx.getStorageSync('aiResult')
const list = []
if (mbtiResult) list.push({ type: 'mbti', key: 'mbti', emoji: '🧠', typeName: 'MBTI性格测试', resultText: mbtiResult.mbtiType || '未知', testTime: this.formatTime(mbtiResult.timestamp), data: mbtiResult })
if (pdpResult) list.push({ type: 'pdp', key: 'pdp', emoji: pdpResult.description?.emoji || '🦁', typeName: 'PDP行为偏好测试', resultText: pdpResult.description?.type || '未知', testTime: this.formatTime(pdpResult.timestamp || pdpResult.completedAt), data: pdpResult })
if (discResult) list.push({ type: 'disc', key: 'disc', emoji: '📊', typeName: 'DISC性格测试', resultText: (discResult.dominantType || '未知') + '型', testTime: this.formatTime(discResult.timestamp || discResult.completedAt), data: discResult })
if (aiResult && !app.globalData.reviewMode) list.push({ type: 'ai', key: 'ai', emoji: '👁️', typeName: '面相分析', resultText: aiResult.mbti || '未知', testTime: this.formatTime(aiResult.timestamp || aiResult.completedAt), data: aiResult })
this.setData({ list, total: list.length, loading: false })
},
changeTab(e) {
const tab = e.currentTarget.dataset.tab
const names = { all: '', mbti: 'MBTI', pdp: 'PDP', disc: 'DISC', ai: '面相', resume: '简历' }
this.setData({ activeTab: tab, tabName: names[tab] || '' })
this.loadAll()
},
formatTime(timestamp) {
if (!timestamp) return '未知时间'
const date = new Date(timestamp)
const y = date.getFullYear()
const m = String(date.getMonth() + 1).padStart(2, '0')
const d = String(date.getDate()).padStart(2, '0')
const h = String(date.getHours()).padStart(2, '0')
const min = String(date.getMinutes()).padStart(2, '0')
return `${y}-${m}-${d} ${h}:${min}`
},
viewDetail(e) {
const type = e.currentTarget.dataset.type
const id = e.currentTarget.dataset.id
const routes = {
mbti: '/pages/result/mbti',
disc: '/pages/result/disc',
pdp: '/pages/result/pdp',
ai: '/pages/index/result',
resume: '/pages/result/resume'
}
const base = routes[type]
if (!base) return
if ((type === 'ai' || type === 'resume') && !id) return
const query = id ? `?id=${id}&type=${type}` : ''
wx.navigateTo({ url: query ? base + query : base })
},
goToTest() {
const routes = {
mbti: '/pages/test/mbti',
disc: '/pages/test/disc',
pdp: '/pages/test/pdp',
ai: '/pages/index/camera',
resume: '/pages/enterprise/index'
}
const url = routes[this.data.activeTab]
if (url) wx.navigateTo({ url })
else wx.switchTab({ url: '/pages/index/index' })
},
})

View File

@@ -1,74 +1,74 @@
<!--pages/history/index.wxml - 测试历史记录(按旧版模板重构)-->
<view class="container">
<!-- 测试类型标签 -->
<view class="tabs-container">
<scroll-view scroll-x class="tabs-scroll">
<view class="tabs">
<view class="tab-item {{activeTab === 'all' ? 'active' : ''}}" bindtap="changeTab" data-tab="all">
<text class="tab-text">全部</text>
</view>
<view class="tab-item {{activeTab === 'mbti' ? 'active' : ''}}" bindtap="changeTab" data-tab="mbti">
<text class="tab-text">MBTI</text>
</view>
<view class="tab-item {{activeTab === 'pdp' ? 'active' : ''}}" bindtap="changeTab" data-tab="pdp">
<text class="tab-text">PDP</text>
</view>
<view class="tab-item {{activeTab === 'disc' ? 'active' : ''}}" bindtap="changeTab" data-tab="disc">
<text class="tab-text">DISC</text>
</view>
<view class="tab-item {{activeTab === 'ai' ? 'active' : ''}}" bindtap="changeTab" data-tab="ai">
<text class="tab-text">面相</text>
</view>
<view wx:if="{{isEnterprise}}" class="tab-item {{activeTab === 'resume' ? 'active' : ''}}" bindtap="changeTab" data-tab="resume">
<text class="tab-text">简历</text>
</view>
</view>
</scroll-view>
</view>
<!-- 空状态 -->
<view class="empty-container" wx:if="{{list.length === 0 && !loading}}">
<text class="empty-icon">📋</text>
<text class="empty-text">暂无{{activeTab === 'all' ? '' : tabName}}历史记录</text>
<view class="primary-button" bindtap="goToTest">
<text class="button-text">开始测试</text>
</view>
</view>
<!-- 测试记录列表 -->
<view class="test-list" wx:if="{{list.length > 0}}">
<view class="test-card" wx:for="{{list}}" wx:key="key" bindtap="viewDetail" data-type="{{item.type}}" data-index="{{index}}" data-id="{{item.id}}">
<view class="card-content">
<view class="icon-circle icon-{{item.type}}">
<text class="icon-emoji">{{item.emoji}}</text>
</view>
<view class="test-details">
<view class="test-header-row">
<text class="test-title">{{item.typeName}}</text>
<view class="result-badge badge-{{item.type}}">
<text class="badge-text">{{item.resultText}}</text>
</view>
</view>
<view class="test-time-row">
<text class="test-time">{{item.testTime}}</text>
<text class="test-enterprise" wx:if="{{item.enterpriseName}}">{{item.enterpriseName}}</text>
</view>
</view>
</view>
</view>
<!-- 加载中 -->
<view class="load-more-row" wx:if="{{loading}}">
<text class="load-more-text">加载中...</text>
</view>
<view wx:elif="{{list.length > 0}}" class="load-more-row">
<text class="load-more-text no-more">共 {{total}} 条</text>
</view>
</view>
<!-- 首次加载骨架 -->
<view class="empty-container" wx:if="{{loading && list.length === 0}}">
<text class="load-more-text">加载中...</text>
</view>
</view>
<!--pages/history/index.wxml - 测试历史记录(按旧版模板重构)-->
<view class="container">
<!-- 测试类型标签 -->
<view class="tabs-container">
<scroll-view scroll-x class="tabs-scroll">
<view class="tabs">
<view class="tab-item {{activeTab === 'all' ? 'active' : ''}}" bindtap="changeTab" data-tab="all">
<text class="tab-text">全部</text>
</view>
<view class="tab-item {{activeTab === 'mbti' ? 'active' : ''}}" bindtap="changeTab" data-tab="mbti">
<text class="tab-text">MBTI</text>
</view>
<view class="tab-item {{activeTab === 'pdp' ? 'active' : ''}}" bindtap="changeTab" data-tab="pdp">
<text class="tab-text">PDP</text>
</view>
<view class="tab-item {{activeTab === 'disc' ? 'active' : ''}}" bindtap="changeTab" data-tab="disc">
<text class="tab-text">DISC</text>
</view>
<view wx:if="{{!reviewMode}}" class="tab-item {{activeTab === 'ai' ? 'active' : ''}}" bindtap="changeTab" data-tab="ai">
<text class="tab-text">面相</text>
</view>
<view wx:if="{{isEnterprise}}" class="tab-item {{activeTab === 'resume' ? 'active' : ''}}" bindtap="changeTab" data-tab="resume">
<text class="tab-text">简历</text>
</view>
</view>
</scroll-view>
</view>
<!-- 空状态 -->
<view class="empty-container" wx:if="{{list.length === 0 && !loading}}">
<text class="empty-icon">📋</text>
<text class="empty-text">暂无{{activeTab === 'all' ? '' : tabName}}历史记录</text>
<view class="primary-button" bindtap="goToTest">
<text class="button-text">开始测试</text>
</view>
</view>
<!-- 测试记录列表 -->
<view class="test-list" wx:if="{{list.length > 0}}">
<view class="test-card" wx:for="{{list}}" wx:key="key" bindtap="viewDetail" data-type="{{item.type}}" data-index="{{index}}" data-id="{{item.id}}">
<view class="card-content">
<view class="icon-circle icon-{{item.type}}">
<text class="icon-emoji">{{item.emoji}}</text>
</view>
<view class="test-details">
<view class="test-header-row">
<text class="test-title">{{item.typeName}}</text>
<view class="result-badge badge-{{item.type}}">
<text class="badge-text">{{item.resultText}}</text>
</view>
</view>
<view class="test-time-row">
<text class="test-time">{{item.testTime}}</text>
<text class="test-enterprise" wx:if="{{item.enterpriseName}}">{{item.enterpriseName}}</text>
</view>
</view>
</view>
</view>
<!-- 加载中 -->
<view class="load-more-row" wx:if="{{loading}}">
<text class="load-more-text">加载中...</text>
</view>
<view wx:elif="{{list.length > 0}}" class="load-more-row">
<text class="load-more-text no-more">共 {{total}} 条</text>
</view>
</view>
<!-- 首次加载骨架 -->
<view class="empty-container" wx:if="{{loading && list.length === 0}}">
<text class="load-more-text">加载中...</text>
</view>
</view>

View File

@@ -1,196 +1,201 @@
// pages/index/camera.js - 面相分析拍照页,拍完后上传到服务器再跳转结果页
const app = getApp()
const { hasPhone, bindPhoneByCode, ensureProfileCompleteAndRedirect } = require('../../utils/phoneAuth.js')
Page({
data: {
photos: [],
photoIndex: 0,
guideTexts: ['请正对镜头', '请向左转45°', '请向右转45°'],
guideText: '请正对镜头',
uploading: false,
needPhoneAuth: false,
aiAnalysisText: '智能分析'
},
onLoad() {
this.cameraContext = wx.createCameraContext()
const tc = app.globalData.textConfig
if (tc && tc.aiAnalysisText) {
this.setData({ aiAnalysisText: tc.aiAnalysisText })
} else {
app.getRuntimeConfig().then((cfg) => {
if (cfg && cfg.textConfig) {
app.globalData.textConfig = cfg.textConfig
this.setData({ aiAnalysisText: cfg.textConfig.aiAnalysisText || '智能分析' })
}
}).catch(() => {})
}
},
onShow() {
if (!ensureProfileCompleteAndRedirect()) return
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({ selected: 1 })
}
this.setData({ needPhoneAuth: !hasPhone() })
const tc = app.globalData.textConfig
if (tc && tc.aiAnalysisText) {
this.setData({ aiAnalysisText: tc.aiAnalysisText })
}
},
// 本页就地授权手机号
onGetPhoneNumber(e) {
const { code, errMsg } = e.detail || {}
if (errMsg && errMsg.indexOf('getPhoneNumber:fail') === 0) {
wx.showToast({ title: '需要授权手机号才能继续', icon: 'none' })
return
}
if (!code && !hasPhone()) {
wx.showToast({ title: '获取手机号失败', icon: 'none' })
return
}
if (!code && hasPhone()) {
// 已有手机号,无需重复请求
this.setData({ needPhoneAuth: false })
return
}
bindPhoneByCode(code).then(() => {
this.setData({ needPhoneAuth: false })
}).catch(() => {
// 失败时保持 needPhoneAuth 为 true等待用户重新授权
this.setData({ needPhoneAuth: !hasPhone() })
})
},
// 拍照
takePhoto() {
if (this.data.photos.length >= 3) {
wx.showToast({ title: '已拍满3张', icon: 'none' })
return
}
this.cameraContext.takePhoto({
quality: 'high',
success: (res) => {
const photos = [...this.data.photos, res.tempImagePath]
const photoIndex = photos.length
const guideText = this.data.guideTexts[photoIndex] || '拍摄完成'
this.setData({
photos,
photoIndex,
guideText
})
if (photos.length === 3) {
wx.showToast({ title: '拍摄完成', icon: 'success' })
}
},
fail: (err) => {
wx.showToast({ title: '拍照失败', icon: 'none' })
console.error('拍照失败:', err)
}
})
},
// 重新拍摄全部照片
retakeAll() {
wx.showModal({
title: '重新拍摄',
content: '确定要重新拍摄所有照片吗?',
confirmText: '确定',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
this.setData({
photos: [],
photoIndex: 0,
guideText: '请正对镜头'
})
wx.showToast({ title: '已清空,请重新拍摄', icon: 'success' })
}
}
})
},
// 完成拍照:先上传 3 张图到服务器,拿到 URL 后再跳转结果页
completeCapture() {
if (!ensureProfileCompleteAndRedirect()) return
if (!hasPhone()) {
wx.showToast({ title: '请先授权手机号', icon: 'none' })
this.setData({ needPhoneAuth: true })
return
}
const photos = this.data.photos
if (!photos || photos.length === 0) {
wx.showToast({ title: '请先拍摄照片', icon: 'none' })
return
}
this.setData({ uploading: true })
wx.showLoading({ title: '上传中...', mask: true })
const apiBase = (app.globalData && app.globalData.apiBase) ? app.globalData.apiBase.replace(/\/$/, '') : ''
const token = (app.globalData && app.globalData.token) || wx.getStorageSync('token') || ''
const uploadUrl = apiBase + '/api/upload/image'
const uploadOne = (path) => {
return new Promise((resolve, reject) => {
wx.uploadFile({
url: uploadUrl,
filePath: path,
name: 'file',
header: token ? { Authorization: 'Bearer ' + token } : {},
success: (res) => {
try {
const data = JSON.parse(res.data)
if (data.code === 200 && data.data && data.data.url) {
resolve(data.data.url)
} else {
reject(new Error(data.message || '上传失败'))
}
} catch (e) {
reject(new Error('解析上传结果失败'))
}
},
fail: (err) => reject(err)
})
})
}
Promise.all(photos.map(uploadOne))
.then((urls) => {
wx.hideLoading()
wx.setStorageSync('aiPhotos', urls)
this.setData({ uploading: false })
wx.navigateTo({ url: '/pages/index/result' })
})
.catch((err) => {
wx.hideLoading()
this.setData({ uploading: false })
wx.showToast({ title: err.message || '上传失败', icon: 'none' })
})
},
// 从相册选择:跳转到上传页(三角度上传),不改变当前 scope/企业上下文
goToUpload() {
wx.navigateTo({
url: '/pages/index/upload'
})
},
// 相机错误
onCameraError(e) {
console.error('相机错误:', e)
wx.showModal({
title: '相机权限',
content: '请允许使用相机权限以进行AI人脸分析',
confirmText: '去设置',
success: (res) => {
if (res.confirm) {
wx.openSetting()
}
}
})
}
})
// pages/index/camera.js - 面相分析拍照页,拍完后上传到服务器再跳转结果页
const app = getApp()
const { hasPhone, bindPhoneByCode, ensureProfileCompleteAndRedirect } = require('../../utils/phoneAuth.js')
Page({
data: {
photos: [],
photoIndex: 0,
guideTexts: ['请正对镜头', '请向左转45°', '请向右转45°'],
guideText: '请正对镜头',
uploading: false,
needPhoneAuth: false,
aiAnalysisText: '智能分析'
},
onLoad() {
this.cameraContext = wx.createCameraContext()
const tc = app.globalData.textConfig
if (tc && tc.aiAnalysisText) {
this.setData({ aiAnalysisText: tc.aiAnalysisText })
} else {
app.getRuntimeConfig().then((cfg) => {
if (cfg && cfg.textConfig) {
app.globalData.textConfig = cfg.textConfig
this.setData({ aiAnalysisText: cfg.textConfig.aiAnalysisText || '智能分析' })
}
}).catch(() => {})
}
},
onShow() {
// 审核模式下重定向到测试选择页
if (app.globalData.reviewMode) {
wx.navigateTo({ url: '/pages/test-select/index' })
return
}
if (!ensureProfileCompleteAndRedirect()) return
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({ selected: 1 })
}
this.setData({ needPhoneAuth: !hasPhone() })
const tc = app.globalData.textConfig
if (tc && tc.aiAnalysisText) {
this.setData({ aiAnalysisText: tc.aiAnalysisText })
}
},
// 本页就地授权手机号
onGetPhoneNumber(e) {
const { code, errMsg } = e.detail || {}
if (errMsg && errMsg.indexOf('getPhoneNumber:fail') === 0) {
wx.showToast({ title: '需要授权手机号才能继续', icon: 'none' })
return
}
if (!code && !hasPhone()) {
wx.showToast({ title: '获取手机号失败', icon: 'none' })
return
}
if (!code && hasPhone()) {
// 已有手机号,无需重复请求
this.setData({ needPhoneAuth: false })
return
}
bindPhoneByCode(code).then(() => {
this.setData({ needPhoneAuth: false })
}).catch(() => {
// 失败时保持 needPhoneAuth 为 true等待用户重新授权
this.setData({ needPhoneAuth: !hasPhone() })
})
},
// 拍照
takePhoto() {
if (this.data.photos.length >= 3) {
wx.showToast({ title: '已拍满3张', icon: 'none' })
return
}
this.cameraContext.takePhoto({
quality: 'high',
success: (res) => {
const photos = [...this.data.photos, res.tempImagePath]
const photoIndex = photos.length
const guideText = this.data.guideTexts[photoIndex] || '拍摄完成'
this.setData({
photos,
photoIndex,
guideText
})
if (photos.length === 3) {
wx.showToast({ title: '拍摄完成', icon: 'success' })
}
},
fail: (err) => {
wx.showToast({ title: '拍照失败', icon: 'none' })
console.error('拍照失败:', err)
}
})
},
// 重新拍摄全部照片
retakeAll() {
wx.showModal({
title: '重新拍摄',
content: '确定要重新拍摄所有照片吗?',
confirmText: '确定',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
this.setData({
photos: [],
photoIndex: 0,
guideText: '请正对镜头'
})
wx.showToast({ title: '已清空,请重新拍摄', icon: 'success' })
}
}
})
},
// 完成拍照:先上传 3 张图到服务器,拿到 URL 后再跳转结果页
completeCapture() {
if (!ensureProfileCompleteAndRedirect()) return
if (!hasPhone()) {
wx.showToast({ title: '请先授权手机号', icon: 'none' })
this.setData({ needPhoneAuth: true })
return
}
const photos = this.data.photos
if (!photos || photos.length === 0) {
wx.showToast({ title: '请先拍摄照片', icon: 'none' })
return
}
this.setData({ uploading: true })
wx.showLoading({ title: '上传中...', mask: true })
const apiBase = (app.globalData && app.globalData.apiBase) ? app.globalData.apiBase.replace(/\/$/, '') : ''
const token = (app.globalData && app.globalData.token) || wx.getStorageSync('token') || ''
const uploadUrl = apiBase + '/api/upload/image'
const uploadOne = (path) => {
return new Promise((resolve, reject) => {
wx.uploadFile({
url: uploadUrl,
filePath: path,
name: 'file',
header: token ? { Authorization: 'Bearer ' + token } : {},
success: (res) => {
try {
const data = JSON.parse(res.data)
if (data.code === 200 && data.data && data.data.url) {
resolve(data.data.url)
} else {
reject(new Error(data.message || '上传失败'))
}
} catch (e) {
reject(new Error('解析上传结果失败'))
}
},
fail: (err) => reject(err)
})
})
}
Promise.all(photos.map(uploadOne))
.then((urls) => {
wx.hideLoading()
wx.setStorageSync('aiPhotos', urls)
this.setData({ uploading: false })
wx.navigateTo({ url: '/pages/index/result' })
})
.catch((err) => {
wx.hideLoading()
this.setData({ uploading: false })
wx.showToast({ title: err.message || '上传失败', icon: 'none' })
})
},
// 从相册选择:跳转到上传页(三角度上传),不改变当前 scope/企业上下文
goToUpload() {
wx.navigateTo({
url: '/pages/index/upload'
})
},
// 相机错误
onCameraError(e) {
console.error('相机错误:', e)
wx.showModal({
title: '相机权限',
content: '请允许使用相机权限以进行AI人脸分析',
confirmText: '去设置',
success: (res) => {
if (res.confirm) {
wx.openSetting()
}
}
})
}
})

View File

@@ -1,177 +1,188 @@
// pages/index/index.js
const app = getApp()
const { request } = require('../../utils/request.js')
Page({
data: {
statusBarHeight: 0,
navbarHeight: 88,
showEnterpriseEntry: false,
siteTitle: '神仙团队AI性格测试',
startButtonText: '开始面相测试',
aiAnalysisText: '智能分析'
},
onLoad(options) {
// 获取状态栏高度和屏幕信息
const systemInfo = wx.getSystemInfoSync()
const statusBarHeight = systemInfo.statusBarHeight || 0
const screenWidth = systemInfo.screenWidth || 375
// 将状态栏高度从px转换为rpx
const statusBarHeightRpx = (statusBarHeight * 750) / screenWidth
const navbarHeightRpx = statusBarHeightRpx + 88// 状态栏 + 导航栏内容
const userInfo = getApp().globalData.userInfo || wx.getStorageSync('userInfo') || {}
const gd = getApp().globalData
this.setData({
statusBarHeight: statusBarHeightRpx,
navbarHeight: navbarHeightRpx,
showEnterpriseEntry: userInfo.hasEnterprise === true,
siteTitle: gd.siteTitle || '神仙团队AI性格测试',
startButtonText: (gd.textConfig && gd.textConfig.startButtonText) || '开始面相测试',
aiAnalysisText: (gd.textConfig && gd.textConfig.aiAnalysisText) || '智能分析'
})
// 预加载站点名称与文案配置
app.getRuntimeConfig().then((cfg) => {
if (cfg) {
if (cfg.siteTitle) {
getApp().globalData.siteTitle = cfg.siteTitle
this.setData({ siteTitle: cfg.siteTitle })
}
if (cfg.textConfig) {
getApp().globalData.textConfig = cfg.textConfig
this.setData({
startButtonText: cfg.textConfig.startButtonText || '开始面相测试',
aiAnalysisText: cfg.textConfig.aiAnalysisText || '智能分析'
})
}
}
}).catch(() => {})
// ── 解析入参(兼容扫码 scene / 分享链接 options──
const rawScene = (options && options.scene) ? decodeURIComponent(options.scene) : ''
const sceneParams = {}
if (rawScene) {
rawScene.split('&').forEach(pair => {
const [k, v] = pair.split('=')
if (k) sceneParams[k] = v || ''
})
}
const uid = parseInt(sceneParams.uid || (options && options.uid) || 0, 10)
const eid = parseInt(sceneParams.eid || (options && options.eid) || 0, 10)
console.log('[index/onLoad] 解码参数 =>', {
rawScene,
sceneParams,
options,
uid,
eid
})
// 携带 eid跳转企业版首页
if (eid > 0) {
getApp().globalData.enterpriseIdFromScene = eid
wx.navigateTo({ url: '/pages/enterprise/index?uid=' + uid + '&eid=' + eid })
return
}
// 个人版分销绑定
if (uid > 0) {
app.globalData._pendingInviterId = uid
app.globalData._pendingInviterScope = 'personal'
}
},
onShow() {
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({ selected: 0 })
}
// 个人版首页:固定 scope=personal并清除企业来源上下文
try {
getApp().globalData.appScope = 'personal'
getApp().globalData.enterpriseIdFromScene = null
} catch (e) {}
const gd = getApp().globalData
this.setData({
siteTitle: gd.siteTitle || '神仙团队AI性格测试',
startButtonText: (gd.textConfig && gd.textConfig.startButtonText) || '开始面相测试',
aiAnalysisText: (gd.textConfig && gd.textConfig.aiAnalysisText) || '智能分析'
})
const userInfo = getApp().globalData.userInfo || wx.getStorageSync('userInfo') || {}
this.setData({ showEnterpriseEntry: userInfo.hasEnterprise === true })
// 有 token 时拉取最新用户信息(含 hasEnterprise绑定企业后无需重新登录即可展示企业版入口
const token = getApp().globalData.token || wx.getStorageSync('token')
if (token) {
request({
url: '/api/auth/me',
method: 'GET',
success: (res) => {
if (res.statusCode === 200 && res.data && res.data.code === 200 && res.data.data) {
const user = res.data.data
getApp().globalData.userInfo = user
wx.setStorageSync('userInfo', user)
this.setData({ showEnterpriseEntry: user.hasEnterprise === true })
}
}
})
}
},
// 开始拍照(个人版入口:强制本次链路为个人定价)
startCamera() {
try { getApp().globalData.appScope = 'personal' } catch (e) {}
wx.switchTab({
url: '/pages/index/camera'
})
},
// 上传照片(个人版入口:强制本次链路为个人定价)
uploadPhoto() {
try { getApp().globalData.appScope = 'personal' } catch (e) {}
// 这里仅负责跳转到全新的「拍摄或上传照片」页面,具体拍摄/上传逻辑在新页面实现
wx.navigateTo({
url: '/pages/index/upload'
})
},
// 切换到企业版(仅已绑定企业的用户可进入,优先用登录返回的 hasEnterprise避免多请求
switchToEnterprise() {
const app = getApp()
const userInfo = app.globalData.userInfo || wx.getStorageSync('userInfo') || {}
if (userInfo.hasEnterprise === true) {
wx.navigateTo({ url: '/pages/enterprise/index' })
return
}
if (userInfo.hasEnterprise === false) {
wx.showToast({ title: '您尚未绑定任何企业,无法使用企业版', icon: 'none', duration: 2500 })
return
}
// 缓存里没有 hasEnterprise 时:临时用 enterprise scope 获取配置判定
try { app.globalData.appScope = 'enterprise' } catch (e) {}
app.ensureLogin().then(() => app.getRuntimeConfig()).then((cfg) => {
if ((cfg && cfg.pricingType) === 'enterprise') {
wx.navigateTo({ url: '/pages/enterprise/index' })
} else {
wx.showToast({ title: '您尚未绑定任何企业,无法使用企业版', icon: 'none', duration: 2500 })
}
}).catch(() => {
wx.showToast({ title: '无法获取配置,请稍后重试', icon: 'none' })
})
},
onShareAppMessage() {
const { getSharePathByScope } = require('../../utils/share')
return {
title: 'AI人脸性格分析 - 看看你的面相透露了什么性格密码',
path: getSharePathByScope('/pages/index/index')
}
},
onShareTimeline() {
const { buildShareQuery } = require('../../utils/share')
return {
title: 'AI人脸性格分析 - 看看你的面相透露了什么性格密码',
query: buildShareQuery()
}
}
})
// pages/index/index.js
const app = getApp()
const { request } = require('../../utils/request.js')
Page({
data: {
statusBarHeight: 0,
navbarHeight: 88,
showEnterpriseEntry: false,
siteTitle: '神仙团队AI性格测试',
startButtonText: '开始面相测试',
aiAnalysisText: '智能分析',
reviewMode: false
},
onLoad(options) {
// 获取状态栏高度和屏幕信息
const systemInfo = wx.getSystemInfoSync()
const statusBarHeight = systemInfo.statusBarHeight || 0
const screenWidth = systemInfo.screenWidth || 375
// 将状态栏高度从px转换为rpx
const statusBarHeightRpx = (statusBarHeight * 750) / screenWidth
const navbarHeightRpx = statusBarHeightRpx + 88// 状态栏 + 导航栏内容
const userInfo = getApp().globalData.userInfo || wx.getStorageSync('userInfo') || {}
const gd = getApp().globalData
this.setData({
statusBarHeight: statusBarHeightRpx,
navbarHeight: navbarHeightRpx,
showEnterpriseEntry: userInfo.hasEnterprise === true,
siteTitle: gd.reviewMode ? (gd.siteTitle || '神仙团队性格测试').replace(/AI/gi, '') : (gd.siteTitle || '神仙团队AI性格测试'),
startButtonText: gd.reviewMode ? '开始性格测试' : ((gd.textConfig && gd.textConfig.startButtonText) || '开始面相测试'),
aiAnalysisText: gd.reviewMode ? '分析' : ((gd.textConfig && gd.textConfig.aiAnalysisText) || '智能分析'),
reviewMode: !!gd.reviewMode
})
// 预加载站点名称与文案配置
app.getRuntimeConfig().then((cfg) => {
if (cfg) {
const rm = !!cfg.reviewMode
getApp().globalData.reviewMode = rm
if (cfg.siteTitle) {
getApp().globalData.siteTitle = cfg.siteTitle
this.setData({ siteTitle: rm ? cfg.siteTitle.replace(/AI/gi, '') : cfg.siteTitle })
}
if (cfg.textConfig) {
getApp().globalData.textConfig = cfg.textConfig
this.setData({
startButtonText: rm ? '开始性格测试' : (cfg.textConfig.startButtonText || '开始面相测试'),
aiAnalysisText: rm ? '分析' : (cfg.textConfig.aiAnalysisText || '智能分析')
})
}
this.setData({ reviewMode: rm })
}
}).catch(() => {})
// ── 解析入参(兼容扫码 scene / 分享链接 options──
const rawScene = (options && options.scene) ? decodeURIComponent(options.scene) : ''
const sceneParams = {}
if (rawScene) {
rawScene.split('&').forEach(pair => {
const [k, v] = pair.split('=')
if (k) sceneParams[k] = v || ''
})
}
const uid = parseInt(sceneParams.uid || (options && options.uid) || 0, 10)
const eid = parseInt(sceneParams.eid || (options && options.eid) || 0, 10)
console.log('[index/onLoad] 解码参数 =>', {
rawScene,
sceneParams,
options,
uid,
eid
})
// 携带 eid跳转企业版首页
if (eid > 0) {
getApp().globalData.enterpriseIdFromScene = eid
wx.navigateTo({ url: '/pages/enterprise/index?uid=' + uid + '&eid=' + eid })
return
}
// 个人版分销绑定
if (uid > 0) {
app.globalData._pendingInviterId = uid
app.globalData._pendingInviterScope = 'personal'
}
},
onShow() {
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({ selected: 0 })
}
// 个人版首页:固定 scope=personal并清除企业来源上下文
try {
getApp().globalData.appScope = 'personal'
getApp().globalData.enterpriseIdFromScene = null
} catch (e) {}
const gd = getApp().globalData
const rm = !!gd.reviewMode
this.setData({
siteTitle: rm ? (gd.siteTitle || '神仙团队性格测试').replace(/AI/gi, '') : (gd.siteTitle || '神仙团队AI性格测试'),
startButtonText: rm ? '开始性格测试' : ((gd.textConfig && gd.textConfig.startButtonText) || '开始面相测试'),
aiAnalysisText: rm ? '分析' : ((gd.textConfig && gd.textConfig.aiAnalysisText) || '智能分析'),
reviewMode: rm
})
const userInfo = getApp().globalData.userInfo || wx.getStorageSync('userInfo') || {}
this.setData({ showEnterpriseEntry: userInfo.hasEnterprise === true })
// 有 token 时拉取最新用户信息(含 hasEnterprise绑定企业后无需重新登录即可展示企业版入口
const token = getApp().globalData.token || wx.getStorageSync('token')
if (token) {
request({
url: '/api/auth/me',
method: 'GET',
success: (res) => {
if (res.statusCode === 200 && res.data && res.data.code === 200 && res.data.data) {
const user = res.data.data
getApp().globalData.userInfo = user
wx.setStorageSync('userInfo', user)
this.setData({ showEnterpriseEntry: user.hasEnterprise === true })
}
}
})
}
},
// 开始测试(审核模式跳问卷,正常模式跳拍照)
startCamera() {
try { getApp().globalData.appScope = 'personal' } catch (e) {}
if (this.data.reviewMode) {
wx.navigateTo({ url: '/pages/test-select/index' })
} else {
wx.switchTab({ url: '/pages/index/camera' })
}
},
// 上传照片(个人版入口:强制本次链路为个人定价
uploadPhoto() {
try { getApp().globalData.appScope = 'personal' } catch (e) {}
// 这里仅负责跳转到全新的「拍摄或上传照片」页面,具体拍摄/上传逻辑在新页面实现
wx.navigateTo({
url: '/pages/index/upload'
})
},
// 切换到企业版(仅已绑定企业的用户可进入,优先用登录返回的 hasEnterprise避免多请求
switchToEnterprise() {
const app = getApp()
const userInfo = app.globalData.userInfo || wx.getStorageSync('userInfo') || {}
if (userInfo.hasEnterprise === true) {
wx.navigateTo({ url: '/pages/enterprise/index' })
return
}
if (userInfo.hasEnterprise === false) {
wx.showToast({ title: '您尚未绑定任何企业,无法使用企业版', icon: 'none', duration: 2500 })
return
}
// 缓存里没有 hasEnterprise 时:临时用 enterprise scope 获取配置判定
try { app.globalData.appScope = 'enterprise' } catch (e) {}
app.ensureLogin().then(() => app.getRuntimeConfig()).then((cfg) => {
if ((cfg && cfg.pricingType) === 'enterprise') {
wx.navigateTo({ url: '/pages/enterprise/index' })
} else {
wx.showToast({ title: '您尚未绑定任何企业,无法使用企业版', icon: 'none', duration: 2500 })
}
}).catch(() => {
wx.showToast({ title: '无法获取配置,请稍后重试', icon: 'none' })
})
},
onShareAppMessage() {
const { getSharePathByScope } = require('../../utils/share')
const rm = this.data.reviewMode
return {
title: rm ? '性格测试 - 快来测测你的MBTI性格类型' : 'AI人脸性格分析 - 看看你的面相透露了什么性格密码',
path: getSharePathByScope('/pages/index/index')
}
},
onShareTimeline() {
const { buildShareQuery } = require('../../utils/share')
const rm = this.data.reviewMode
return {
title: rm ? '性格测试 - 快来测测你的MBTI性格类型' : 'AI人脸性格分析 - 看看你的面相透露了什么性格密码',
query: buildShareQuery()
}
}
})

View File

@@ -1,61 +1,85 @@
<!--pages/index/index.wxml - AI测试首页按旧版模板重构-->
<view class="container" style="padding-top: {{navbarHeight}}px;">
<!-- 自定义导航栏 -->
<view class="custom-navbar" style="padding-top: {{statusBarHeight}}rpx;">
<view class="navbar-content">
<view class="switch-enterprise-btn" wx:if="{{showEnterpriseEntry}}" bindtap="switchToEnterprise">
<text class="enterprise-icon">🏢</text>
<text class="enterprise-text">企业版</text>
</view>
<view class="navbar-title">{{siteTitle || '神仙团队AI性格测试'}}</view>
<view class="navbar-placeholder"></view>
</view>
</view>
<!-- 背景装饰(与企业版一致) -->
<view class="bg-decoration bg-top-right"></view>
<view class="bg-decoration bg-bottom-left"></view>
<view class="top-image-section">
<view class="image-container">
<view class="image-wrapper">
<image class="main-image" src="/images/mbti-team-image.png" mode="aspectFit"></image>
</view>
<view class="float-tag tag-1">面相分析</view>
<view class="float-tag tag-2">相分析</view>
<view class="float-tag tag-3">性格测评</view>
</view>
</view>
<view class="process-section">
<view class="section-title">测试流程</view>
<view class="process-steps">
<view class="step-item">
<view class="step-circle active">1</view>
<text class="step-label">STEP1</text>
<text class="step-text">拍摄照片</text>
</view>
<view class="step-line"></view>
<view class="step-item">
<view class="step-circle">2</view>
<text class="step-label">STEP2</text>
<text class="step-text">{{aiAnalysisText || '智能分析'}}</text>
</view>
<view class="step-line"></view>
<view class="step-item">
<view class="step-circle">3</view>
<text class="step-label">STEP3</text>
<text class="step-text">生成报告</text>
</view>
</view>
</view>
<view class="start-button" bindtap="startCamera">
<text class="button-text">{{startButtonText || '开始面相测试'}}</text>
</view>
<view class="ai-generated-tip">人工智能生成</view>
<!-- 自定义底部导航 -->
<custom-tab-bar />
</view>
<!--pages/index/index.wxml - 首页-->
<view class="container" style="padding-top: {{navbarHeight}}px;">
<!-- 自定义导航栏 -->
<view class="custom-navbar" style="padding-top: {{statusBarHeight}}rpx;">
<view class="navbar-content">
<view class="switch-enterprise-btn" wx:if="{{showEnterpriseEntry && !reviewMode}}" bindtap="switchToEnterprise">
<text class="enterprise-icon">🏢</text>
<text class="enterprise-text">企业版</text>
</view>
<view class="navbar-title">{{siteTitle || '神仙团队性格测试'}}</view>
<view class="navbar-placeholder"></view>
</view>
</view>
<!-- 背景装饰 -->
<view class="bg-decoration bg-top-right"></view>
<view class="bg-decoration bg-bottom-left"></view>
<view class="top-image-section">
<view class="image-container">
<view class="image-wrapper">
<image class="main-image" src="/images/mbti-team-image.png" mode="aspectFit"></image>
</view>
<!-- 审核模式:只显示性格测评标签 -->
<view class="float-tag tag-1" wx:if="{{!reviewMode}}">相分析</view>
<view class="float-tag tag-2" wx:if="{{!reviewMode}}">骨相分析</view>
<view class="float-tag tag-3">性格测评</view>
<view class="float-tag tag-1" wx:if="{{reviewMode}}">MBTI</view>
<view class="float-tag tag-2" wx:if="{{reviewMode}}">DISC</view>
</view>
</view>
<view class="process-section">
<view class="section-title">测试流程</view>
<view class="process-steps" wx:if="{{!reviewMode}}">
<view class="step-item">
<view class="step-circle active">1</view>
<text class="step-label">STEP1</text>
<text class="step-text">拍摄照片</text>
</view>
<view class="step-line"></view>
<view class="step-item">
<view class="step-circle">2</view>
<text class="step-label">STEP2</text>
<text class="step-text">{{aiAnalysisText || '智能分析'}}</text>
</view>
<view class="step-line"></view>
<view class="step-item">
<view class="step-circle">3</view>
<text class="step-label">STEP3</text>
<text class="step-text">生成报告</text>
</view>
</view>
<!-- 审核模式:纯问卷流程 -->
<view class="process-steps" wx:if="{{reviewMode}}">
<view class="step-item">
<view class="step-circle active">1</view>
<text class="step-label">STEP1</text>
<text class="step-text">选择测试</text>
</view>
<view class="step-line"></view>
<view class="step-item">
<view class="step-circle">2</view>
<text class="step-label">STEP2</text>
<text class="step-text">回答问题</text>
</view>
<view class="step-line"></view>
<view class="step-item">
<view class="step-circle">3</view>
<text class="step-label">STEP3</text>
<text class="step-text">查看结果</text>
</view>
</view>
</view>
<view class="start-button" bindtap="startCamera">
<text class="button-text">{{startButtonText || '开始性格测试'}}</text>
</view>
<!-- 审核模式下隐藏AI标签 -->
<view class="ai-generated-tip" wx:if="{{!reviewMode}}">人工智能生成</view>
<!-- 自定义底部导航 -->
<custom-tab-bar />
</view>

File diff suppressed because it is too large Load Diff

View File

@@ -1,304 +1,304 @@
<!--pages/index/result.wxml - AI分析结果按旧版模板重构-->
<view class="container">
<!-- 分析中状态 -->
<view class="analyzing-modal" wx:if="{{isAnalyzing}}">
<view class="analyzing-content">
<view class="analyzing-icon">
<view class="spinner"></view>
</view>
<text class="analyzing-title">{{analyzingTitle || '正在分析中'}}</text>
<text class="analyzing-desc">{{analyzingTip}}</text>
<view class="analyzing-bar">
<view class="analyzing-bar-fill" style="width: {{progress}}%"></view>
</view>
<text class="analyzing-hint">这个过程可能需要30秒到1分钟请耐心等待...</text>
</view>
</view>
<!-- 无人脸错误状态 -->
<view class="no-face-wrap" wx:if="{{!isAnalyzing && noFaceError}}">
<view class="no-face-card">
<view class="no-face-icon">
<text class="no-face-emoji">🚫</text>
</view>
<text class="no-face-title">未检测到人脸</text>
<text class="no-face-desc">{{noFaceMessage}}</text>
<view class="no-face-tips">
<text class="no-face-tips-title">拍摄建议:</text>
<text class="no-face-tip-item">• 确保面部完整出现在画面中</text>
<text class="no-face-tip-item">• 保持光线充足,避免逆光</text>
<text class="no-face-tip-item">• 正对镜头,不要遮挡面部</text>
<text class="no-face-tip-item">• 保持适当距离,不要过近或过远</text>
</view>
<view class="no-face-btn" bindtap="retake">
<text class="no-face-btn-text">重新拍摄</text>
</view>
</view>
</view>
<!-- 分析结果 -->
<view wx:if="{{!isAnalyzing && showResult}}">
<!-- 状态横幅 -->
<view class="status-banner {{hasError ? 'error-banner' : ''}}">
<view class="status-dot {{hasError ? 'error-dot' : ''}}"></view>
<text class="status-text">{{hasError ? '分析出现异常' : '分析完成,以下是您的个性化报告'}}</text>
</view>
<!-- 错误内容 -->
<view class="error-content" wx:if="{{hasError}}">
<text class="error-title">错误信息</text>
<text class="error-message">{{errorMessage}}</text>
<view class="retry-btn" bindtap="retake">
<text class="retry-text">重新分析</text>
</view>
</view>
<!-- 性格类型分析卡片 -->
<view class="card personality-card" wx:if="{{!hasError}}">
<text class="card-title">性格类型分析</text>
<!-- MBTI类型 -->
<view class="personality-type">
<text class="type-code">{{result.mbti}}</text>
<text class="type-name">{{result.title}}</text>
<text class="type-desc">{{result.summary}}</text>
</view>
<!-- PDP和DISC -->
<view class="type-details">
<view class="type-box pdp-box">
<text class="type-label">PDP主性格</text>
<text class="type-value">{{result.pdp || '--'}}</text>
<text class="type-sub" wx:if="{{result.pdpAux}}">辅助: {{result.pdpAux}}</text>
</view>
<view class="type-box disc-box">
<text class="type-label">DISC类型</text>
<text class="type-value">{{result.disc || '--'}}型</text>
<text class="type-sub" wx:if="{{result.discAux}}">辅助: {{result.discAux}}</text>
</view>
</view>
<!-- 主要优势 -->
<view class="advantages-box" wx:if="{{result.traits.length > 0}}">
<text class="advantages-label">主要优势</text>
<view class="advantages-list">
<view class="advantage-item" wx:for="{{result.traits}}" wx:key="*this">
<view class="advantage-dot"></view>
<text class="advantage-text">{{item}}</text>
</view>
</view>
</view>
<view class="next-btn" bindtap="goToMBTI">
<text class="next-btn-text">下一步:详细性格测试 →</text>
</view>
</view>
<!-- 性格概述 -->
<view class="card" wx:if="{{!hasError && result.summary}}">
<text class="card-title">性格概述</text>
<text class="card-text">{{result.summary}}</text>
</view>
<!-- 盖洛普前三大优势(付费后/免费皆展示,有数据才显示) -->
<view class="card gallup-card" wx:if="{{!hasError && result.gallupTop3.length > 0}}">
<text class="card-title">盖洛普前三大优势</text>
<view class="gallup-list">
<view class="gallup-item" wx:for="{{result.gallupTop3}}" wx:key="*this">
<text class="gallup-rank">{{index + 1}}</text>
<text class="gallup-text">{{item}}</text>
</view>
</view>
</view>
<!-- 付费解锁完整报告(不展示虚拟假数据) -->
<view class="card paywall-card" wx:if="{{!hasError && payInfo.requiresPayment && !payInfo.isPaid}}">
<view class="paywall-content">
<text class="paywall-fake-title">完整版性格深度解析</text>
<text class="paywall-tip">解锁后将展示完整报告内容</text>
<!-- 中间解锁按钮:未有手机号时就地触发微信手机号授权 -->
<button
class="paywall-btn"
wx:if="{{!hasPhone}}"
open-type="getPhoneNumber"
bindgetphonenumber="onGetPhoneNumberForFacePay"
>
<text class="paywall-btn-main">解锁完整报告</text>
<text class="paywall-btn-price">¥{{payInfo.amountYuan || 0}} / 次</text>
</button>
<!-- 已有手机号时:普通按钮,直接解锁 -->
<button
class="paywall-btn"
wx:elif="{{hasPhone}}"
bindtap="unlockFullReport"
>
<text class="paywall-btn-main">解锁完整报告</text>
<text class="paywall-btn-price">¥{{payInfo.amountYuan || 0}} / 次</text>
</button>
<text class="paywall-tip">一次性解锁本次{{reportTitle || '分析报告'}},永久保存在「历史记录」中</text>
</view>
</view>
<!-- ========== 详细分析区(解锁后可见) ========== -->
<block wx:if="{{!hasError && (!payInfo.requiresPayment || payInfo.isPaid)}}">
<!-- 面相分析 -->
<block wx:if="{{result.faceAnalysis}}">
<!-- 完整结构化面相(旧 object 格式) -->
<view class="card" wx:if="{{result.faceAnalysis.facialFeatures && result.faceAnalysis.facialFeatures.length > 0}}">
<text class="card-title">面部特征分析</text>
<view class="feature-list">
<view class="feature-item" wx:for="{{result.faceAnalysis.facialFeatures}}" wx:key="label">
<text class="feature-label">{{item.label}}</text>
<text class="feature-desc">{{item.description}}</text>
</view>
</view>
</view>
</block>
<!-- 字符串版面相(新 prompt 返回) -->
<view class="card" wx:elif="{{result.faceAnalysisText}}">
<text class="card-title">面相分析</text>
<text class="card-text">{{result.faceAnalysisText}}</text>
</view>
<!-- 骨相分析 -->
<block wx:if="{{result.boneAnalysis && result.boneAnalysis.boneFormSummary}}">
<!-- 完整结构化骨相(旧 object 格式) -->
<view class="card">
<text class="card-title">骨相分析(《冰鉴》八骨)</text>
<view class="bone-content">
<view class="bone-item" wx:if="{{result.boneAnalysis.boneFormSummary.elementType}}">
<text class="bone-label">五行形相:</text>
<text class="bone-text">{{result.boneAnalysis.boneFormSummary.elementType}}</text>
</view>
<view class="bone-item" wx:if="{{result.boneAnalysis.boneFormSummary.boneFleshRelation}}">
<text class="bone-label">骨肉关系:</text>
<text class="bone-text">{{result.boneAnalysis.boneFormSummary.boneFleshRelation}}</text>
</view>
</view>
</view>
</block>
<!-- 字符串版骨相(新 prompt 返回) -->
<view class="card" wx:elif="{{result.boneAnalysisText}}">
<text class="card-title">骨相分析(《冰鉴》八骨)</text>
<text class="card-text">{{result.boneAnalysisText}}</text>
</view>
<!-- 人际关系 -->
<view class="card" wx:if="{{result.relationship}}">
<text class="card-title">人际关系与团队合作</text>
<text class="card-text">{{result.relationship}}</text>
</view>
<!-- 企业版:简历亮点 -->
<view class="card" wx:if="{{result.resumeHighlights}}">
<text class="card-title">简历亮点</text>
<text class="card-text">{{result.resumeHighlights}}</text>
</view>
<!-- 企业版:职业画像 portrait -->
<view class="card" wx:if="{{result.portrait}}">
<text class="card-title">职业画像</text>
<view class="enterprise-section" wx:if="{{result.portrait.coreStrengths && result.portrait.coreStrengths.length > 0}}">
<text class="enterprise-label">核心优势</text>
<view class="enterprise-tags">
<text class="enterprise-tag" wx:for="{{result.portrait.coreStrengths}}" wx:key="*this">{{item}}</text>
</view>
</view>
<view class="enterprise-section" wx:if="{{result.portrait.coreRisks && result.portrait.coreRisks.length > 0}}">
<text class="enterprise-label">潜在风险</text>
<view class="enterprise-tags">
<text class="enterprise-tag risk" wx:for="{{result.portrait.coreRisks}}" wx:key="*this">{{item}}</text>
</view>
</view>
<view class="enterprise-section" wx:if="{{result.portrait.workStyle}}">
<text class="enterprise-label">工作风格</text>
<text class="card-text">{{result.portrait.workStyle}}</text>
</view>
</view>
<!-- 企业版HR视角 hrView -->
<view class="card" wx:if="{{result.hrView}}">
<text class="card-title">HR视角</text>
<view class="enterprise-section" wx:if="{{result.hrView.roleRecommend}}">
<text class="enterprise-label">岗位推荐</text>
<view wx:if="{{result.hrView.roleRecommend.bestFit && result.hrView.roleRecommend.bestFit.length > 0}}">
<text class="enterprise-sublabel">最适合</text>
<view class="enterprise-tags">
<text class="enterprise-tag" wx:for="{{result.hrView.roleRecommend.bestFit}}" wx:key="*this">{{item}}</text>
</view>
</view>
<view wx:if="{{result.hrView.roleRecommend.notSuitable && result.hrView.roleRecommend.notSuitable.length > 0}}" style="margin-top: 12rpx;">
<text class="enterprise-sublabel">不适合</text>
<view class="enterprise-tags">
<text class="enterprise-tag risk" wx:for="{{result.hrView.roleRecommend.notSuitable}}" wx:key="*this">{{item}}</text>
</view>
</view>
</view>
<view class="enterprise-section" wx:if="{{result.hrView.lifecycle}}">
<text class="enterprise-label">成长周期</text>
<view class="lifecycle-list">
<view class="lifecycle-item" wx:if="{{result.hrView.lifecycle.onboarding}}">
<text class="lifecycle-key">入职适应</text>
<text class="lifecycle-val">{{result.hrView.lifecycle.onboarding}}</text>
</view>
<view class="lifecycle-item" wx:if="{{result.hrView.lifecycle.probation}}">
<text class="lifecycle-key">试用期</text>
<text class="lifecycle-val">{{result.hrView.lifecycle.probation}}</text>
</view>
<view class="lifecycle-item" wx:if="{{result.hrView.lifecycle.growth}}">
<text class="lifecycle-key">成长期</text>
<text class="lifecycle-val">{{result.hrView.lifecycle.growth}}</text>
</view>
<view class="lifecycle-item" wx:if="{{result.hrView.lifecycle.retention}}">
<text class="lifecycle-key">留人因素</text>
<text class="lifecycle-val">{{result.hrView.lifecycle.retention}}</text>
</view>
</view>
</view>
<view class="enterprise-section" wx:if="{{result.hrView.performance}}">
<text class="enterprise-label">绩效潜力</text>
<text class="card-text" wx:if="{{result.hrView.performance.potential}}">潜力:{{result.hrView.performance.potential}}</text>
<view wx:if="{{result.hrView.performance.drivers && result.hrView.performance.drivers.length > 0}}" style="margin-top: 8rpx;">
<text class="enterprise-sublabel">驱动因子:</text>
<text class="card-text">{{result.hrView.performance.drivers.join('、')}}</text>
</view>
</view>
<view class="enterprise-section" wx:if="{{result.hrView.teamFit}}">
<text class="enterprise-label">团队适配</text>
<text class="card-text">{{result.hrView.teamFit.bestTeam || ''}}</text>
<text class="card-text" wx:if="{{result.hrView.teamFit.manageAdvice}}" style="margin-top: 8rpx; display: block;">管理建议:{{result.hrView.teamFit.manageAdvice}}</text>
</view>
</view>
<!-- 企业版:老板视角 bossView -->
<view class="card boss-card" wx:if="{{result.bossView}}">
<text class="card-title">老板视角</text>
<text class="boss-headline" wx:if="{{result.bossView.headline}}">{{result.bossView.headline}}</text>
<view class="boss-metrics" wx:if="{{result.bossView.metrics && result.bossView.metrics.length > 0}}">
<view class="boss-metric" wx:for="{{result.bossView.metrics}}" wx:key="label">
<text class="boss-metric-label">{{item.label}}</text>
<text class="boss-metric-value {{item.level}}">{{item.value}}</text>
</view>
</view>
<text class="card-text" wx:if="{{result.bossView.costInsight}}" style="margin-top: 16rpx;">用人成本预判:{{result.bossView.costInsight}}</text>
</view>
</block>
<!-- 操作按钮 -->
<view class="action-section" wx:if="{{!hasError}}">
<button class="btn btn-primary" open-type="share">
<text class="btn-text">分享我的分析报告</text>
</button>
<view class="btn btn-outline" bindtap="retake">
<text class="btn-text-outline">重新分析</text>
</view>
<view class="btn btn-outline" bindtap="goHome">
<text class="btn-text-outline">返回首页</text>
</view>
</view>
<view class="ai-generated-tip">人工智能生成</view>
</view>
</view>
<!--pages/index/result.wxml - AI分析结果按旧版模板重构-->
<view class="container">
<!-- 分析中状态 -->
<view class="analyzing-modal" wx:if="{{isAnalyzing}}">
<view class="analyzing-content">
<view class="analyzing-icon">
<view class="spinner"></view>
</view>
<text class="analyzing-title">{{analyzingTitle || '正在分析中'}}</text>
<text class="analyzing-desc">{{analyzingTip}}</text>
<view class="analyzing-bar">
<view class="analyzing-bar-fill" style="width: {{progress}}%"></view>
</view>
<text class="analyzing-hint">这个过程可能需要30秒到1分钟请耐心等待...</text>
</view>
</view>
<!-- 无人脸错误状态 -->
<view class="no-face-wrap" wx:if="{{!isAnalyzing && noFaceError}}">
<view class="no-face-card">
<view class="no-face-icon">
<text class="no-face-emoji">🚫</text>
</view>
<text class="no-face-title">未检测到人脸</text>
<text class="no-face-desc">{{noFaceMessage}}</text>
<view class="no-face-tips">
<text class="no-face-tips-title">拍摄建议:</text>
<text class="no-face-tip-item">• 确保面部完整出现在画面中</text>
<text class="no-face-tip-item">• 保持光线充足,避免逆光</text>
<text class="no-face-tip-item">• 正对镜头,不要遮挡面部</text>
<text class="no-face-tip-item">• 保持适当距离,不要过近或过远</text>
</view>
<view class="no-face-btn" bindtap="retake">
<text class="no-face-btn-text">重新拍摄</text>
</view>
</view>
</view>
<!-- 分析结果 -->
<view wx:if="{{!isAnalyzing && showResult}}">
<!-- 状态横幅 -->
<view class="status-banner {{hasError ? 'error-banner' : ''}}">
<view class="status-dot {{hasError ? 'error-dot' : ''}}"></view>
<text class="status-text">{{hasError ? '分析出现异常' : '分析完成,以下是您的个性化报告'}}</text>
</view>
<!-- 错误内容 -->
<view class="error-content" wx:if="{{hasError}}">
<text class="error-title">错误信息</text>
<text class="error-message">{{errorMessage}}</text>
<view class="retry-btn" bindtap="retake">
<text class="retry-text">重新分析</text>
</view>
</view>
<!-- 性格类型分析卡片 -->
<view class="card personality-card" wx:if="{{!hasError}}">
<text class="card-title">性格类型分析</text>
<!-- MBTI类型 -->
<view class="personality-type">
<text class="type-code">{{result.mbti}}</text>
<text class="type-name">{{result.title}}</text>
<text class="type-desc">{{result.summary}}</text>
</view>
<!-- PDP和DISC -->
<view class="type-details">
<view class="type-box pdp-box">
<text class="type-label">PDP主性格</text>
<text class="type-value">{{result.pdp || '--'}}</text>
<text class="type-sub" wx:if="{{result.pdpAux}}">辅助: {{result.pdpAux}}</text>
</view>
<view class="type-box disc-box">
<text class="type-label">DISC类型</text>
<text class="type-value">{{result.disc || '--'}}型</text>
<text class="type-sub" wx:if="{{result.discAux}}">辅助: {{result.discAux}}</text>
</view>
</view>
<!-- 主要优势 -->
<view class="advantages-box" wx:if="{{result.traits.length > 0}}">
<text class="advantages-label">主要优势</text>
<view class="advantages-list">
<view class="advantage-item" wx:for="{{result.traits}}" wx:key="*this">
<view class="advantage-dot"></view>
<text class="advantage-text">{{item}}</text>
</view>
</view>
</view>
<view class="next-btn" bindtap="goToMBTI">
<text class="next-btn-text">下一步:详细性格测试 →</text>
</view>
</view>
<!-- 性格概述 -->
<view class="card" wx:if="{{!hasError && result.summary}}">
<text class="card-title">性格概述</text>
<text class="card-text">{{result.summary}}</text>
</view>
<!-- 盖洛普前三大优势(付费后/免费皆展示,有数据才显示) -->
<view class="card gallup-card" wx:if="{{!hasError && result.gallupTop3.length > 0}}">
<text class="card-title">盖洛普前三大优势</text>
<view class="gallup-list">
<view class="gallup-item" wx:for="{{result.gallupTop3}}" wx:key="*this">
<text class="gallup-rank">{{index + 1}}</text>
<text class="gallup-text">{{item}}</text>
</view>
</view>
</view>
<!-- 付费解锁完整报告(不展示虚拟假数据) -->
<view class="card paywall-card" wx:if="{{!hasError && payInfo.requiresPayment && !payInfo.isPaid}}">
<view class="paywall-content">
<text class="paywall-fake-title">完整版性格深度解析</text>
<text class="paywall-tip">解锁后将展示完整报告内容</text>
<!-- 中间解锁按钮:未有手机号时就地触发微信手机号授权 -->
<button
class="paywall-btn"
wx:if="{{!hasPhone}}"
open-type="getPhoneNumber"
bindgetphonenumber="onGetPhoneNumberForFacePay"
>
<text class="paywall-btn-main">解锁完整报告</text>
<text class="paywall-btn-price">¥{{payInfo.amountYuan || 0}} / 次</text>
</button>
<!-- 已有手机号时:普通按钮,直接解锁 -->
<button
class="paywall-btn"
wx:elif="{{hasPhone}}"
bindtap="unlockFullReport"
>
<text class="paywall-btn-main">解锁完整报告</text>
<text class="paywall-btn-price">¥{{payInfo.amountYuan || 0}} / 次</text>
</button>
<text class="paywall-tip">一次性解锁本次{{reportTitle || '分析报告'}},永久保存在「历史记录」中</text>
</view>
</view>
<!-- ========== 详细分析区(解锁后可见) ========== -->
<block wx:if="{{!hasError && (!payInfo.requiresPayment || payInfo.isPaid)}}">
<!-- 面相分析 -->
<block wx:if="{{result.faceAnalysis}}">
<!-- 完整结构化面相(旧 object 格式) -->
<view class="card" wx:if="{{result.faceAnalysis.facialFeatures && result.faceAnalysis.facialFeatures.length > 0}}">
<text class="card-title">面部特征分析</text>
<view class="feature-list">
<view class="feature-item" wx:for="{{result.faceAnalysis.facialFeatures}}" wx:key="label">
<text class="feature-label">{{item.label}}</text>
<text class="feature-desc">{{item.description}}</text>
</view>
</view>
</view>
</block>
<!-- 字符串版面相(新 prompt 返回) -->
<view class="card" wx:elif="{{result.faceAnalysisText}}">
<text class="card-title">面相分析</text>
<text class="card-text">{{result.faceAnalysisText}}</text>
</view>
<!-- 骨相分析 -->
<block wx:if="{{result.boneAnalysis && result.boneAnalysis.boneFormSummary}}">
<!-- 完整结构化骨相(旧 object 格式) -->
<view class="card">
<text class="card-title">骨相分析(《冰鉴》八骨)</text>
<view class="bone-content">
<view class="bone-item" wx:if="{{result.boneAnalysis.boneFormSummary.elementType}}">
<text class="bone-label">五行形相:</text>
<text class="bone-text">{{result.boneAnalysis.boneFormSummary.elementType}}</text>
</view>
<view class="bone-item" wx:if="{{result.boneAnalysis.boneFormSummary.boneFleshRelation}}">
<text class="bone-label">骨肉关系:</text>
<text class="bone-text">{{result.boneAnalysis.boneFormSummary.boneFleshRelation}}</text>
</view>
</view>
</view>
</block>
<!-- 字符串版骨相(新 prompt 返回) -->
<view class="card" wx:elif="{{result.boneAnalysisText}}">
<text class="card-title">骨相分析(《冰鉴》八骨)</text>
<text class="card-text">{{result.boneAnalysisText}}</text>
</view>
<!-- 人际关系 -->
<view class="card" wx:if="{{result.relationship}}">
<text class="card-title">人际关系与团队合作</text>
<text class="card-text">{{result.relationship}}</text>
</view>
<!-- 企业版:简历亮点 -->
<view class="card" wx:if="{{result.resumeHighlights}}">
<text class="card-title">简历亮点</text>
<text class="card-text">{{result.resumeHighlights}}</text>
</view>
<!-- 企业版:职业画像 portrait -->
<view class="card" wx:if="{{result.portrait}}">
<text class="card-title">职业画像</text>
<view class="enterprise-section" wx:if="{{result.portrait.coreStrengths && result.portrait.coreStrengths.length > 0}}">
<text class="enterprise-label">核心优势</text>
<view class="enterprise-tags">
<text class="enterprise-tag" wx:for="{{result.portrait.coreStrengths}}" wx:key="*this">{{item}}</text>
</view>
</view>
<view class="enterprise-section" wx:if="{{result.portrait.coreRisks && result.portrait.coreRisks.length > 0}}">
<text class="enterprise-label">潜在风险</text>
<view class="enterprise-tags">
<text class="enterprise-tag risk" wx:for="{{result.portrait.coreRisks}}" wx:key="*this">{{item}}</text>
</view>
</view>
<view class="enterprise-section" wx:if="{{result.portrait.workStyle}}">
<text class="enterprise-label">工作风格</text>
<text class="card-text">{{result.portrait.workStyle}}</text>
</view>
</view>
<!-- 企业版HR视角 hrView -->
<view class="card" wx:if="{{result.hrView}}">
<text class="card-title">HR视角</text>
<view class="enterprise-section" wx:if="{{result.hrView.roleRecommend}}">
<text class="enterprise-label">岗位推荐</text>
<view wx:if="{{result.hrView.roleRecommend.bestFit && result.hrView.roleRecommend.bestFit.length > 0}}">
<text class="enterprise-sublabel">最适合</text>
<view class="enterprise-tags">
<text class="enterprise-tag" wx:for="{{result.hrView.roleRecommend.bestFit}}" wx:key="*this">{{item}}</text>
</view>
</view>
<view wx:if="{{result.hrView.roleRecommend.notSuitable && result.hrView.roleRecommend.notSuitable.length > 0}}" style="margin-top: 12rpx;">
<text class="enterprise-sublabel">不适合</text>
<view class="enterprise-tags">
<text class="enterprise-tag risk" wx:for="{{result.hrView.roleRecommend.notSuitable}}" wx:key="*this">{{item}}</text>
</view>
</view>
</view>
<view class="enterprise-section" wx:if="{{result.hrView.lifecycle}}">
<text class="enterprise-label">成长周期</text>
<view class="lifecycle-list">
<view class="lifecycle-item" wx:if="{{result.hrView.lifecycle.onboarding}}">
<text class="lifecycle-key">入职适应</text>
<text class="lifecycle-val">{{result.hrView.lifecycle.onboarding}}</text>
</view>
<view class="lifecycle-item" wx:if="{{result.hrView.lifecycle.probation}}">
<text class="lifecycle-key">试用期</text>
<text class="lifecycle-val">{{result.hrView.lifecycle.probation}}</text>
</view>
<view class="lifecycle-item" wx:if="{{result.hrView.lifecycle.growth}}">
<text class="lifecycle-key">成长期</text>
<text class="lifecycle-val">{{result.hrView.lifecycle.growth}}</text>
</view>
<view class="lifecycle-item" wx:if="{{result.hrView.lifecycle.retention}}">
<text class="lifecycle-key">留人因素</text>
<text class="lifecycle-val">{{result.hrView.lifecycle.retention}}</text>
</view>
</view>
</view>
<view class="enterprise-section" wx:if="{{result.hrView.performance}}">
<text class="enterprise-label">绩效潜力</text>
<text class="card-text" wx:if="{{result.hrView.performance.potential}}">潜力:{{result.hrView.performance.potential}}</text>
<view wx:if="{{result.hrView.performance.drivers && result.hrView.performance.drivers.length > 0}}" style="margin-top: 8rpx;">
<text class="enterprise-sublabel">驱动因子:</text>
<text class="card-text">{{result.hrView.performance.drivers.join('、')}}</text>
</view>
</view>
<view class="enterprise-section" wx:if="{{result.hrView.teamFit}}">
<text class="enterprise-label">团队适配</text>
<text class="card-text">{{result.hrView.teamFit.bestTeam || ''}}</text>
<text class="card-text" wx:if="{{result.hrView.teamFit.manageAdvice}}" style="margin-top: 8rpx; display: block;">管理建议:{{result.hrView.teamFit.manageAdvice}}</text>
</view>
</view>
<!-- 企业版:老板视角 bossView -->
<view class="card boss-card" wx:if="{{result.bossView}}">
<text class="card-title">老板视角</text>
<text class="boss-headline" wx:if="{{result.bossView.headline}}">{{result.bossView.headline}}</text>
<view class="boss-metrics" wx:if="{{result.bossView.metrics && result.bossView.metrics.length > 0}}">
<view class="boss-metric" wx:for="{{result.bossView.metrics}}" wx:key="label">
<text class="boss-metric-label">{{item.label}}</text>
<text class="boss-metric-value {{item.level}}">{{item.value}}</text>
</view>
</view>
<text class="card-text" wx:if="{{result.bossView.costInsight}}" style="margin-top: 16rpx;">用人成本预判:{{result.bossView.costInsight}}</text>
</view>
</block>
<!-- 操作按钮 -->
<view class="action-section" wx:if="{{!hasError}}">
<button class="btn btn-primary" open-type="share">
<text class="btn-text">分享我的分析报告</text>
</button>
<view class="btn btn-outline" bindtap="retake">
<text class="btn-text-outline">重新分析</text>
</view>
<view class="btn btn-outline" bindtap="goHome">
<text class="btn-text-outline">返回首页</text>
</view>
</view>
<view class="ai-generated-tip" wx:if="{{!reviewMode}}">人工智能生成</view>
</view>
</view>

View File

@@ -1,193 +1,198 @@
// pages/index/camera.js - 面相分析拍照页,拍完后上传到服务器再跳转结果页
const app = getApp()
const { hasPhone, bindPhoneByCode, ensureProfileCompleteAndRedirect } = require('../../utils/phoneAuth.js')
Page({
data: {
sampleImages: [
'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260312/img_69b22c5b77dd72.66364633.png',
'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260312/img_69b22d71b312f8.14596541.png',
'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260312/img_69b22d78d52b94.76297015.png'
],
photos: ['', '', ''],
uploadedUrls: [],
photoIndex: 0,
guideTexts: ['请正对镜头', '请向左转45°', '请向右转45°'],
guideText: '请正对镜头',
uploading: false,
needPhoneAuth: false,
aiAnalysisText: '智能分析'
},
onLoad() {
const tc = app.globalData.textConfig
if (tc && tc.aiAnalysisText) {
this.setData({ aiAnalysisText: tc.aiAnalysisText })
} else {
app.getRuntimeConfig().then((cfg) => {
if (cfg && cfg.textConfig) {
app.globalData.textConfig = cfg.textConfig
this.setData({ aiAnalysisText: cfg.textConfig.aiAnalysisText || '智能分析' })
}
}).catch(() => {})
}
},
onShow() {
if (!ensureProfileCompleteAndRedirect()) return
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({ selected: 1 })
}
this.setData({ needPhoneAuth: !hasPhone() })
const tc = app.globalData.textConfig
if (tc && tc.aiAnalysisText) {
this.setData({ aiAnalysisText: tc.aiAnalysisText })
}
},
// 本页就地授权手机号
onGetPhoneNumber(e) {
const { code, errMsg } = e.detail || {}
if (errMsg && errMsg.indexOf('getPhoneNumber:fail') === 0) {
wx.showToast({ title: '需要授权手机号才能继续', icon: 'none' })
return
}
if (!code && !hasPhone()) {
wx.showToast({ title: '获取手机号失败', icon: 'none' })
return
}
if (!code && hasPhone()) {
// 已有手机号,无需重复请求
this.setData({ needPhoneAuth: false })
return
}
bindPhoneByCode(code).then(() => {
this.setData({ needPhoneAuth: false })
}).catch(() => {
// 失败时保持 needPhoneAuth 为 true等待用户重新授权
this.setData({ needPhoneAuth: !hasPhone() })
})
},
// 选择/拍摄照片通用方法index: 0 正面 / 1 左侧 / 2 右侧)
choosePhoto(index) {
wx.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['camera', 'album'],
success: (res) => {
const tempPath = res.tempFilePaths[0]
const photos = this.data.photos.slice()
photos[index] = tempPath
const filledCount = photos.filter(Boolean).length
const guideText = this.data.guideTexts[filledCount] || '拍摄完成'
this.setData({ photos, photoIndex: filledCount, guideText })
// 选完图后立即上传当前这张到服务器
this.uploadSinglePhoto(index, tempPath)
},
fail: () => {}
})
},
// 单张上传到服务器,成功后记录 URL
uploadSinglePhoto(index, localPath) {
const apiBase = (app.globalData && app.globalData.apiBase) ? app.globalData.apiBase.replace(/\/$/, '') : ''
if (!apiBase) {
wx.showToast({ title: '网络未就绪,请稍后再试', icon: 'none' })
return
}
const token = (app.globalData && app.globalData.token) || wx.getStorageSync('token') || ''
const uploadUrl = apiBase + '/api/upload/image'
wx.showLoading({ title: '上传中...', mask: true })
wx.uploadFile({
url: uploadUrl,
filePath: localPath,
name: 'file',
header: token ? { Authorization: 'Bearer ' + token } : {},
success: (res) => {
try {
const data = JSON.parse(res.data)
if (data.code === 200 && data.data && data.data.url) {
const url = data.data.url
const uploadedUrls = this.data.uploadedUrls.slice()
uploadedUrls[index] = url
const photos = this.data.photos.slice()
photos[index] = url
this.setData({ uploadedUrls, photos })
} else {
wx.showToast({ title: data.message || '上传失败', icon: 'none' })
}
} catch (e) {
wx.showToast({ title: '解析上传结果失败', icon: 'none' })
}
},
fail: (err) => {
console.error('[upload] 单张上传失败:', err)
wx.showToast({ title: '上传失败,请重试', icon: 'none' })
},
complete: () => {
wx.hideLoading()
}
})
},
// 三个角度对应的上传按钮
onUploadFront() {
this.choosePhoto(0)
},
onUploadLeft() {
this.choosePhoto(1)
},
onUploadRight() {
this.choosePhoto(2)
},
// 重新拍摄全部照片
retakeAll() {
wx.showModal({
title: '重新拍摄',
content: '确定要重新拍摄所有照片吗?',
confirmText: '确定',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
this.setData({
photos: ['', '', ''],
uploadedUrls: [],
photoIndex: 0,
guideText: '请正对镜头'
})
wx.showToast({ title: '已清空,请重新拍摄', icon: 'success' })
}
}
})
},
// 完成拍照:先上传 3 张图到服务器,拿到 URL 后再跳转结果页
completeCapture() {
if (!ensureProfileCompleteAndRedirect()) return
const urls = (this.data.uploadedUrls || []).filter(Boolean)
if (!urls.length) {
wx.showToast({ title: '请先上传至少一张照片', icon: 'none' })
return
}
// 此时 URL 已在选择时上传完成,这里只负责保存和跳转
wx.setStorageSync('aiPhotos', urls)
wx.navigateTo({ url: '/pages/index/result' })
},
// 相机错误
onCameraError(e) {
console.error('相机错误:', e)
wx.showModal({
title: '相机权限',
content: '请允许使用相机权限以进行AI人脸分析',
confirmText: '去设置',
success: (res) => {
if (res.confirm) {
wx.openSetting()
}
}
})
}
})
// pages/index/camera.js - 面相分析拍照页,拍完后上传到服务器再跳转结果页
const app = getApp()
const { hasPhone, bindPhoneByCode, ensureProfileCompleteAndRedirect } = require('../../utils/phoneAuth.js')
Page({
data: {
sampleImages: [
'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260312/img_69b22c5b77dd72.66364633.png',
'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260312/img_69b22d71b312f8.14596541.png',
'https://karuosiyujzk.oss-cn-shenzhen.aliyuncs.com/mbti/20260312/img_69b22d78d52b94.76297015.png'
],
photos: ['', '', ''],
uploadedUrls: [],
photoIndex: 0,
guideTexts: ['请正对镜头', '请向左转45°', '请向右转45°'],
guideText: '请正对镜头',
uploading: false,
needPhoneAuth: false,
aiAnalysisText: '智能分析'
},
onLoad() {
const tc = app.globalData.textConfig
if (tc && tc.aiAnalysisText) {
this.setData({ aiAnalysisText: tc.aiAnalysisText })
} else {
app.getRuntimeConfig().then((cfg) => {
if (cfg && cfg.textConfig) {
app.globalData.textConfig = cfg.textConfig
this.setData({ aiAnalysisText: cfg.textConfig.aiAnalysisText || '智能分析' })
}
}).catch(() => {})
}
},
onShow() {
// 审核模式下重定向到测试选择页
if (app.globalData.reviewMode) {
wx.navigateTo({ url: '/pages/test-select/index' })
return
}
if (!ensureProfileCompleteAndRedirect()) return
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({ selected: 1 })
}
this.setData({ needPhoneAuth: !hasPhone() })
const tc = app.globalData.textConfig
if (tc && tc.aiAnalysisText) {
this.setData({ aiAnalysisText: tc.aiAnalysisText })
}
},
// 本页就地授权手机号
onGetPhoneNumber(e) {
const { code, errMsg } = e.detail || {}
if (errMsg && errMsg.indexOf('getPhoneNumber:fail') === 0) {
wx.showToast({ title: '需要授权手机号才能继续', icon: 'none' })
return
}
if (!code && !hasPhone()) {
wx.showToast({ title: '获取手机号失败', icon: 'none' })
return
}
if (!code && hasPhone()) {
// 已有手机号,无需重复请求
this.setData({ needPhoneAuth: false })
return
}
bindPhoneByCode(code).then(() => {
this.setData({ needPhoneAuth: false })
}).catch(() => {
// 失败时保持 needPhoneAuth 为 true等待用户重新授权
this.setData({ needPhoneAuth: !hasPhone() })
})
},
// 选择/拍摄照片通用方法index: 0 正面 / 1 左侧 / 2 右侧)
choosePhoto(index) {
wx.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['camera', 'album'],
success: (res) => {
const tempPath = res.tempFilePaths[0]
const photos = this.data.photos.slice()
photos[index] = tempPath
const filledCount = photos.filter(Boolean).length
const guideText = this.data.guideTexts[filledCount] || '拍摄完成'
this.setData({ photos, photoIndex: filledCount, guideText })
// 选完图后立即上传当前这张到服务器
this.uploadSinglePhoto(index, tempPath)
},
fail: () => {}
})
},
// 单张上传到服务器,成功后记录 URL
uploadSinglePhoto(index, localPath) {
const apiBase = (app.globalData && app.globalData.apiBase) ? app.globalData.apiBase.replace(/\/$/, '') : ''
if (!apiBase) {
wx.showToast({ title: '网络未就绪,请稍后再试', icon: 'none' })
return
}
const token = (app.globalData && app.globalData.token) || wx.getStorageSync('token') || ''
const uploadUrl = apiBase + '/api/upload/image'
wx.showLoading({ title: '上传中...', mask: true })
wx.uploadFile({
url: uploadUrl,
filePath: localPath,
name: 'file',
header: token ? { Authorization: 'Bearer ' + token } : {},
success: (res) => {
try {
const data = JSON.parse(res.data)
if (data.code === 200 && data.data && data.data.url) {
const url = data.data.url
const uploadedUrls = this.data.uploadedUrls.slice()
uploadedUrls[index] = url
const photos = this.data.photos.slice()
photos[index] = url
this.setData({ uploadedUrls, photos })
} else {
wx.showToast({ title: data.message || '上传失败', icon: 'none' })
}
} catch (e) {
wx.showToast({ title: '解析上传结果失败', icon: 'none' })
}
},
fail: (err) => {
console.error('[upload] 单张上传失败:', err)
wx.showToast({ title: '上传失败,请重试', icon: 'none' })
},
complete: () => {
wx.hideLoading()
}
})
},
// 三个角度对应的上传按钮
onUploadFront() {
this.choosePhoto(0)
},
onUploadLeft() {
this.choosePhoto(1)
},
onUploadRight() {
this.choosePhoto(2)
},
// 重新拍摄全部照片
retakeAll() {
wx.showModal({
title: '重新拍摄',
content: '确定要重新拍摄所有照片吗?',
confirmText: '确定',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
this.setData({
photos: ['', '', ''],
uploadedUrls: [],
photoIndex: 0,
guideText: '请正对镜头'
})
wx.showToast({ title: '已清空,请重新拍摄', icon: 'success' })
}
}
})
},
// 完成拍照:先上传 3 张图到服务器,拿到 URL 后再跳转结果页
completeCapture() {
if (!ensureProfileCompleteAndRedirect()) return
const urls = (this.data.uploadedUrls || []).filter(Boolean)
if (!urls.length) {
wx.showToast({ title: '请先上传至少一张照片', icon: 'none' })
return
}
// 此时 URL 已在选择时上传完成,这里只负责保存和跳转
wx.setStorageSync('aiPhotos', urls)
wx.navigateTo({ url: '/pages/index/result' })
},
// 相机错误
onCameraError(e) {
console.error('相机错误:', e)
wx.showModal({
title: '相机权限',
content: '请允许使用相机权限以进行AI人脸分析',
confirmText: '去设置',
success: (res) => {
if (res.confirm) {
wx.openSetting()
}
}
})
}
})

View File

@@ -1,318 +1,323 @@
// pages/profile/index.js - 我的页面
const app = getApp()
const { getTypeOnly } = require('../../utils/resultFormat')
const { request } = require('../../utils/request')
Page({
data: {
hasLogin: false,
userInfo: null,
balance: 0,
testCount: 0,
hasResults: false,
mbtiType: '',
discType: '',
pdpType: '',
aiType: '',
mbtiTime: '',
discTime: '',
pdpTime: '',
aiTime: '',
/** 最近记录的数据库 ID用于跳转时传参 */
mbtiResultId: null,
discResultId: null,
pdpResultId: null,
aiResultId: null,
loginLoading: false,
loginFailed: false,
nicknameDisplay: '',
displayNickname: '',
/** 默认头像(根据昵称):无头像时显示首字+背景色 */
avatarLetter: '登',
avatarBgColor: '#6366f1',
// 推广中心统计与配置(来自 /api/distribution/stats
promoDistributionEnabled: true,
promoCenterTitle: '推广中心',
promoTotalInvite: 0,
promoTotalEarned: '0.00',
promoWithdrawable: '0.00',
/** 是否有企业权限(绑定企业):有则显示「我的简历」 */
hasEnterprise: false
},
onLoad() {
// 仅在 onLoad 执行一次onShow 里的 runLoginThenLoad 会导致重复请求
this.runLoginThenLoad()
},
onShow() {
// 如果是从其他页面返回,且已经登录,则只刷新数据而不重新执行登录流程
if (this.data.hasLogin) {
this.loadData()
}
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({ selected: 2 })
}
},
/** 先确保登录完成(静默登录),再刷新页面数据 */
runLoginThenLoad() {
this.setData({ loginLoading: true, loginFailed: false })
app.ensureLogin().then((ok) => {
this.setData({ loginLoading: false, loginFailed: !ok })
this.loadData()
}).catch(() => {
this.setData({ loginLoading: false, loginFailed: true })
this.loadData()
})
},
/** 生成随机后缀:仅 26 英文字母(大小写)与数字,不含特殊字符,默认 4 位 */
_randomWechatUserSuffix(len) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
let s = ''
for (let i = 0; i < (len || 4); i++) {
s += chars[Math.floor(Math.random() * chars.length)]
}
return s
},
/** 根据昵称生成默认头像的首字与背景色(同一昵称同色) */
_avatarFromNickname(name) {
const str = (name && String(name).trim()) || '登'
const letter = str.charAt(0).toUpperCase() || '登'
const palette = ['#6366f1', '#8b5cf6', '#ec4899', '#f43f5e', '#14b8a6', '#0ea5e9', '#3b82f6', '#eab308']
let hash = 0
for (let i = 0; i < str.length; i++) hash += str.charCodeAt(i)
const bgColor = palette[Math.abs(hash) % palette.length]
return { avatarLetter: letter, avatarBgColor: bgColor }
},
/** 获取或生成当前用户的「微信用户XXXX」后缀同一用户固定仅 26 字母+数字) */
_getOrCreateWechatUserSuffix(userInfo) {
const uid = (userInfo && (userInfo.id || userInfo.userId)) ? String(userInfo.id || userInfo.userId) : '_default'
const storageKey = 'wechat_user_suffix'
const stored = wx.getStorageSync(storageKey)
if (stored && typeof stored === 'object' && stored[uid]) return stored[uid]
const suffix = this._randomWechatUserSuffix(4)
const next = { ...(stored && typeof stored === 'object' ? stored : {}), [uid]: suffix }
wx.setStorageSync(storageKey, next)
return suffix
},
loadData() {
const userInfo = app.globalData.userInfo || wx.getStorageSync('userInfo')
const token = app.globalData.token || wx.getStorageSync('token')
// 1. 先同步渲染用户基础信息(昵称/头像)
const nickname = (userInfo && (userInfo.nickname || userInfo.nickName))
? String(userInfo.nickname || userInfo.nickName).trim() : ''
let nicknameDisplay = nickname
let displayNickname = ''
if (!nickname && userInfo) {
const suffix = this._getOrCreateWechatUserSuffix(userInfo)
displayNickname = '微信用户' + suffix
nicknameDisplay = displayNickname
}
const avatarFromName = userInfo
? this._avatarFromNickname(nicknameDisplay)
: this._avatarFromNickname('点击登录')
const hasEnterprise = !!(userInfo && (userInfo.hasEnterprise === true || (userInfo.enterpriseId && Number(userInfo.enterpriseId) > 0)))
this.setData({
hasLogin: !!token || !!userInfo,
userInfo: userInfo || null,
hasEnterprise,
nicknameDisplay,
displayNickname,
avatarLetter: avatarFromName.avatarLetter,
avatarBgColor: avatarFromName.avatarBgColor
})
// 2. 已登录则从服务端拉取最近记录;失败降级读 localStorage
if (token || userInfo) {
this._loadRecentFromAPI()
this._loadPromoStats()
}
},
/** 从 /api/test/recent 拉取各类型最新记录 */
_loadRecentFromAPI() {
const scope = app.globalData.appScope || 'personal'
request({
url: `/api/test/recent?scope=${scope}`,
method: 'GET',
success: (res) => {
// res 是 wx.request 原始响应:{ statusCode, data: { code, data, message } }
const payload = res && res.data
if (!payload || payload.code !== 200 || !payload.data) {
this._loadRecentFromStorage()
return
}
const { records = {}, totalCount = 0 } = payload.data
const r = records
// DISC resultText 后端已含「型」type badge 只显示字母,去掉「型」
const discType = r.disc ? r.disc.resultText.replace(/型$/, '') : ''
this.setData({
testCount: totalCount,
hasResults: !!(r.mbti || r.disc || r.pdp || r.ai),
mbtiType: r.mbti ? r.mbti.resultText : '',
discType,
pdpType: r.pdp ? r.pdp.resultText : '',
aiType: r.ai ? r.ai.resultText : '',
mbtiTime: r.mbti ? r.mbti.testTime : '',
discTime: r.disc ? r.disc.testTime : '',
pdpTime: r.pdp ? r.pdp.testTime : '',
aiTime: r.ai ? r.ai.testTime : '',
mbtiResultId: r.mbti ? r.mbti.id : null,
discResultId: r.disc ? r.disc.id : null,
pdpResultId: r.pdp ? r.pdp.id : null,
aiResultId: r.ai ? r.ai.id : null,
})
},
fail: () => {
this._loadRecentFromStorage()
}
})
},
/** 从 /api/distribution/stats 拉取推广中心统计 */
_loadPromoStats() {
request({
url: '/api/distribution/stats',
method: 'GET',
success: (res) => {
const payload = res && res.data
if (payload && payload.code === 200 && payload.data) {
const d = payload.data
this.setData({
promoDistributionEnabled: d.distributionEnabled !== false,
promoCenterTitle: d.promoCenterTitle || '推广中心',
promoTotalInvite: d.totalInvite || 0,
promoTotalEarned: d.totalEarned || '0.00',
promoWithdrawable: d.walletBalance || '0.00'
})
}
}
})
},
/** 降级:从 localStorage 读最近记录(兼容离线或 API 失败) */
_loadRecentFromStorage() {
const mbtiResult = wx.getStorageSync('mbtiResult')
const discResult = wx.getStorageSync('discResult')
const pdpResult = wx.getStorageSync('pdpResult')
const aiResult = wx.getStorageSync('aiResult')
let testCount = 0
if (mbtiResult) testCount++
if (discResult) testCount++
if (pdpResult) testCount++
if (aiResult) testCount++
const _fmt = (ts) => {
if (!ts) return ''
const d = new Date(typeof ts === 'number' && ts < 1e12 ? ts * 1000 : ts)
if (isNaN(d.getTime())) return ''
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
}
this.setData({
testCount,
hasResults: testCount > 0,
mbtiType: mbtiResult ? getTypeOnly(mbtiResult, 'mbti') : '',
discType: discResult ? getTypeOnly(discResult, 'disc') : '',
pdpType: pdpResult ? getTypeOnly(pdpResult, 'pdp') : '',
aiType: aiResult ? (aiResult.mbtiType || aiResult.type || '') : '',
mbtiTime: _fmt(mbtiResult && (mbtiResult.createdAt || mbtiResult.timestamp || mbtiResult.testTime)),
discTime: _fmt(discResult && (discResult.createdAt || discResult.timestamp || discResult.testTime)),
pdpTime: _fmt(pdpResult && (pdpResult.createdAt || pdpResult.timestamp || pdpResult.testTime)),
aiTime: _fmt(aiResult && (aiResult.createdAt || aiResult.timestamp || aiResult.testTime)),
mbtiResultId: null,
discResultId: null,
pdpResultId: null,
aiResultId: null,
})
},
/** 点击登录:先静默登录拿到 token */
doLogin() {
this.setData({ loginLoading: true, loginFailed: false })
app.ensureLogin().then((ok) => {
this.setData({ loginLoading: false })
if (ok) {
this.loadData()
} else {
wx.showToast({ title: '登录失败,请检查网络或稍后重试', icon: 'none' })
this.setData({ loginFailed: true })
}
}).catch(() => {
this.setData({ loginLoading: false, loginFailed: true })
wx.showToast({ title: '登录失败', icon: 'none' })
})
},
goToIndex() { wx.switchTab({ url: '/pages/index/index' }) },
goToCamera() { wx.switchTab({ url: '/pages/index/camera' }) },
goToHistory() { wx.navigateTo({ url: '/pages/history/index' }) },
goToUserProfile() { wx.navigateTo({ url: '/pages/user-profile/index' }) },
goToPurchase() { wx.navigateTo({ url: '/pages/purchase/index?tab=personal' }) },
goToPurchasePersonal() { wx.navigateTo({ url: '/pages/purchase/index?tab=personal' }) },
goToPurchaseEnterprise() { wx.navigateTo({ url: '/pages/purchase/index?tab=enterprise' }) },
goToEnterprise() { wx.navigateTo({ url: '/pages/enterprise/index' }) },
goToPromo() { wx.navigateTo({ url: '/pages/promo/index' }) },
goToMyResume() { wx.navigateTo({ url: '/pages/enterprise/resume-history' }) },
goToSettings() {
wx.showToast({ title: '开发中', icon: 'none' })
},
shareApp() {
// 触发分享
},
viewMBTI() {
const id = this.data.mbtiResultId
wx.navigateTo({ url: id ? `/pages/result/mbti?id=${id}&type=mbti` : '/pages/result/mbti' })
},
viewDISC() {
const id = this.data.discResultId
wx.navigateTo({ url: id ? `/pages/result/disc?id=${id}&type=disc` : '/pages/result/disc' })
},
viewPDP() {
const id = this.data.pdpResultId
wx.navigateTo({ url: id ? `/pages/result/pdp?id=${id}&type=pdp` : '/pages/result/pdp' })
},
viewAI() {
const id = this.data.aiResultId
wx.navigateTo({ url: id ? `/pages/index/result?id=${id}&type=ai` : '/pages/index/result' })
},
logout() {
wx.showModal({
title: '确认退出',
content: '退出后测试记录仍会保留',
success: (res) => {
if (res.confirm) {
app.logout()
this.setData({ hasLogin: false, userInfo: null, loginFailed: false })
}
}
})
},
onShareAppMessage() {
const { getSharePathByScope } = require('../../utils/share')
return {
title: '神仙团队AI性格测试 - 发现你的MBTI类型',
path: getSharePathByScope('/pages/index/index')
}
},
onShareTimeline() {
const { buildShareQuery } = require('../../utils/share')
return {
title: '神仙团队AI性格测试 - 发现你的MBTI类型',
query: buildShareQuery()
}
}
})
// pages/profile/index.js - 我的页面
const app = getApp()
const { getTypeOnly } = require('../../utils/resultFormat')
const { request } = require('../../utils/request')
Page({
data: {
hasLogin: false,
userInfo: null,
balance: 0,
testCount: 0,
hasResults: false,
mbtiType: '',
discType: '',
pdpType: '',
aiType: '',
mbtiTime: '',
discTime: '',
pdpTime: '',
aiTime: '',
reviewMode: false,
/** 最近记录的数据库 ID用于跳转时传参 */
mbtiResultId: null,
discResultId: null,
pdpResultId: null,
aiResultId: null,
loginLoading: false,
loginFailed: false,
nicknameDisplay: '',
displayNickname: '',
/** 默认头像(根据昵称):无头像时显示首字+背景色 */
avatarLetter: '',
avatarBgColor: '#6366f1',
// 推广中心统计与配置(来自 /api/distribution/stats
promoDistributionEnabled: true,
promoCenterTitle: '推广中心',
promoTotalInvite: 0,
promoTotalEarned: '0.00',
promoWithdrawable: '0.00',
/** 是否有企业权限(绑定企业):有则显示「我的简历」 */
hasEnterprise: false
},
onLoad() {
// 仅在 onLoad 执行一次onShow 里的 runLoginThenLoad 会导致重复请求
this.runLoginThenLoad()
},
onShow() {
// 同步审核模式
this.setData({ reviewMode: !!app.globalData.reviewMode })
// 如果是从其他页面返回,且已经登录,则只刷新数据而不重新执行登录流程
if (this.data.hasLogin) {
this.loadData()
}
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({ selected: 2 })
}
},
/** 先确保登录完成(静默登录),再刷新页面数据 */
runLoginThenLoad() {
this.setData({ loginLoading: true, loginFailed: false })
app.ensureLogin().then((ok) => {
this.setData({ loginLoading: false, loginFailed: !ok })
this.loadData()
}).catch(() => {
this.setData({ loginLoading: false, loginFailed: true })
this.loadData()
})
},
/** 生成随机后缀:仅 26 英文字母(大小写)与数字,不含特殊字符,默认 4 位 */
_randomWechatUserSuffix(len) {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
let s = ''
for (let i = 0; i < (len || 4); i++) {
s += chars[Math.floor(Math.random() * chars.length)]
}
return s
},
/** 根据昵称生成默认头像的首字与背景色(同一昵称同色) */
_avatarFromNickname(name) {
const str = (name && String(name).trim()) || '登'
const letter = str.charAt(0).toUpperCase() || '登'
const palette = ['#6366f1', '#8b5cf6', '#ec4899', '#f43f5e', '#14b8a6', '#0ea5e9', '#3b82f6', '#eab308']
let hash = 0
for (let i = 0; i < str.length; i++) hash += str.charCodeAt(i)
const bgColor = palette[Math.abs(hash) % palette.length]
return { avatarLetter: letter, avatarBgColor: bgColor }
},
/** 获取或生成当前用户的「微信用户XXXX」后缀同一用户固定仅 26 字母+数字) */
_getOrCreateWechatUserSuffix(userInfo) {
const uid = (userInfo && (userInfo.id || userInfo.userId)) ? String(userInfo.id || userInfo.userId) : '_default'
const storageKey = 'wechat_user_suffix'
const stored = wx.getStorageSync(storageKey)
if (stored && typeof stored === 'object' && stored[uid]) return stored[uid]
const suffix = this._randomWechatUserSuffix(4)
const next = { ...(stored && typeof stored === 'object' ? stored : {}), [uid]: suffix }
wx.setStorageSync(storageKey, next)
return suffix
},
loadData() {
const userInfo = app.globalData.userInfo || wx.getStorageSync('userInfo')
const token = app.globalData.token || wx.getStorageSync('token')
// 1. 先同步渲染用户基础信息(昵称/头像)
const nickname = (userInfo && (userInfo.nickname || userInfo.nickName))
? String(userInfo.nickname || userInfo.nickName).trim() : ''
let nicknameDisplay = nickname
let displayNickname = ''
if (!nickname && userInfo) {
const suffix = this._getOrCreateWechatUserSuffix(userInfo)
displayNickname = '微信用户' + suffix
nicknameDisplay = displayNickname
}
const avatarFromName = userInfo
? this._avatarFromNickname(nicknameDisplay)
: this._avatarFromNickname('点击登录')
const hasEnterprise = !!(userInfo && (userInfo.hasEnterprise === true || (userInfo.enterpriseId && Number(userInfo.enterpriseId) > 0)))
this.setData({
hasLogin: !!token || !!userInfo,
userInfo: userInfo || null,
hasEnterprise,
nicknameDisplay,
displayNickname,
avatarLetter: avatarFromName.avatarLetter,
avatarBgColor: avatarFromName.avatarBgColor
})
// 2. 已登录则从服务端拉取最近记录;失败降级读 localStorage
if (token || userInfo) {
this._loadRecentFromAPI()
this._loadPromoStats()
}
},
/** 从 /api/test/recent 拉取各类型最新记录 */
_loadRecentFromAPI() {
const scope = app.globalData.appScope || 'personal'
request({
url: `/api/test/recent?scope=${scope}`,
method: 'GET',
success: (res) => {
// res 是 wx.request 原始响应:{ statusCode, data: { code, data, message } }
const payload = res && res.data
if (!payload || payload.code !== 200 || !payload.data) {
this._loadRecentFromStorage()
return
}
const { records = {}, totalCount = 0 } = payload.data
const r = records
// DISC resultText 后端已含「型」type badge 只显示字母,去掉「型」
const discType = r.disc ? r.disc.resultText.replace(/型$/, '') : ''
this.setData({
testCount: totalCount,
hasResults: !!(r.mbti || r.disc || r.pdp || r.ai),
mbtiType: r.mbti ? r.mbti.resultText : '',
discType,
pdpType: r.pdp ? r.pdp.resultText : '',
aiType: r.ai ? r.ai.resultText : '',
mbtiTime: r.mbti ? r.mbti.testTime : '',
discTime: r.disc ? r.disc.testTime : '',
pdpTime: r.pdp ? r.pdp.testTime : '',
aiTime: r.ai ? r.ai.testTime : '',
mbtiResultId: r.mbti ? r.mbti.id : null,
discResultId: r.disc ? r.disc.id : null,
pdpResultId: r.pdp ? r.pdp.id : null,
aiResultId: r.ai ? r.ai.id : null,
})
},
fail: () => {
this._loadRecentFromStorage()
}
})
},
/** 从 /api/distribution/stats 拉取推广中心统计 */
_loadPromoStats() {
request({
url: '/api/distribution/stats',
method: 'GET',
success: (res) => {
const payload = res && res.data
if (payload && payload.code === 200 && payload.data) {
const d = payload.data
this.setData({
promoDistributionEnabled: d.distributionEnabled !== false,
promoCenterTitle: d.promoCenterTitle || '推广中心',
promoTotalInvite: d.totalInvite || 0,
promoTotalEarned: d.totalEarned || '0.00',
promoWithdrawable: d.walletBalance || '0.00'
})
}
}
})
},
/** 降级:从 localStorage 读最近记录(兼容离线或 API 失败) */
_loadRecentFromStorage() {
const mbtiResult = wx.getStorageSync('mbtiResult')
const discResult = wx.getStorageSync('discResult')
const pdpResult = wx.getStorageSync('pdpResult')
const aiResult = wx.getStorageSync('aiResult')
let testCount = 0
if (mbtiResult) testCount++
if (discResult) testCount++
if (pdpResult) testCount++
if (aiResult) testCount++
const _fmt = (ts) => {
if (!ts) return ''
const d = new Date(typeof ts === 'number' && ts < 1e12 ? ts * 1000 : ts)
if (isNaN(d.getTime())) return ''
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
}
this.setData({
testCount,
hasResults: testCount > 0,
mbtiType: mbtiResult ? getTypeOnly(mbtiResult, 'mbti') : '',
discType: discResult ? getTypeOnly(discResult, 'disc') : '',
pdpType: pdpResult ? getTypeOnly(pdpResult, 'pdp') : '',
aiType: aiResult ? (aiResult.mbtiType || aiResult.type || '') : '',
mbtiTime: _fmt(mbtiResult && (mbtiResult.createdAt || mbtiResult.timestamp || mbtiResult.testTime)),
discTime: _fmt(discResult && (discResult.createdAt || discResult.timestamp || discResult.testTime)),
pdpTime: _fmt(pdpResult && (pdpResult.createdAt || pdpResult.timestamp || pdpResult.testTime)),
aiTime: _fmt(aiResult && (aiResult.createdAt || aiResult.timestamp || aiResult.testTime)),
mbtiResultId: null,
discResultId: null,
pdpResultId: null,
aiResultId: null,
})
},
/** 点击登录:先静默登录拿到 token */
doLogin() {
this.setData({ loginLoading: true, loginFailed: false })
app.ensureLogin().then((ok) => {
this.setData({ loginLoading: false })
if (ok) {
this.loadData()
} else {
wx.showToast({ title: '登录失败,请检查网络或稍后重试', icon: 'none' })
this.setData({ loginFailed: true })
}
}).catch(() => {
this.setData({ loginLoading: false, loginFailed: true })
wx.showToast({ title: '登录失败', icon: 'none' })
})
},
goToIndex() { wx.switchTab({ url: '/pages/index/index' }) },
goToCamera() { wx.switchTab({ url: '/pages/index/camera' }) },
goToHistory() { wx.navigateTo({ url: '/pages/history/index' }) },
goToUserProfile() { wx.navigateTo({ url: '/pages/user-profile/index' }) },
goToPurchase() { wx.navigateTo({ url: '/pages/purchase/index?tab=personal' }) },
goToPurchasePersonal() { wx.navigateTo({ url: '/pages/purchase/index?tab=personal' }) },
goToPurchaseEnterprise() { wx.navigateTo({ url: '/pages/purchase/index?tab=enterprise' }) },
goToEnterprise() { wx.navigateTo({ url: '/pages/enterprise/index' }) },
goToPromo() { wx.navigateTo({ url: '/pages/promo/index' }) },
goToMyResume() { wx.navigateTo({ url: '/pages/enterprise/resume-history' }) },
goToSettings() {
wx.showToast({ title: '开发中', icon: 'none' })
},
shareApp() {
// 触发分享
},
viewMBTI() {
const id = this.data.mbtiResultId
wx.navigateTo({ url: id ? `/pages/result/mbti?id=${id}&type=mbti` : '/pages/result/mbti' })
},
viewDISC() {
const id = this.data.discResultId
wx.navigateTo({ url: id ? `/pages/result/disc?id=${id}&type=disc` : '/pages/result/disc' })
},
viewPDP() {
const id = this.data.pdpResultId
wx.navigateTo({ url: id ? `/pages/result/pdp?id=${id}&type=pdp` : '/pages/result/pdp' })
},
viewAI() {
const id = this.data.aiResultId
wx.navigateTo({ url: id ? `/pages/index/result?id=${id}&type=ai` : '/pages/index/result' })
},
logout() {
wx.showModal({
title: '确认退出',
content: '退出后测试记录仍会保留',
success: (res) => {
if (res.confirm) {
app.logout()
this.setData({ hasLogin: false, userInfo: null, loginFailed: false })
}
}
})
},
onShareAppMessage() {
const { getSharePathByScope } = require('../../utils/share')
const rm = !!app.globalData.reviewMode
return {
title: rm ? '神仙团队性格测试 - 发现你的MBTI类型' : '神仙团队AI性格测试 - 发现你的MBTI类型',
path: getSharePathByScope('/pages/index/index')
}
},
onShareTimeline() {
const { buildShareQuery } = require('../../utils/share')
const rm = !!app.globalData.reviewMode
return {
title: rm ? '神仙团队性格测试 - 发现你的MBTI类型' : '神仙团队AI性格测试 - 发现你的MBTI类型',
query: buildShareQuery()
}
}
})

View File

@@ -1,212 +1,212 @@
<!--pages/profile/index.wxml-->
<view class="page">
<!-- 顶部栏 -->
<view class="topbar">
<text class="topbar-title">我的</text>
<view class="topbar-actions">
<!-- <view class="settings-btn" bindtap="goToSettings">
<text class="settings-icon">⚙️</text>
</view> -->
</view>
</view>
<!-- 未登录 -->
<view class="user-card" wx:if="{{!hasLogin}}" bindtap="doLogin">
<view class="avatar-ring">
<view class="avatar-letter-wrap" style="background:{{avatarBgColor}}">
<text class="avatar-letter">{{avatarLetter}}</text>
</view>
</view>
<view class="user-meta">
<text class="user-name" wx:if="{{loginLoading}}">登录中...</text>
<text class="user-name" wx:elif="{{loginFailed}}">登录失败,点击重试</text>
<text class="user-name" wx:else>点击登录</text>
<text class="user-sub">登录后查看你的测试结果</text>
</view>
<text class="chevron"></text>
</view>
<!-- 已登录:用户卡片(点击进入个人资料) -->
<view class="user-card" wx:if="{{hasLogin}}" bindtap="goToUserProfile">
<!-- 头像区 -->
<view class="avatar-wrap">
<view class="avatar-ring">
<image wx:if="{{userInfo && (userInfo.avatarUrl || userInfo.avatar)}}"
class="avatar-img"
src="{{userInfo.avatarUrl || userInfo.avatar}}"
mode="aspectFill"/>
<view wx:else class="avatar-letter-wrap" style="background:{{avatarBgColor}}">
<text class="avatar-letter">{{avatarLetter}}</text>
</view>
</view>
<view class="online-dot"></view>
</view>
<!-- 昵称 + 标签 -->
<view class="user-meta">
<text class="nickname-text">{{nicknameDisplay || '点击设置昵称'}}</text>
<!-- 类型标签 -->
<scroll-view scroll-x class="tags-scroll">
<view class="tags-row">
<view class="tag tag-purple" wx:if="{{mbtiType}}">
<text class="tag-text">{{mbtiType}}</text>
</view>
<view class="tag tag-blue" wx:if="{{discType}}">
<text class="tag-text">{{discType}}型</text>
</view>
<view class="tag tag-orange" wx:if="{{pdpType}}">
<text class="tag-text">{{pdpType}}</text>
</view>
<view class="tag tag-gray" wx:if="{{!mbtiType && !discType && !pdpType}}">
<text class="tag-text">暂无测试记录</text>
</view>
</view>
</scroll-view>
</view>
<text class="chevron"></text>
</view>
<!-- 最新测试卡片(横向滚动) -->
<view class="section" wx:if="{{hasLogin && hasResults}}">
<view class="section-header">
<text class="section-title">最新测试</text>
<view class="section-link" bindtap="goToHistory">
<text class="section-link-text">查看全部</text>
</view>
</view>
<scroll-view scroll-x class="cards-scroll" enhanced show-scrollbar="{{false}}">
<view class="cards-row">
<!-- MBTI -->
<view class="result-card card-purple" wx:if="{{mbtiType}}" bindtap="viewMBTI">
<view class="card-deco"></view>
<view class="card-icon-wrap card-icon-purple">
<text class="card-icon">🧠</text>
</view>
<text class="card-label">MBTI性格</text>
<text class="card-value">{{mbtiType}}</text>
<text class="card-time">{{mbtiTime}}</text>
</view>
<!-- DISC -->
<view class="result-card card-blue" wx:if="{{discType}}" bindtap="viewDISC">
<view class="card-deco"></view>
<view class="card-icon-wrap card-icon-blue">
<text class="card-icon">📊</text>
</view>
<text class="card-label">DISC测评</text>
<text class="card-value">{{discType}}型</text>
<text class="card-time">{{discTime}}</text>
</view>
<!-- PDP -->
<view class="result-card card-orange" wx:if="{{pdpType}}" bindtap="viewPDP">
<view class="card-deco"></view>
<view class="card-icon-wrap card-icon-orange">
<text class="card-icon">🦁</text>
</view>
<text class="card-label">PDP行为</text>
<text class="card-value">{{pdpType}}</text>
<text class="card-time">{{pdpTime}}</text>
</view>
<!-- AI 面相 -->
<view class="result-card card-rose" wx:if="{{aiType}}" bindtap="viewAI">
<view class="card-deco"></view>
<view class="card-icon-wrap card-icon-rose">
<text class="card-icon">👁️</text>
</view>
<text class="card-label">面相分析</text>
<text class="card-value">{{aiType}}</text>
<text class="card-time">{{aiTime}}</text>
</view>
</view>
</scroll-view>
</view>
<!-- 推广中心(根据管理端开关显示/隐藏,标题可配置) -->
<view class="section px-section" wx:if="{{hasLogin && promoDistributionEnabled}}">
<view class="promo-card" bindtap="goToPromo">
<view class="promo-header">
<view class="promo-title-wrap">
<text class="promo-icon">📈</text>
<text class="promo-title">{{promoCenterTitle}}</text>
</view>
</view>
<view class="promo-stats">
<view class="promo-stat-item">
<text class="promo-stat-label">邀请好友</text>
<text class="promo-stat-value">{{promoTotalInvite}}</text>
</view>
<view class="promo-stat-divider"></view>
<view class="promo-stat-item">
<text class="promo-stat-label">累计收益</text>
<text class="promo-stat-value">¥{{promoTotalEarned}}</text>
</view>
<view class="promo-stat-divider"></view>
<view class="promo-stat-item">
<text class="promo-stat-label">可提现</text>
<text class="promo-stat-value promo-highlight">¥{{promoWithdrawable}}</text>
</view>
</view>
</view>
</view>
<!-- 服务菜单 -->
<view class="section px-section">
<text class="section-title">深度服务</text>
<view class="menu-card">
<!-- 专业报告:跳到个人版开通页面 -->
<view class="menu-item" bindtap="goToPurchasePersonal">
<view class="menu-icon-wrap menu-icon-red">
<text class="menu-icon">📄</text>
</view>
<view class="menu-content">
<text class="menu-title">专业报告</text>
<text class="menu-sub">解锁完整的深度性格解析</text>
</view>
<text class="menu-chevron"></text>
</view>
<view class="menu-divider"></view>
<!-- 企业版服务:跳到企业版开通页面 -->
<view class="menu-item" bindtap="goToPurchaseEnterprise">
<view class="menu-icon-wrap menu-icon-indigo">
<text class="menu-icon">🏢</text>
</view>
<view class="menu-content">
<text class="menu-title">企业版服务</text>
<text class="menu-sub">团队测评与人才管理方案</text>
</view>
<text class="menu-chevron"></text>
</view>
<view class="menu-divider"></view>
<!-- 测试历史:仍然进入历史记录页面 -->
<view class="menu-item" bindtap="goToHistory">
<view class="menu-icon-wrap menu-icon-amber">
<text class="menu-icon">🕒</text>
</view>
<view class="menu-content">
<text class="menu-title">测试历史</text>
<text class="menu-sub">{{testCount > 0 ? testCount + '条记录' : '查看过往所有测试记录'}}</text>
</view>
<text class="menu-chevron"></text>
</view>
<view class="menu-divider" wx:if="{{hasEnterprise}}"></view>
<!-- 我的简历:仅绑定企业的用户可见,展示当前企业下的简历并可设默认 -->
<view class="menu-item" wx:if="{{hasEnterprise}}" bindtap="goToMyResume">
<view class="menu-icon-wrap menu-icon-indigo">
<text class="menu-icon">📋</text>
</view>
<view class="menu-content">
<text class="menu-title">我的简历</text>
<text class="menu-sub">查看与设置默认简历</text>
</view>
<text class="menu-chevron"></text>
</view>
</view>
</view>
<view class="bottom-safe"></view>
</view>
<!--pages/profile/index.wxml-->
<view class="page">
<!-- 顶部栏 -->
<view class="topbar">
<text class="topbar-title">我的</text>
<view class="topbar-actions">
<!-- <view class="settings-btn" bindtap="goToSettings">
<text class="settings-icon">⚙️</text>
</view> -->
</view>
</view>
<!-- 未登录 -->
<view class="user-card" wx:if="{{!hasLogin}}" bindtap="doLogin">
<view class="avatar-ring">
<view class="avatar-letter-wrap" style="background:{{avatarBgColor}}">
<text class="avatar-letter">{{avatarLetter}}</text>
</view>
</view>
<view class="user-meta">
<text class="user-name" wx:if="{{loginLoading}}">登录中...</text>
<text class="user-name" wx:elif="{{loginFailed}}">登录失败,点击重试</text>
<text class="user-name" wx:else>点击登录</text>
<text class="user-sub">登录后查看你的测试结果</text>
</view>
<text class="chevron"></text>
</view>
<!-- 已登录:用户卡片(点击进入个人资料) -->
<view class="user-card" wx:if="{{hasLogin}}" bindtap="goToUserProfile">
<!-- 头像区 -->
<view class="avatar-wrap">
<view class="avatar-ring">
<image wx:if="{{userInfo && (userInfo.avatarUrl || userInfo.avatar)}}"
class="avatar-img"
src="{{userInfo.avatarUrl || userInfo.avatar}}"
mode="aspectFill"/>
<view wx:else class="avatar-letter-wrap" style="background:{{avatarBgColor}}">
<text class="avatar-letter">{{avatarLetter}}</text>
</view>
</view>
<view class="online-dot"></view>
</view>
<!-- 昵称 + 标签 -->
<view class="user-meta">
<text class="nickname-text">{{nicknameDisplay || '点击设置昵称'}}</text>
<!-- 类型标签 -->
<scroll-view scroll-x class="tags-scroll">
<view class="tags-row">
<view class="tag tag-purple" wx:if="{{mbtiType}}">
<text class="tag-text">{{mbtiType}}</text>
</view>
<view class="tag tag-blue" wx:if="{{discType}}">
<text class="tag-text">{{discType}}型</text>
</view>
<view class="tag tag-orange" wx:if="{{pdpType}}">
<text class="tag-text">{{pdpType}}</text>
</view>
<view class="tag tag-gray" wx:if="{{!mbtiType && !discType && !pdpType}}">
<text class="tag-text">暂无测试记录</text>
</view>
</view>
</scroll-view>
</view>
<text class="chevron"></text>
</view>
<!-- 最新测试卡片(横向滚动) -->
<view class="section" wx:if="{{hasLogin && hasResults}}">
<view class="section-header">
<text class="section-title">最新测试</text>
<view class="section-link" bindtap="goToHistory">
<text class="section-link-text">查看全部</text>
</view>
</view>
<scroll-view scroll-x class="cards-scroll" enhanced show-scrollbar="{{false}}">
<view class="cards-row">
<!-- MBTI -->
<view class="result-card card-purple" wx:if="{{mbtiType}}" bindtap="viewMBTI">
<view class="card-deco"></view>
<view class="card-icon-wrap card-icon-purple">
<text class="card-icon">🧠</text>
</view>
<text class="card-label">MBTI性格</text>
<text class="card-value">{{mbtiType}}</text>
<text class="card-time">{{mbtiTime}}</text>
</view>
<!-- DISC -->
<view class="result-card card-blue" wx:if="{{discType}}" bindtap="viewDISC">
<view class="card-deco"></view>
<view class="card-icon-wrap card-icon-blue">
<text class="card-icon">📊</text>
</view>
<text class="card-label">DISC测评</text>
<text class="card-value">{{discType}}型</text>
<text class="card-time">{{discTime}}</text>
</view>
<!-- PDP -->
<view class="result-card card-orange" wx:if="{{pdpType}}" bindtap="viewPDP">
<view class="card-deco"></view>
<view class="card-icon-wrap card-icon-orange">
<text class="card-icon">🦁</text>
</view>
<text class="card-label">PDP行为</text>
<text class="card-value">{{pdpType}}</text>
<text class="card-time">{{pdpTime}}</text>
</view>
<!-- AI 面相(审核模式下隐藏) -->
<view class="result-card card-rose" wx:if="{{aiType && !reviewMode}}" bindtap="viewAI">
<view class="card-deco"></view>
<view class="card-icon-wrap card-icon-rose">
<text class="card-icon">👁️</text>
</view>
<text class="card-label">面相分析</text>
<text class="card-value">{{aiType}}</text>
<text class="card-time">{{aiTime}}</text>
</view>
</view>
</scroll-view>
</view>
<!-- 推广中心(根据管理端开关显示/隐藏,标题可配置) -->
<view class="section px-section" wx:if="{{hasLogin && promoDistributionEnabled}}">
<view class="promo-card" bindtap="goToPromo">
<view class="promo-header">
<view class="promo-title-wrap">
<text class="promo-icon">📈</text>
<text class="promo-title">{{promoCenterTitle}}</text>
</view>
</view>
<view class="promo-stats">
<view class="promo-stat-item">
<text class="promo-stat-label">邀请好友</text>
<text class="promo-stat-value">{{promoTotalInvite}}</text>
</view>
<view class="promo-stat-divider"></view>
<view class="promo-stat-item">
<text class="promo-stat-label">累计收益</text>
<text class="promo-stat-value">¥{{promoTotalEarned}}</text>
</view>
<view class="promo-stat-divider"></view>
<view class="promo-stat-item">
<text class="promo-stat-label">可提现</text>
<text class="promo-stat-value promo-highlight">¥{{promoWithdrawable}}</text>
</view>
</view>
</view>
</view>
<!-- 服务菜单 -->
<view class="section px-section">
<text class="section-title">深度服务</text>
<view class="menu-card">
<!-- 专业报告:跳到个人版开通页面 -->
<view class="menu-item" bindtap="goToPurchasePersonal">
<view class="menu-icon-wrap menu-icon-red">
<text class="menu-icon">📄</text>
</view>
<view class="menu-content">
<text class="menu-title">专业报告</text>
<text class="menu-sub">解锁完整的深度性格解析</text>
</view>
<text class="menu-chevron"></text>
</view>
<view class="menu-divider"></view>
<!-- 企业版服务:跳到企业版开通页面 -->
<view class="menu-item" bindtap="goToPurchaseEnterprise">
<view class="menu-icon-wrap menu-icon-indigo">
<text class="menu-icon">🏢</text>
</view>
<view class="menu-content">
<text class="menu-title">企业版服务</text>
<text class="menu-sub">团队测评与人才管理方案</text>
</view>
<text class="menu-chevron"></text>
</view>
<view class="menu-divider"></view>
<!-- 测试历史:仍然进入历史记录页面 -->
<view class="menu-item" bindtap="goToHistory">
<view class="menu-icon-wrap menu-icon-amber">
<text class="menu-icon">🕒</text>
</view>
<view class="menu-content">
<text class="menu-title">测试历史</text>
<text class="menu-sub">{{testCount > 0 ? testCount + '条记录' : '查看过往所有测试记录'}}</text>
</view>
<text class="menu-chevron"></text>
</view>
<view class="menu-divider" wx:if="{{hasEnterprise}}"></view>
<!-- 我的简历:仅绑定企业的用户可见,展示当前企业下的简历并可设默认 -->
<view class="menu-item" wx:if="{{hasEnterprise}}" bindtap="goToMyResume">
<view class="menu-icon-wrap menu-icon-indigo">
<text class="menu-icon">📋</text>
</view>
<view class="menu-content">
<text class="menu-title">我的简历</text>
<text class="menu-sub">查看与设置默认简历</text>
</view>
<text class="menu-chevron"></text>
</view>
</view>
</view>
<view class="bottom-safe"></view>
</view>