diff --git a/admin/package-lock.json b/admin/package-lock.json index 4a513a0..61b506b 100644 --- a/admin/package-lock.json +++ b/admin/package-lock.json @@ -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": "*" diff --git a/admin/src/views/superadmin/Settings.vue b/admin/src/views/superadmin/Settings.vue index a3d829b..421cc46 100644 --- a/admin/src/views/superadmin/Settings.vue +++ b/admin/src/views/superadmin/Settings.vue @@ -1,871 +1,1078 @@ - - - - - + + + + + diff --git a/api/app/controller/api/AppConfig.php b/api/app/controller/api/AppConfig.php index ce30514..39763fc 100644 --- a/api/app/controller/api/AppConfig.php +++ b/api/app/controller/api/AppConfig.php @@ -1,201 +1,212 @@ -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_config(enterprise_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]); - } -} +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_config(enterprise_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]); + } +} diff --git a/api/app/controller/superadmin/Settings.php b/api/app/controller/superadmin/Settings.php index f0ea893..0056498 100644 --- a/api/app/controller/superadmin/Settings.php +++ b/api/app/controller/superadmin/Settings.php @@ -1,494 +1,530 @@ -request->user ?? null; - if (!$user || $user['role'] !== 'superadmin') { - return error('无权限访问', 403); - } - - try { - // 获取系统配置(全局 enterprise_id=0) - $systemConfig = SystemConfigModel::where('key', 'system')->where('enterprise_id', 0)->find(); - $notificationConfig = SystemConfigModel::where('key', 'notification')->where('enterprise_id', 0)->find(); - $promptsConfig = SystemConfigModel::where('key', 'prompts')->where('enterprise_id', 0)->find(); - $reportRequiresPaymentConfig = SystemConfigModel::where('key', 'report_requires_payment')->where('enterprise_id', 0)->find(); - $textConfigModel = SystemConfigModel::where('key', 'text_config')->where('enterprise_id', 0)->find(); - - // 获取当前超管用户名(直接使用JWT中的username) - $jwtUsername = $user['username'] ?? null; - $username = 'admin'; - - if ($jwtUsername) { - $currentUser = UserModel::where('username', $jwtUsername) - ->where('role', 'superadmin') - ->find(); - if ($currentUser) { - $username = $currentUser->username; - } else { - // 如果找不到用户,使用JWT中的username - $username = $jwtUsername; - } - } - - return success([ - 'system' => $systemConfig ? $systemConfig->value : [ - 'siteName' => '神仙团队AI性格测试', - 'siteDescription' => '专业的AI性格测试平台', - 'miniprogramName' => '神仙团队AI性格测试', - 'maintenanceMode' => false, - 'maxTestsPerDay' => 100, - 'trialTestCount' => 10 - ], - 'notification' => $notificationConfig ? $notificationConfig->value : [ - 'emailNotification' => true, - 'lowBalanceAlert' => true, - 'lowBalanceThreshold' => 1000, - 'newEnterpriseNotify' => true - ], - 'prompts' => $promptsConfig && !empty($promptsConfig->value) ? $promptsConfig->value : [ - 'faceAnalyze' => '{"mbti":"四字母如INTJ","pdp":"老虎/孔雀/考拉/猫头鹰/变色龙其一","disc":"D/I/S/C其一","overview":"一段50字以内的综合描述","faceAnalysis":"面相特点简短描述"}', - 'reportSummary' => '' - ], - 'reportRequiresPayment' => $reportRequiresPaymentConfig && !empty($reportRequiresPaymentConfig->value) ? $reportRequiresPaymentConfig->value : ['face' => 1, 'mbti' => 0, 'disc' => 0, 'pdp' => 0], - 'textConfig' => $textConfigModel && !empty($textConfigModel->value) ? $textConfigModel->value : [ - 'analyzingTitle' => '正在分析中', - 'startButtonText' => '开始面相测试', - 'startButtonEnterprise' => '开始面部测试', - 'reportTitle' => '分析报告', - 'aiAnalysisText' => '智能分析' - ], - 'username' => $username - ]); - } catch (\Exception $e) { - return error('获取配置失败:' . $e->getMessage(), 500); - } - } - - /** - * 更新系统配置 - * @return \think\response\Json - */ - public function updateSystem() - { - // 验证是否为超级管理员 - $user = $this->request->user ?? null; - if (!$user || $user['role'] !== 'superadmin') { - return error('无权限访问', 403); - } - - // 前端 axios 发 JSON body,用 getContent 解析更可靠 - $input = json_decode($this->request->getContent(), true); - if (!is_array($input)) { - $input = []; - } - - $allowedKeys = ['siteName', 'siteDescription', 'miniprogramName', 'maintenanceMode', 'maxTestsPerDay', 'trialTestCount']; - $data = array_intersect_key($input, array_flip($allowedKeys)); - // 兼容 fallback:JSON 解析失败时尝试 Request::only - if (empty($data)) { - $data = Request::only($allowedKeys); - } - $textConfig = $input['textConfig'] ?? (Request::param('textConfig') ?: []); - - try { - // 查找或创建全局配置(enterprise_id=0) - $config = SystemConfigModel::where('key', 'system')->where('enterprise_id', 0)->find(); - if (!$config) { - $config = new SystemConfigModel(); - $config->key = 'system'; - $config->enterprise_id = 0; - $config->description = '系统基础配置'; - } - $config->value = $data; - $config->save(); - - // 更新站点信息 - $this->updateSiteInfo($data); - - // 保存全局小程序文案配置(enterprise_id=0) - if (is_array($textConfig)) { - $tcKeys = ['analyzingTitle', 'startButtonText', 'startButtonEnterprise', 'reportTitle', 'aiAnalysisText']; - $tcData = array_intersect_key($textConfig, array_flip($tcKeys)); - $tcDefaults = ['analyzingTitle' => '正在分析中', 'startButtonText' => '开始面相测试', 'startButtonEnterprise' => '开始面部测试', 'reportTitle' => '分析报告', 'aiAnalysisText' => '智能分析']; - $tcConfig = SystemConfigModel::where('key', 'text_config')->where('enterprise_id', 0)->find(); - if (!$tcConfig) { - $tcConfig = new SystemConfigModel(); - $tcConfig->key = 'text_config'; - $tcConfig->enterprise_id = 0; - $tcConfig->description = '小程序文案配置(全局)'; - } - $tcConfig->value = array_merge($tcDefaults, $tcData); - $tcConfig->save(); - } - - return success($config->value, '系统配置已保存'); - } catch (\Exception $e) { - return error('保存失败:' . $e->getMessage(), 500); - } - } - - /** - * 更新「报告需付费」配置:哪些测试类型需付费后才显示完整报告 - * PUT body: { "face": 1, "mbti": 0, "disc": 0, "pdp": 0 }(1=需付费解锁完整,0=免费完整) - * @return \think\response\Json - */ - public function updateReportRequiresPayment() - { - $user = $this->request->user ?? null; - if (!$user || $user['role'] !== 'superadmin') { - return error('无权限访问', 403); - } - - $data = Request::param('reportRequiresPayment', Request::param('data', [])); - if (!is_array($data)) { - return error('配置格式错误', 400); - } - - $defaults = ['face' => 1, 'mbti' => 0, 'disc' => 0, 'pdp' => 0]; - $value = array_merge($defaults, array_intersect_key($data, array_flip(['face', 'mbti', 'disc', 'pdp']))); - $value = array_map(function ($v) { return (int) $v ? 1 : 0; }, $value); - - try { - $config = SystemConfigModel::where('key', 'report_requires_payment')->where('enterprise_id', 0)->find(); - if (!$config) { - $config = new SystemConfigModel(); - $config->key = 'report_requires_payment'; - $config->enterprise_id = 0; - $config->description = '哪些测试类型需付费后才显示完整报告:1需付费0免费'; - } - $config->value = $value; - $config->save(); - return success($config->value, '报告付费开关已保存'); - } catch (\Exception $e) { - return error('保存失败:' . $e->getMessage(), 500); - } - } - - /** - * 更新提示词配置 - * @return \think\response\Json - */ - public function updatePrompts() - { - $user = $this->request->user ?? null; - if (!$user || $user['role'] !== 'superadmin') { - return error('无权限访问', 403); - } - - $data = Request::param('prompts', []); - if (!is_array($data)) { - return error('提示词配置格式错误', 400); - } - - try { - $config = SystemConfigModel::where('key', 'prompts')->where('enterprise_id', 0)->find(); - if (!$config) { - $config = new SystemConfigModel(); - $config->key = 'prompts'; - $config->enterprise_id = 0; - $config->description = '系统提示词配置(如面相分析、企业简历等)'; - } - $config->value = $data; - $config->save(); - return success($config->value, '提示词配置已保存'); - } catch (\Exception $e) { - return error('保存失败:' . $e->getMessage(), 500); - } - } - - /** - * 更新通知配置 - * @return \think\response\Json - */ - public function updateNotification() - { - // 验证是否为超级管理员 - $user = $this->request->user ?? null; - if (!$user || $user['role'] !== 'superadmin') { - return error('无权限访问', 403); - } - - $data = Request::only([ - 'emailNotification', 'lowBalanceAlert', - 'lowBalanceThreshold', 'newEnterpriseNotify' - ]); - - try { - $config = SystemConfigModel::where('key', 'notification')->where('enterprise_id', 0)->find(); - if (!$config) { - $config = new SystemConfigModel(); - $config->key = 'notification'; - $config->enterprise_id = 0; - $config->description = '通知与告警配置'; - } - $config->value = $data; - $config->save(); - - return success($config->value, '通知配置已保存'); - } catch (\Exception $e) { - return error('保存失败:' . $e->getMessage(), 500); - } - } - - /** - * 更新超管账户信息 - * @return \think\response\Json - */ - public function updateCredentials() - { - // 验证是否为超级管理员 - $user = $this->request->user ?? null; - if (!$user || ($user['role'] ?? '') !== 'superadmin') { - return error('无权限访问', 403); - } - - // 兼容 axios JSON PUT 与表单提交 - $rawBody = $this->request->getContent(); - if (empty($rawBody)) { - $rawBody = file_get_contents('php://input'); - } - $input = $rawBody ? json_decode($rawBody, true) : null; - if (!is_array($input)) { - $input = []; - } - - $username = trim((string)($input['username'] ?? Request::param('username', ''))); - $currentPassword = (string)($input['currentPassword'] ?? Request::param('currentPassword', '')); - $newPassword = (string)($input['newPassword'] ?? Request::param('newPassword', '')); - $confirmPassword = (string)($input['confirmPassword'] ?? Request::param('confirmPassword', '')); - - if (empty($username)) { - return error('用户名不能为空', 400); - } - - try { - // 优先使用JWT中的username来查找用户(最可靠的方式) - $jwtUsername = $user['username'] ?? null; - - if (empty($jwtUsername)) { - \think\facade\Log::error('JWT中缺少username', [ - 'user' => $user, - 'requestUserId' => $this->request->userId ?? null - ]); - return error('无法获取用户信息,请重新登录', 400); - } - - // 直接通过username查找用户 - $userModel = UserModel::where('username', $jwtUsername) - ->where('role', 'superadmin') - ->find(); - - if (!$userModel) { - // 添加调试信息 - \think\facade\Log::error('用户不存在', [ - 'jwtUsername' => $jwtUsername, - 'user' => $user, - 'requestUserId' => $this->request->userId ?? null, - 'requestUsername' => $username - ]); - return error('用户不存在,请检查登录状态', 404); - } - - // 验证当前用户是否为超级管理员(双重验证) - if ($userModel->role !== 'superadmin') { - \think\facade\Log::error('用户角色不正确', [ - 'userId' => $userModel->id, - 'role' => $userModel->role - ]); - return error('无权限修改此账户', 403); - } - - // 如果要修改密码,需要验证当前密码 - if (!empty($newPassword)) { - if (empty($currentPassword)) { - return error('请输入当前密码', 400); - } - - if ($newPassword !== $confirmPassword) { - return error('两次输入的密码不一致', 400); - } - - // 验证当前密码(User 模型中 password 字段已是加密值) - if (!password_verify($currentPassword, $userModel->password)) { - return error('当前密码错误', 400); - } - - // 更新密码:传入明文,交由 User 模型的 setPasswordAttr 自动加密 - $userModel->password = $newPassword; - } - - // 更新用户名 - if ($username !== $userModel->username) { - // 检查用户名是否已存在(排除当前用户) - $exists = UserModel::where('username', $username) - ->where('id', '<>', $userModel->id) - ->find(); - - if ($exists) { - return error('用户名已存在', 400); - } - - $userModel->username = $username; - } - - $userModel->save(); - - return success([ - 'username' => $userModel->username - ], '账户信息已更新'); - } catch (\Exception $e) { - return error('更新失败:' . $e->getMessage(), 500); - } - } - - /** - * 获取可用字体列表 - * GET /api/v1/superadmin/settings/fonts - */ - public function getFonts() - { - $user = $this->request->user ?? null; - if (!$user || $user['role'] !== 'superadmin') { - return error('无权限访问', 403); - } - $fonts = \app\common\service\PosterService::getAvailableFonts(); - return success([ - 'fonts' => $fonts, - 'fontDir' => root_path() . 'public/fonts/', - 'dirExist' => is_dir(root_path() . 'public/fonts/'), - ]); - } - - /** - * 获取海报配置 - * GET /api/v1/superadmin/settings/poster - */ - public function getPosterConfig() - { - $user = $this->request->user ?? null; - if (!$user || $user['role'] !== 'superadmin') { - return error('无权限访问', 403); - } - - $row = Db::name('system_config')->where('key', 'poster_config')->where('enterprise_id', 0)->find(); - $raw = $row['value'] ?? null; - $poster = self::decodeJsonSafe($raw) ?: [ - 'bgColor' => '#ffffff', - 'bgImage' => '', - 'elements' => [] - ]; - return success(['poster' => $poster]); - } - - /** - * 保存海报配置 - * PUT /api/v1/superadmin/settings/poster - */ - public function updatePosterConfig() - { - $user = $this->request->user ?? null; - if (!$user || $user['role'] !== 'superadmin') { - return error('无权限访问', 403); - } - - $input = json_decode($this->request->getContent(), true); - if (!is_array($input)) { - $input = []; - } - $data = [ - 'bgColor' => $input['bgColor'] ?? '#ffffff', - 'bgImage' => $input['bgImage'] ?? '', - 'elements' => $input['elements'] ?? [] - ]; - $jsonValue = json_encode($data, JSON_UNESCAPED_UNICODE); - - try { - $now = time(); - $exists = Db::name('system_config')->where('key', 'poster_config')->where('enterprise_id', 0)->find(); - if ($exists) { - Db::name('system_config') - ->where('key', 'poster_config') - ->where('enterprise_id', 0) - ->update(['value' => $jsonValue, 'updatedAt' => $now]); - } else { - Db::name('system_config')->insert([ - 'key' => 'poster_config', - 'enterprise_id' => 0, - 'value' => $jsonValue, - 'description' => '分销海报可视化配置(全局)', - 'createdAt' => $now, - 'updatedAt' => $now, - ]); - } - return success(null, '海报配置已保存'); - } catch (\Exception $e) { - return error('保存失败:' . $e->getMessage(), 500); - } - } - - /** - * 安全解码 JSON(处理可能的多重编码) - */ - private static function decodeJsonSafe($raw): ?array - { - if (!$raw) return null; - $val = $raw; - for ($i = 0; $i < 5 && is_string($val); $i++) { - $decoded = json_decode($val, true); - if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) break; - $val = $decoded; - } - return is_array($val) ? $val : null; - } - - /** - * 更新站点信息 - * 当系统配置中的siteName或siteDescription修改时,同步更新站点信息 - */ - private function updateSiteInfo($systemData) - { - try { - $siteConfig = SystemConfigModel::where('key', 'site_info')->where('enterprise_id', 0)->find(); - $siteInfo = [ - 'siteName' => $systemData['siteName'] ?? '', - 'siteDescription' => $systemData['siteDescription'] ?? '', - 'miniprogramName' => $systemData['miniprogramName'] ?? '', - 'updatedAt' => time() - ]; - if (!$siteConfig) { - $siteConfig = new SystemConfigModel(); - $siteConfig->key = 'site_info'; - $siteConfig->enterprise_id = 0; - $siteConfig->description = '站点信息配置'; - } - $siteConfig->value = $siteInfo; - $siteConfig->save(); - - // 也可以更新其他相关的配置或缓存 - // 例如:清除缓存、更新.env文件等 - - } catch (\Exception $e) { - // 站点信息更新失败不影响系统配置保存 - \think\facade\Log::error('更新站点信息失败:' . $e->getMessage()); - } - } -} - +request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + try { + // 获取系统配置(全局 enterprise_id=0) + $systemConfig = SystemConfigModel::where('key', 'system')->where('enterprise_id', 0)->find(); + $notificationConfig = SystemConfigModel::where('key', 'notification')->where('enterprise_id', 0)->find(); + $promptsConfig = SystemConfigModel::where('key', 'prompts')->where('enterprise_id', 0)->find(); + $reportRequiresPaymentConfig = SystemConfigModel::where('key', 'report_requires_payment')->where('enterprise_id', 0)->find(); + $textConfigModel = SystemConfigModel::where('key', 'text_config')->where('enterprise_id', 0)->find(); + $reviewModeConfig = SystemConfigModel::where('key', 'review_mode')->where('enterprise_id', 0)->find(); + + // 获取当前超管用户名(直接使用JWT中的username) + $jwtUsername = $user['username'] ?? null; + $username = 'admin'; + + if ($jwtUsername) { + $currentUser = UserModel::where('username', $jwtUsername) + ->where('role', 'superadmin') + ->find(); + if ($currentUser) { + $username = $currentUser->username; + } else { + // 如果找不到用户,使用JWT中的username + $username = $jwtUsername; + } + } + + return success([ + 'system' => $systemConfig ? $systemConfig->value : [ + 'siteName' => '神仙团队AI性格测试', + 'siteDescription' => '专业的AI性格测试平台', + 'miniprogramName' => '神仙团队AI性格测试', + 'maintenanceMode' => false, + 'maxTestsPerDay' => 100, + 'trialTestCount' => 10 + ], + 'reviewMode' => $reviewModeConfig && !empty($reviewModeConfig->value) ? $reviewModeConfig->value : ['enabled' => false], + 'notification' => $notificationConfig ? $notificationConfig->value : [ + 'emailNotification' => true, + 'lowBalanceAlert' => true, + 'lowBalanceThreshold' => 1000, + 'newEnterpriseNotify' => true + ], + 'prompts' => $promptsConfig && !empty($promptsConfig->value) ? $promptsConfig->value : [ + 'faceAnalyze' => '{"mbti":"四字母如INTJ","pdp":"老虎/孔雀/考拉/猫头鹰/变色龙其一","disc":"D/I/S/C其一","overview":"一段50字以内的综合描述","faceAnalysis":"面相特点简短描述"}', + 'reportSummary' => '' + ], + 'reportRequiresPayment' => $reportRequiresPaymentConfig && !empty($reportRequiresPaymentConfig->value) ? $reportRequiresPaymentConfig->value : ['face' => 1, 'mbti' => 0, 'disc' => 0, 'pdp' => 0], + 'textConfig' => $textConfigModel && !empty($textConfigModel->value) ? $textConfigModel->value : [ + 'analyzingTitle' => '正在分析中', + 'startButtonText' => '开始面相测试', + 'startButtonEnterprise' => '开始面部测试', + 'reportTitle' => '分析报告', + 'aiAnalysisText' => '智能分析' + ], + 'username' => $username + ]); + } catch (\Exception $e) { + return error('获取配置失败:' . $e->getMessage(), 500); + } + } + + /** + * 更新系统配置 + * @return \think\response\Json + */ + public function updateSystem() + { + // 验证是否为超级管理员 + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + // 前端 axios 发 JSON body,用 getContent 解析更可靠 + $input = json_decode($this->request->getContent(), true); + if (!is_array($input)) { + $input = []; + } + + $allowedKeys = ['siteName', 'siteDescription', 'miniprogramName', 'maintenanceMode', 'maxTestsPerDay', 'trialTestCount']; + $data = array_intersect_key($input, array_flip($allowedKeys)); + // 兼容 fallback:JSON 解析失败时尝试 Request::only + if (empty($data)) { + $data = Request::only($allowedKeys); + } + $textConfig = $input['textConfig'] ?? (Request::param('textConfig') ?: []); + + try { + // 查找或创建全局配置(enterprise_id=0) + $config = SystemConfigModel::where('key', 'system')->where('enterprise_id', 0)->find(); + if (!$config) { + $config = new SystemConfigModel(); + $config->key = 'system'; + $config->enterprise_id = 0; + $config->description = '系统基础配置'; + } + $config->value = $data; + $config->save(); + + // 更新站点信息 + $this->updateSiteInfo($data); + + // 保存全局小程序文案配置(enterprise_id=0) + if (is_array($textConfig)) { + $tcKeys = ['analyzingTitle', 'startButtonText', 'startButtonEnterprise', 'reportTitle', 'aiAnalysisText']; + $tcData = array_intersect_key($textConfig, array_flip($tcKeys)); + $tcDefaults = ['analyzingTitle' => '正在分析中', 'startButtonText' => '开始面相测试', 'startButtonEnterprise' => '开始面部测试', 'reportTitle' => '分析报告', 'aiAnalysisText' => '智能分析']; + $tcConfig = SystemConfigModel::where('key', 'text_config')->where('enterprise_id', 0)->find(); + if (!$tcConfig) { + $tcConfig = new SystemConfigModel(); + $tcConfig->key = 'text_config'; + $tcConfig->enterprise_id = 0; + $tcConfig->description = '小程序文案配置(全局)'; + } + $tcConfig->value = array_merge($tcDefaults, $tcData); + $tcConfig->save(); + } + + return success($config->value, '系统配置已保存'); + } catch (\Exception $e) { + return error('保存失败:' . $e->getMessage(), 500); + } + } + + /** + * 更新「报告需付费」配置:哪些测试类型需付费后才显示完整报告 + * PUT body: { "face": 1, "mbti": 0, "disc": 0, "pdp": 0 }(1=需付费解锁完整,0=免费完整) + * @return \think\response\Json + */ + public function updateReportRequiresPayment() + { + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + $data = Request::param('reportRequiresPayment', Request::param('data', [])); + if (!is_array($data)) { + return error('配置格式错误', 400); + } + + $defaults = ['face' => 1, 'mbti' => 0, 'disc' => 0, 'pdp' => 0]; + $value = array_merge($defaults, array_intersect_key($data, array_flip(['face', 'mbti', 'disc', 'pdp']))); + $value = array_map(function ($v) { return (int) $v ? 1 : 0; }, $value); + + try { + $config = SystemConfigModel::where('key', 'report_requires_payment')->where('enterprise_id', 0)->find(); + if (!$config) { + $config = new SystemConfigModel(); + $config->key = 'report_requires_payment'; + $config->enterprise_id = 0; + $config->description = '哪些测试类型需付费后才显示完整报告:1需付费0免费'; + } + $config->value = $value; + $config->save(); + return success($config->value, '报告付费开关已保存'); + } catch (\Exception $e) { + return error('保存失败:' . $e->getMessage(), 500); + } + } + + /** + * 更新提示词配置 + * @return \think\response\Json + */ + public function updatePrompts() + { + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + $data = Request::param('prompts', []); + if (!is_array($data)) { + return error('提示词配置格式错误', 400); + } + + try { + $config = SystemConfigModel::where('key', 'prompts')->where('enterprise_id', 0)->find(); + if (!$config) { + $config = new SystemConfigModel(); + $config->key = 'prompts'; + $config->enterprise_id = 0; + $config->description = '系统提示词配置(如面相分析、企业简历等)'; + } + $config->value = $data; + $config->save(); + return success($config->value, '提示词配置已保存'); + } catch (\Exception $e) { + return error('保存失败:' . $e->getMessage(), 500); + } + } + + /** + * 更新通知配置 + * @return \think\response\Json + */ + public function updateNotification() + { + // 验证是否为超级管理员 + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + $data = Request::only([ + 'emailNotification', 'lowBalanceAlert', + 'lowBalanceThreshold', 'newEnterpriseNotify' + ]); + + try { + $config = SystemConfigModel::where('key', 'notification')->where('enterprise_id', 0)->find(); + if (!$config) { + $config = new SystemConfigModel(); + $config->key = 'notification'; + $config->enterprise_id = 0; + $config->description = '通知与告警配置'; + } + $config->value = $data; + $config->save(); + + return success($config->value, '通知配置已保存'); + } catch (\Exception $e) { + return error('保存失败:' . $e->getMessage(), 500); + } + } + + /** + * 更新超管账户信息 + * @return \think\response\Json + */ + public function updateCredentials() + { + // 验证是否为超级管理员 + $user = $this->request->user ?? null; + if (!$user || ($user['role'] ?? '') !== 'superadmin') { + return error('无权限访问', 403); + } + + // 兼容 axios JSON PUT 与表单提交 + $rawBody = $this->request->getContent(); + if (empty($rawBody)) { + $rawBody = file_get_contents('php://input'); + } + $input = $rawBody ? json_decode($rawBody, true) : null; + if (!is_array($input)) { + $input = []; + } + + $username = trim((string)($input['username'] ?? Request::param('username', ''))); + $currentPassword = (string)($input['currentPassword'] ?? Request::param('currentPassword', '')); + $newPassword = (string)($input['newPassword'] ?? Request::param('newPassword', '')); + $confirmPassword = (string)($input['confirmPassword'] ?? Request::param('confirmPassword', '')); + + if (empty($username)) { + return error('用户名不能为空', 400); + } + + try { + // 优先使用JWT中的username来查找用户(最可靠的方式) + $jwtUsername = $user['username'] ?? null; + + if (empty($jwtUsername)) { + \think\facade\Log::error('JWT中缺少username', [ + 'user' => $user, + 'requestUserId' => $this->request->userId ?? null + ]); + return error('无法获取用户信息,请重新登录', 400); + } + + // 直接通过username查找用户 + $userModel = UserModel::where('username', $jwtUsername) + ->where('role', 'superadmin') + ->find(); + + if (!$userModel) { + // 添加调试信息 + \think\facade\Log::error('用户不存在', [ + 'jwtUsername' => $jwtUsername, + 'user' => $user, + 'requestUserId' => $this->request->userId ?? null, + 'requestUsername' => $username + ]); + return error('用户不存在,请检查登录状态', 404); + } + + // 验证当前用户是否为超级管理员(双重验证) + if ($userModel->role !== 'superadmin') { + \think\facade\Log::error('用户角色不正确', [ + 'userId' => $userModel->id, + 'role' => $userModel->role + ]); + return error('无权限修改此账户', 403); + } + + // 如果要修改密码,需要验证当前密码 + if (!empty($newPassword)) { + if (empty($currentPassword)) { + return error('请输入当前密码', 400); + } + + if ($newPassword !== $confirmPassword) { + return error('两次输入的密码不一致', 400); + } + + // 验证当前密码(User 模型中 password 字段已是加密值) + if (!password_verify($currentPassword, $userModel->password)) { + return error('当前密码错误', 400); + } + + // 更新密码:传入明文,交由 User 模型的 setPasswordAttr 自动加密 + $userModel->password = $newPassword; + } + + // 更新用户名 + if ($username !== $userModel->username) { + // 检查用户名是否已存在(排除当前用户) + $exists = UserModel::where('username', $username) + ->where('id', '<>', $userModel->id) + ->find(); + + if ($exists) { + return error('用户名已存在', 400); + } + + $userModel->username = $username; + } + + $userModel->save(); + + return success([ + 'username' => $userModel->username + ], '账户信息已更新'); + } catch (\Exception $e) { + return error('更新失败:' . $e->getMessage(), 500); + } + } + + /** + * 更新审核模式配置 + * PUT body: { "enabled": true/false } + * 开启后小程序隐藏AI面相分析功能,仅展示问卷测试,用于通过微信审核 + */ + public function updateReviewMode() + { + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + $input = json_decode($this->request->getContent(), true); + if (!is_array($input)) { + $input = Request::param(); + } + $enabled = !empty($input['enabled']); + + try { + $config = SystemConfigModel::where('key', 'review_mode')->where('enterprise_id', 0)->find(); + if (!$config) { + $config = new SystemConfigModel(); + $config->key = 'review_mode'; + $config->enterprise_id = 0; + $config->description = '审核模式:开启后隐藏AI功能以通过微信审核'; + } + $config->value = ['enabled' => $enabled]; + $config->save(); + return success($config->value, '审核模式已' . ($enabled ? '开启' : '关闭')); + } catch (\Exception $e) { + return error('保存失败:' . $e->getMessage(), 500); + } + } + + /** + * 获取可用字体列表 + * GET /api/v1/superadmin/settings/fonts + */ + public function getFonts() + { + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + $fonts = \app\common\service\PosterService::getAvailableFonts(); + return success([ + 'fonts' => $fonts, + 'fontDir' => root_path() . 'public/fonts/', + 'dirExist' => is_dir(root_path() . 'public/fonts/'), + ]); + } + + /** + * 获取海报配置 + * GET /api/v1/superadmin/settings/poster + */ + public function getPosterConfig() + { + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + $row = Db::name('system_config')->where('key', 'poster_config')->where('enterprise_id', 0)->find(); + $raw = $row['value'] ?? null; + $poster = self::decodeJsonSafe($raw) ?: [ + 'bgColor' => '#ffffff', + 'bgImage' => '', + 'elements' => [] + ]; + return success(['poster' => $poster]); + } + + /** + * 保存海报配置 + * PUT /api/v1/superadmin/settings/poster + */ + public function updatePosterConfig() + { + $user = $this->request->user ?? null; + if (!$user || $user['role'] !== 'superadmin') { + return error('无权限访问', 403); + } + + $input = json_decode($this->request->getContent(), true); + if (!is_array($input)) { + $input = []; + } + $data = [ + 'bgColor' => $input['bgColor'] ?? '#ffffff', + 'bgImage' => $input['bgImage'] ?? '', + 'elements' => $input['elements'] ?? [] + ]; + $jsonValue = json_encode($data, JSON_UNESCAPED_UNICODE); + + try { + $now = time(); + $exists = Db::name('system_config')->where('key', 'poster_config')->where('enterprise_id', 0)->find(); + if ($exists) { + Db::name('system_config') + ->where('key', 'poster_config') + ->where('enterprise_id', 0) + ->update(['value' => $jsonValue, 'updatedAt' => $now]); + } else { + Db::name('system_config')->insert([ + 'key' => 'poster_config', + 'enterprise_id' => 0, + 'value' => $jsonValue, + 'description' => '分销海报可视化配置(全局)', + 'createdAt' => $now, + 'updatedAt' => $now, + ]); + } + return success(null, '海报配置已保存'); + } catch (\Exception $e) { + return error('保存失败:' . $e->getMessage(), 500); + } + } + + /** + * 安全解码 JSON(处理可能的多重编码) + */ + private static function decodeJsonSafe($raw): ?array + { + if (!$raw) return null; + $val = $raw; + for ($i = 0; $i < 5 && is_string($val); $i++) { + $decoded = json_decode($val, true); + if ($decoded === null && json_last_error() !== JSON_ERROR_NONE) break; + $val = $decoded; + } + return is_array($val) ? $val : null; + } + + /** + * 更新站点信息 + * 当系统配置中的siteName或siteDescription修改时,同步更新站点信息 + */ + private function updateSiteInfo($systemData) + { + try { + $siteConfig = SystemConfigModel::where('key', 'site_info')->where('enterprise_id', 0)->find(); + $siteInfo = [ + 'siteName' => $systemData['siteName'] ?? '', + 'siteDescription' => $systemData['siteDescription'] ?? '', + 'miniprogramName' => $systemData['miniprogramName'] ?? '', + 'updatedAt' => time() + ]; + if (!$siteConfig) { + $siteConfig = new SystemConfigModel(); + $siteConfig->key = 'site_info'; + $siteConfig->enterprise_id = 0; + $siteConfig->description = '站点信息配置'; + } + $siteConfig->value = $siteInfo; + $siteConfig->save(); + + // 也可以更新其他相关的配置或缓存 + // 例如:清除缓存、更新.env文件等 + + } catch (\Exception $e) { + // 站点信息更新失败不影响系统配置保存 + \think\facade\Log::error('更新站点信息失败:' . $e->getMessage()); + } + } +} + diff --git a/api/route/api.php b/api/route/api.php index f79bb76..b976b9e 100644 --- a/api/route/api.php +++ b/api/route/api.php @@ -1,382 +1,383 @@ -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']); +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']); diff --git a/miniprogram/app.js b/miniprogram/app.js index dd2a118..9791866 100644 --- a/miniprogram/app.js +++ b/miniprogram/app.js @@ -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 解析出的企业ID(e_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} 是否登录成功(拿到 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} 当前是否有有效登录态 - */ - 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 解析出的企业ID(e_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} 是否登录成功(拿到 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} 当前是否有有效登录态 + */ + 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 + this.globalData.reviewMode = !!data.reviewMode + resolve(data) + } else { + reject(new Error(res.data && res.data.message ? res.data.message : '获取配置失败')) + } + }, + fail: reject + }) + }) + } +}) diff --git a/miniprogram/app.json b/miniprogram/app.json index 6528509..23195ab 100644 --- a/miniprogram/app.json +++ b/miniprogram/app.json @@ -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" +} diff --git a/miniprogram/custom-tab-bar/index.js b/miniprogram/custom-tab-bar/index.js index a11fed6..81515df 100644 --- a/miniprogram/custom-tab-bar/index.js +++ b/miniprogram/custom-tab-bar/index.js @@ -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 }) + } + } +}) diff --git a/miniprogram/custom-tab-bar/index.wxml b/miniprogram/custom-tab-bar/index.wxml index f0ca7b2..cb4f441 100644 --- a/miniprogram/custom-tab-bar/index.wxml +++ b/miniprogram/custom-tab-bar/index.wxml @@ -1,35 +1,48 @@ - - - - - - - 首页 - - - - - - - - - - 我的 - - - + + + + + + + 首页 + + + + + 测试 + + + + + + + + + + + 我的 + + + diff --git a/miniprogram/pages/history/index.js b/miniprogram/pages/history/index.js index 5d780da..45a91b5 100644 --- a/miniprogram/pages/history/index.js +++ b/miniprogram/pages/history/index.js @@ -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' }) + }, + +}) diff --git a/miniprogram/pages/history/index.wxml b/miniprogram/pages/history/index.wxml index 95f5d7a..9c52e16 100644 --- a/miniprogram/pages/history/index.wxml +++ b/miniprogram/pages/history/index.wxml @@ -1,74 +1,74 @@ - - - - - - - - 全部 - - - MBTI - - - PDP - - - DISC - - - 面相 - - - 简历 - - - - - - - - 📋 - 暂无{{activeTab === 'all' ? '' : tabName}}历史记录 - - 开始测试 - - - - - - - - - {{item.emoji}} - - - - {{item.typeName}} - - {{item.resultText}} - - - - {{item.testTime}} - {{item.enterpriseName}} - - - - - - - - 加载中... - - - - 共 {{total}} 条 - - - - - - 加载中... - - + + + + + + + + 全部 + + + MBTI + + + PDP + + + DISC + + + 面相 + + + 简历 + + + + + + + + 📋 + 暂无{{activeTab === 'all' ? '' : tabName}}历史记录 + + 开始测试 + + + + + + + + + {{item.emoji}} + + + + {{item.typeName}} + + {{item.resultText}} + + + + {{item.testTime}} + {{item.enterpriseName}} + + + + + + + + 加载中... + + + + 共 {{total}} 条 + + + + + + 加载中... + + diff --git a/miniprogram/pages/index/camera.js b/miniprogram/pages/index/camera.js index a585250..5e86c89 100644 --- a/miniprogram/pages/index/camera.js +++ b/miniprogram/pages/index/camera.js @@ -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() + } + } + }) + } +}) diff --git a/miniprogram/pages/index/index.js b/miniprogram/pages/index/index.js index b0bde2c..42abf16 100644 --- a/miniprogram/pages/index/index.js +++ b/miniprogram/pages/index/index.js @@ -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() + } + } +}) diff --git a/miniprogram/pages/index/index.wxml b/miniprogram/pages/index/index.wxml index f3ded42..d44d7a5 100644 --- a/miniprogram/pages/index/index.wxml +++ b/miniprogram/pages/index/index.wxml @@ -1,61 +1,85 @@ - - - - - - - 🏢 - 企业版 - - {{siteTitle || '神仙团队AI性格测试'}} - - - - - - - - - - - - - - 面相分析 - 骨相分析 - 性格测评 - - - - - 测试流程 - - - 1 - STEP1 - 拍摄照片 - - - - 2 - STEP2 - {{aiAnalysisText || '智能分析'}} - - - - 3 - STEP3 - 生成报告 - - - - - - {{startButtonText || '开始面相测试'}} - - - 人工智能生成 - - - - + + + + + + + 🏢 + 企业版 + + {{siteTitle || '神仙团队性格测试'}} + + + + + + + + + + + + + + + 面相分析 + 骨相分析 + 性格测评 + MBTI + DISC + + + + + 测试流程 + + + 1 + STEP1 + 拍摄照片 + + + + 2 + STEP2 + {{aiAnalysisText || '智能分析'}} + + + + 3 + STEP3 + 生成报告 + + + + + + 1 + STEP1 + 选择测试 + + + + 2 + STEP2 + 回答问题 + + + + 3 + STEP3 + 查看结果 + + + + + + {{startButtonText || '开始性格测试'}} + + + + 人工智能生成 + + + + diff --git a/miniprogram/pages/index/result.js b/miniprogram/pages/index/result.js index eddbe38..eb6732b 100644 --- a/miniprogram/pages/index/result.js +++ b/miniprogram/pages/index/result.js @@ -1,539 +1,545 @@ -// pages/index/result.js - AI分析结果页(按旧版模板重构) -const app = getApp() -const payment = require('../../utils/payment') -const { hasPhone, bindPhoneByCode, ensureProfileCompleteAndRedirect } = require('../../utils/phoneAuth.js') - -Page({ - data: { - isAnalyzing: true, - showResult: false, - hasError: false, - errorMessage: '', - noFaceError: false, - noFaceMessage: '', - progress: 0, - analyzingTip: '正在识别面部特征...', - activeTab: 'face', // 'face' | 'bone' - // 报告付费信息 - payInfo: { - requiresPayment: false, // 是否需要付费才解锁完整报告 - isPaid: false, // 当前这次是否已解锁 - amountYuan: 0 // 人脸报告价格(元) - }, - result: { - mbti: '', - title: '', - summary: '', - pdp: '', - pdpAux: '', - pdpEmoji: '', - disc: '', - discAux: '', - traits: [], - faceAnalysisText: '', - boneAnalysisText: '', - careers: [], - relationship: '', - gallupTop3: [], - // 完整面相/骨相(object 才用,字符串走 *Text 字段) - faceAnalysis: null, - boneAnalysis: null, - // 企业版:职业画像、HR视角、老板视角、简历亮点 - portrait: null, - hrView: null, - bossView: null, - resumeHighlights: '' - }, - // 当前这次AI分析对应的测试记录ID(mbti_test_results.id) - testResultId: null, - // 支付后是否已经触发过一次“刷新完整报告”,避免重复刷新 - hasReloadedAfterPay: false, - // 是否已在本地拥有手机号(决定是否还需要弹出微信手机号授权) - hasPhone: false, - analyzingTitle: '正在分析中', - reportTitle: '分析报告', - aiAnalysisText: '智能分析' - }, - - onLoad(options) { - const id = options && options.id - const type = options && options.type - - // 加载文案配置(分析中提示、报告标题等) - const tc = app.globalData.textConfig - if (tc) { - this.setData({ - analyzingTitle: tc.analyzingTitle || '正在分析中', - reportTitle: tc.reportTitle || '分析报告', - aiAnalysisText: tc.aiAnalysisText || '智能分析' - }) - if (tc.reportTitle) wx.setNavigationBarTitle({ title: tc.reportTitle }) - } else { - app.getRuntimeConfig().then((cfg) => { - if (cfg && cfg.textConfig) { - app.globalData.textConfig = cfg.textConfig - const t = cfg.textConfig - this.setData({ - analyzingTitle: t.analyzingTitle || '正在分析中', - reportTitle: t.reportTitle || '分析报告', - aiAnalysisText: t.aiAnalysisText || '智能分析' - }) - if (t.reportTitle) wx.setNavigationBarTitle({ title: t.reportTitle }) - } - }).catch(() => {}) - } - - // 从历史记录进入:根据ID从后端读取数据库中的结果 - if (id && type === 'ai') { - const token = app.globalData.token || wx.getStorageSync('token') - const apiBase = app.globalData.apiBase - if (!token || !apiBase) { - wx.showToast({ title: '未登录,无法读取历史记录', icon: 'none' }) - setTimeout(() => wx.navigateBack(), 1500) - return - } - - wx.showLoading({ title: '加载历史记录...' }) - wx.request({ - url: `${apiBase}/api/test/detail`, - method: 'GET', - header: { - 'Authorization': `Bearer ${token}` - }, - data: { id }, - success: (res) => { - wx.hideLoading() - if (res.statusCode === 200 && res.data && res.data.data) { - const payload = res.data.data - const apiData = payload.data || payload - // 历史详情场景下,记录当前测试记录ID - this.setData({ testResultId: id }) - this.processResult(apiData) - // 从历史进入:金额与是否需付款以 test_results 的 paidAmount/needPaymentToUnlock 为准 - this.initPayInfoFromRuntime( - !!payload.requiresPayment, - !!payload.isPaid, - payload - ) - } else { - wx.showToast({ title: res.data?.message || '加载失败', icon: 'none' }) - setTimeout(() => wx.navigateBack(), 1500) - } - }, - fail: () => { - wx.hideLoading() - wx.showToast({ title: '网络错误,加载失败', icon: 'none' }) - setTimeout(() => wx.navigateBack(), 1500) - } - }) - return - } - - // 正常从拍照流程进入:调用 /api/analyze - this.startAnalysis() - }, - - onShow() { - if (!ensureProfileCompleteAndRedirect()) return - this.setData({ hasPhone: hasPhone() }) - const tc = app.globalData.textConfig - if (tc) { - this.setData({ - analyzingTitle: tc.analyzingTitle || '正在分析中', - reportTitle: tc.reportTitle || '分析报告', - aiAnalysisText: tc.aiAnalysisText || '智能分析' - }) - } - }, - - // 面相/骨相 Tab 切换 - switchAnalysisTab(e) { - this.setData({ activeTab: e.currentTarget.dataset.tab }) - }, - - // 调用后端AI分析API(aiPhotos 应为上传后的 URL 数组,由拍照页上传后写入) - startAnalysis() { - let photos = wx.getStorageSync('aiPhotos') || [] - if (!Array.isArray(photos)) photos = [] - const isUrls = photos.length > 0 && photos.every(p => typeof p === 'string' && (p.startsWith('http://') || p.startsWith('https://'))) - if (!isUrls || photos.length === 0) { - this.setData({ progress: 100 }) - setTimeout(() => { - wx.showToast({ title: '请先完成拍照并上传', icon: 'none' }) - wx.navigateBack({ delta: 1 }) - }, 300) - return - } - const tips = [ - '正在识别面部特征...', - '分析眉眼特征...', - '结合《冰鉴》分析骨形...', - '匹配MBTI/PDP/DISC...', - '生成综合报告...' - ] - let progress = 0 - let tipIndex = 0 - - // 进度动画 - const timer = setInterval(() => { - progress += 3 - if (progress > 95) progress = 95 - if (progress > (tipIndex + 1) * 18 && tipIndex < tips.length - 1) tipIndex++ - this.setData({ progress: Math.floor(progress), analyzingTip: tips[tipIndex] }) - }, 200) - - // 调用后端API:appScope='enterprise' 时才传 enterpriseId,个人版不传 - const userInfo = app.globalData.userInfo || wx.getStorageSync('userInfo') || {} - const scope = (app.globalData && app.globalData.appScope) || 'personal' - const enterpriseId = scope === 'enterprise' - ? (app.globalData.enterpriseIdFromScene || userInfo.enterpriseId || null) - : null - wx.request({ - url: `${app.globalData.apiBase}/api/analyze`, - method: 'POST', - header: { - 'Content-Type': 'application/json', - 'Authorization': `Bearer ${wx.getStorageSync('token') || ''}` - }, - data: { - photoUrls: photos, - userId: app.globalData.openId || '', - ...(enterpriseId ? { enterpriseId: Number(enterpriseId) } : {}) - }, - success: (res) => { - clearInterval(timer) - - if (res.statusCode === 200 && res.data) { - const bodyCode = res.data.code - // 后端 error() 始终返回 HTTP 200,业务错误码放在 data.code 中 - if (bodyCode === 422) { - // AI 未检测到人脸 - const msg = res.data.message || '图片中未检测到人脸,请确保拍摄时面部清晰可见' - this.setData({ progress: 100 }) - setTimeout(() => this.showNoFaceError(msg), 300) - } else if (bodyCode === 200) { - const apiData = res.data.data || res.data - this.setData({ progress: 100, analyzingTip: '分析完成!' }) - setTimeout(() => { - this.processResult(apiData) - }, 500) - } else { - console.error('API返回错误:', res) - this.setData({ progress: 100 }) - const msg = (res.data && (res.data.message || res.data.msg)) || '分析失败,请稍后重试' - setTimeout(() => this.showAnalyzeError(msg), 300) - } - } else { - console.error('API返回错误:', res) - this.setData({ progress: 100 }) - setTimeout(() => this.showAnalyzeError('分析失败,请稍后重试'), 300) - } - }, - fail: (err) => { - clearInterval(timer) - console.error('API调用失败:', err) - this.setData({ progress: 100 }) - setTimeout(() => this.showAnalyzeError('网络异常,请检查网络后重试'), 300) - } - }) - }, - - // 处理API返回结果 - processResult(apiData) { - const result = { - mbti: apiData.mbti?.type || '', - title: apiData.mbti?.title || '', - summary: apiData.personalitySummary || apiData.overview || '', - pdp: apiData.pdp?.primary || '', - pdpAux: apiData.pdp?.secondary || '', - pdpEmoji: this.getPDPEmoji(apiData.pdp?.primary), - disc: apiData.disc?.primary || '', - discAux: apiData.disc?.secondary || '', - traits: Array.isArray(apiData.advantages) ? apiData.advantages : [], - faceAnalysisText: typeof apiData.faceAnalysis === 'string' ? apiData.faceAnalysis : '', - boneAnalysisText: typeof apiData.boneAnalysis === 'string' ? apiData.boneAnalysis : '', - careers: Array.isArray(apiData.careers) ? apiData.careers : [], - relationship: apiData.relationship || '', - gallupTop3: Array.isArray(apiData.gallupTop3) ? apiData.gallupTop3 : [], - faceAnalysis: null, - boneAnalysis: null, - portrait: apiData.portrait || null, - hrView: apiData.hrView || null, - bossView: apiData.bossView || null, - resumeHighlights: apiData.resumeHighlights || '', - timestamp: Date.now() - } - - wx.setStorageSync('aiResult', result) - if (app.globalData) app.globalData.aiResult = result - // 测试记录已由 /api/analyze 顺带写入,无需再调 /api/test/submit - - const updates = { - isAnalyzing: false, - showResult: true, - hasError: false, - result - } - - // 记录本次测试记录ID(由 /api/analyze 返回) - if (apiData && apiData._testResultId) { - updates.testResultId = apiData._testResultId - } - - this.setData(updates) - - // 优先使用后端 /api/analyze 返回的价格信息,避免二次请求 - if (apiData._payment) { - const p = apiData._payment || {} - this.setData({ - payInfo: { - requiresPayment: !!p.requiresPayment, - isPaid: false, - amountYuan: typeof p.amountYuan === 'number' - ? p.amountYuan - : (p.amountFen ? (p.amountFen / 100) : 0) - } - }) - } - // 无 _payment 时(如从历史详情进入)不在这里调 initPayInfoFromRuntime,避免异步 getRuntimeConfig 后覆盖详情接口返回的 needPaymentToUnlock;由调用方用 detail 的 payload 单独设置 payInfo - }, - - - // PDP类型对应emoji - getPDPEmoji(type) { - const map = { - '老虎': '🐅', '孔雀': '🦚', '无尾熊': '🐨', '考拉': '🐨', - '猫头鹰': '🦉', '变色龙': '🦎' - } - return map[type] || '🐅' - }, - - /** - * 初始化付费信息:结合运行配置 + 可选的数据库记录状态 - * @param {boolean|null} recordRequires 是否从记录中读到需要付费(可为空) - * @param {boolean} recordIsPaid 记录是否已付费 - */ - initPayInfoFromRuntime(recordRequires = null, recordIsPaid = false, detailPayload = null) { - // 从历史/详情进入:优先用 test_results 的 paidAmount,需付款但金额为0 则直接可查看 - if (detailPayload && (detailPayload.paidAmount != null || detailPayload.amountYuan != null || detailPayload.needPaymentToUnlock != null)) { - const paidAmount = Number(detailPayload.paidAmount ?? 0) - const amountYuan = detailPayload.amountYuan != null ? Number(detailPayload.amountYuan) : (paidAmount > 0 ? paidAmount / 100 : 0) - const needPay = detailPayload.needPaymentToUnlock === true || (!!detailPayload.requiresPayment && !detailPayload.isPaid && paidAmount > 0) - this.setData({ - payInfo: { - requiresPayment: needPay, - isPaid: !!detailPayload.isPaid, - amountYuan: needPay ? amountYuan : 0 - } - }) - return - } - app.getRuntimeConfig() - .then((cfg) => { - const pricing = cfg.pricing || {} - const reportRequires = cfg.reportRequiresPayment || {} - const facePriceRaw = pricing.face - const facePrice = typeof facePriceRaw === 'number' - ? facePriceRaw - : Number(facePriceRaw || 0) - - const requiresByConfig = !!(reportRequires && reportRequires.face) - const requiresPayment = - typeof recordRequires === 'boolean' ? recordRequires : requiresByConfig - // 系统设置需付款但金额为0 则直接可查看,不展示付费墙 - const needPay = requiresPayment && facePrice > 0 - const amountYuan = facePrice > 0 ? facePrice : 0 - - this.setData({ - payInfo: { - requiresPayment: needPay, - isPaid: !!recordIsPaid, - amountYuan - } - }) - }) - .catch(() => { - this.setData({ - payInfo: { - requiresPayment: false, - isPaid: !!recordIsPaid, - amountYuan: 0 - } - }) - }) - }, - - // 解锁完整报告:发起人脸测试付费 - unlockFullReport() { - const { payInfo, testResultId, hasReloadedAfterPay } = this.data - if (!payInfo.requiresPayment || payInfo.isPaid) { - return - } - - app.ensureLogin().then((logged) => { - if (!logged) { - wx.showToast({ title: '请先登录后再解锁', icon: 'none' }) - return - } - payment.purchaseFaceTest({ - testResultId, - success: () => { - wx.showToast({ title: '已解锁完整报告', icon: 'success' }) - - // 本地先标记已付费,避免按钮仍然提示“需要解锁” - this.setData({ - 'payInfo.isPaid': true - }) - - // 避免重复触发刷新:只在还没刷新的情况下,延迟 0.5s 拉一次详情 - if (testResultId && !hasReloadedAfterPay) { - this.setData({ hasReloadedAfterPay: true }) - setTimeout(() => { - this.reloadFullDetail(testResultId) - }, 500) - } - }, - fail: () => { - // 支付失败或取消,这里不做额外处理 - } - }) - }) - }, - - // AI结果页付费按钮:就地触发微信手机号授权,然后调用 unlockFullReport - onGetPhoneNumberForFacePay(e) { - if (!ensureProfileCompleteAndRedirect()) return - const { code, errMsg } = e.detail || {} - if (errMsg && errMsg.indexOf('getPhoneNumber:fail') === 0) { - if (!hasPhone()) { - wx.showToast({ title: '需要授权手机号才能继续', icon: 'none' }) - return - } - this.unlockFullReport() - return - } - if (!code) { - if (hasPhone()) { - this.unlockFullReport() - } else { - wx.showToast({ title: '获取手机号失败', icon: 'none' }) - } - return - } - bindPhoneByCode(code) - .then(() => { - this.setData({ hasPhone: true }) - this.unlockFullReport() - }) - .catch(() => { - // 保持在当前页,等待用户重新点击 - }) - }, - - // 解锁成功后,根据测试记录ID重新拉取完整详情 - reloadFullDetail(id) { - if (!id) return - - const app = getApp() - const apiBase = app.globalData?.apiBase || '' - - wx.showLoading({ title: '加载完整报告...' }) - wx.request({ - url: `${apiBase}/api/test/detail`, - method: 'GET', - header: { - 'Authorization': app.globalData?.token ? `Bearer ${app.globalData.token}` : '' - }, - data: { id }, - success: (res) => { - if (res.statusCode === 200 && res.data && res.data.data) { - const payload = res.data.data - const apiData = payload.data || payload - - // 详情接口在后台会根据 isPaid 返回完整结构,这里直接复用已有处理逻辑 - this.processResult(apiData) - - // 同步数据库中的付费状态与金额(test_results.paidAmount) - const recordRequires = - typeof payload.requiresPayment === 'boolean' || typeof payload.requiresPayment === 'number' - ? !!payload.requiresPayment - : null - const recordIsPaid = !!payload.isPaid - this.initPayInfoFromRuntime(recordRequires, recordIsPaid, payload) - } else { - console.error('刷新完整报告失败', res) - } - }, - fail: (err) => { - console.error('请求完整报告失败', err) - }, - complete: () => { - wx.hideLoading() - } - }) - }, - - // 无人脸错误:显示提示并提供重新拍摄入口 - showNoFaceError(message) { - this.setData({ - isAnalyzing: false, - showResult: false, - noFaceError: true, - noFaceMessage: message || '图片中未检测到人脸,请重新拍摄清晰的正面照片' - }) - }, - - // 分析失败:不使用任何本地模拟数据 - showAnalyzeError(message) { - this.setData({ - isAnalyzing: false, - showResult: true, - hasError: true, - errorMessage: message || '分析失败,请稍后重试' - }) - }, - - // 跳转到详情性格测试选择页(MBTI / PDP / DISC 三选一) - goToMBTI() { - wx.navigateTo({ url: '/pages/test-select/index' }) - }, - - shareResult() { - wx.showShareMenu({ withShareTicket: true, menus: ['shareAppMessage', 'shareTimeline'] }) - }, - - retake() { - // camera 是 tabBar 页面,必须用 switchTab - wx.switchTab({ url: '/pages/index/camera' }) - }, - - goHome() { - // 根据当前 scope 跳对应首页 - const scope = (getApp().globalData && getApp().globalData.appScope) || 'personal' - if (scope === 'enterprise') { - wx.navigateTo({ url: '/pages/enterprise/index' }) - } else { - wx.switchTab({ url: '/pages/index/index' }) - } - }, - - onShareAppMessage() { - const r = this.data.result - const t = this.data.aiAnalysisText || '智能分析' - const { getSharePathByScope } = require('../../utils/share') - return { - title: `${t}我是${r?.mbti} ${r?.pdpEmoji}${r?.pdp}型,来测测你的!`, - path: getSharePathByScope('/pages/index/index') - } - }, - - onShareTimeline() { - const r = this.data.result - const t = this.data.aiAnalysisText || '智能分析' - const { buildShareQuery } = require('../../utils/share') - return { - title: `${t}我是${r?.mbti} ${r?.pdpEmoji}${r?.pdp}型,来测测你的!`, - query: buildShareQuery() - } - } -}) +// pages/index/result.js - AI分析结果页(按旧版模板重构) +const app = getApp() +const payment = require('../../utils/payment') +const { hasPhone, bindPhoneByCode, ensureProfileCompleteAndRedirect } = require('../../utils/phoneAuth.js') + +Page({ + data: { + isAnalyzing: true, + showResult: false, + hasError: false, + errorMessage: '', + noFaceError: false, + noFaceMessage: '', + progress: 0, + analyzingTip: '正在识别面部特征...', + activeTab: 'face', // 'face' | 'bone' + // 报告付费信息 + payInfo: { + requiresPayment: false, // 是否需要付费才解锁完整报告 + isPaid: false, // 当前这次是否已解锁 + amountYuan: 0 // 人脸报告价格(元) + }, + result: { + mbti: '', + title: '', + summary: '', + pdp: '', + pdpAux: '', + pdpEmoji: '', + disc: '', + discAux: '', + traits: [], + faceAnalysisText: '', + boneAnalysisText: '', + careers: [], + relationship: '', + gallupTop3: [], + // 完整面相/骨相(object 才用,字符串走 *Text 字段) + faceAnalysis: null, + boneAnalysis: null, + // 企业版:职业画像、HR视角、老板视角、简历亮点 + portrait: null, + hrView: null, + bossView: null, + resumeHighlights: '' + }, + // 当前这次AI分析对应的测试记录ID(mbti_test_results.id) + testResultId: null, + // 支付后是否已经触发过一次“刷新完整报告”,避免重复刷新 + hasReloadedAfterPay: false, + // 是否已在本地拥有手机号(决定是否还需要弹出微信手机号授权) + hasPhone: false, + analyzingTitle: '正在分析中', + reportTitle: '分析报告', + aiAnalysisText: '智能分析', + reviewMode: false + }, + + onLoad(options) { + const id = options && options.id + const type = options && options.type + + // 同步审核模式 + this.setData({ reviewMode: !!app.globalData.reviewMode }) + + // 加载文案配置(分析中提示、报告标题等) + const tc = app.globalData.textConfig + if (tc) { + this.setData({ + analyzingTitle: tc.analyzingTitle || '正在分析中', + reportTitle: tc.reportTitle || '分析报告', + aiAnalysisText: tc.aiAnalysisText || '智能分析' + }) + if (tc.reportTitle) wx.setNavigationBarTitle({ title: tc.reportTitle }) + } else { + app.getRuntimeConfig().then((cfg) => { + if (cfg && cfg.textConfig) { + app.globalData.textConfig = cfg.textConfig + const t = cfg.textConfig + this.setData({ + analyzingTitle: t.analyzingTitle || '正在分析中', + reportTitle: t.reportTitle || '分析报告', + aiAnalysisText: t.aiAnalysisText || '智能分析' + }) + if (t.reportTitle) wx.setNavigationBarTitle({ title: t.reportTitle }) + } + }).catch(() => {}) + } + + // 从历史记录进入:根据ID从后端读取数据库中的结果 + if (id && type === 'ai') { + const token = app.globalData.token || wx.getStorageSync('token') + const apiBase = app.globalData.apiBase + if (!token || !apiBase) { + wx.showToast({ title: '未登录,无法读取历史记录', icon: 'none' }) + setTimeout(() => wx.navigateBack(), 1500) + return + } + + wx.showLoading({ title: '加载历史记录...' }) + wx.request({ + url: `${apiBase}/api/test/detail`, + method: 'GET', + header: { + 'Authorization': `Bearer ${token}` + }, + data: { id }, + success: (res) => { + wx.hideLoading() + if (res.statusCode === 200 && res.data && res.data.data) { + const payload = res.data.data + const apiData = payload.data || payload + // 历史详情场景下,记录当前测试记录ID + this.setData({ testResultId: id }) + this.processResult(apiData) + // 从历史进入:金额与是否需付款以 test_results 的 paidAmount/needPaymentToUnlock 为准 + this.initPayInfoFromRuntime( + !!payload.requiresPayment, + !!payload.isPaid, + payload + ) + } else { + wx.showToast({ title: res.data?.message || '加载失败', icon: 'none' }) + setTimeout(() => wx.navigateBack(), 1500) + } + }, + fail: () => { + wx.hideLoading() + wx.showToast({ title: '网络错误,加载失败', icon: 'none' }) + setTimeout(() => wx.navigateBack(), 1500) + } + }) + return + } + + // 正常从拍照流程进入:调用 /api/analyze + this.startAnalysis() + }, + + onShow() { + if (!ensureProfileCompleteAndRedirect()) return + this.setData({ hasPhone: hasPhone() }) + const tc = app.globalData.textConfig + if (tc) { + this.setData({ + analyzingTitle: tc.analyzingTitle || '正在分析中', + reportTitle: tc.reportTitle || '分析报告', + aiAnalysisText: tc.aiAnalysisText || '智能分析' + }) + } + }, + + // 面相/骨相 Tab 切换 + switchAnalysisTab(e) { + this.setData({ activeTab: e.currentTarget.dataset.tab }) + }, + + // 调用后端AI分析API(aiPhotos 应为上传后的 URL 数组,由拍照页上传后写入) + startAnalysis() { + let photos = wx.getStorageSync('aiPhotos') || [] + if (!Array.isArray(photos)) photos = [] + const isUrls = photos.length > 0 && photos.every(p => typeof p === 'string' && (p.startsWith('http://') || p.startsWith('https://'))) + if (!isUrls || photos.length === 0) { + this.setData({ progress: 100 }) + setTimeout(() => { + wx.showToast({ title: '请先完成拍照并上传', icon: 'none' }) + wx.navigateBack({ delta: 1 }) + }, 300) + return + } + const tips = [ + '正在识别面部特征...', + '分析眉眼特征...', + '结合《冰鉴》分析骨形...', + '匹配MBTI/PDP/DISC...', + '生成综合报告...' + ] + let progress = 0 + let tipIndex = 0 + + // 进度动画 + const timer = setInterval(() => { + progress += 3 + if (progress > 95) progress = 95 + if (progress > (tipIndex + 1) * 18 && tipIndex < tips.length - 1) tipIndex++ + this.setData({ progress: Math.floor(progress), analyzingTip: tips[tipIndex] }) + }, 200) + + // 调用后端API:appScope='enterprise' 时才传 enterpriseId,个人版不传 + const userInfo = app.globalData.userInfo || wx.getStorageSync('userInfo') || {} + const scope = (app.globalData && app.globalData.appScope) || 'personal' + const enterpriseId = scope === 'enterprise' + ? (app.globalData.enterpriseIdFromScene || userInfo.enterpriseId || null) + : null + wx.request({ + url: `${app.globalData.apiBase}/api/analyze`, + method: 'POST', + header: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${wx.getStorageSync('token') || ''}` + }, + data: { + photoUrls: photos, + userId: app.globalData.openId || '', + ...(enterpriseId ? { enterpriseId: Number(enterpriseId) } : {}) + }, + success: (res) => { + clearInterval(timer) + + if (res.statusCode === 200 && res.data) { + const bodyCode = res.data.code + // 后端 error() 始终返回 HTTP 200,业务错误码放在 data.code 中 + if (bodyCode === 422) { + // AI 未检测到人脸 + const msg = res.data.message || '图片中未检测到人脸,请确保拍摄时面部清晰可见' + this.setData({ progress: 100 }) + setTimeout(() => this.showNoFaceError(msg), 300) + } else if (bodyCode === 200) { + const apiData = res.data.data || res.data + this.setData({ progress: 100, analyzingTip: '分析完成!' }) + setTimeout(() => { + this.processResult(apiData) + }, 500) + } else { + console.error('API返回错误:', res) + this.setData({ progress: 100 }) + const msg = (res.data && (res.data.message || res.data.msg)) || '分析失败,请稍后重试' + setTimeout(() => this.showAnalyzeError(msg), 300) + } + } else { + console.error('API返回错误:', res) + this.setData({ progress: 100 }) + setTimeout(() => this.showAnalyzeError('分析失败,请稍后重试'), 300) + } + }, + fail: (err) => { + clearInterval(timer) + console.error('API调用失败:', err) + this.setData({ progress: 100 }) + setTimeout(() => this.showAnalyzeError('网络异常,请检查网络后重试'), 300) + } + }) + }, + + // 处理API返回结果 + processResult(apiData) { + const result = { + mbti: apiData.mbti?.type || '', + title: apiData.mbti?.title || '', + summary: apiData.personalitySummary || apiData.overview || '', + pdp: apiData.pdp?.primary || '', + pdpAux: apiData.pdp?.secondary || '', + pdpEmoji: this.getPDPEmoji(apiData.pdp?.primary), + disc: apiData.disc?.primary || '', + discAux: apiData.disc?.secondary || '', + traits: Array.isArray(apiData.advantages) ? apiData.advantages : [], + faceAnalysisText: typeof apiData.faceAnalysis === 'string' ? apiData.faceAnalysis : '', + boneAnalysisText: typeof apiData.boneAnalysis === 'string' ? apiData.boneAnalysis : '', + careers: Array.isArray(apiData.careers) ? apiData.careers : [], + relationship: apiData.relationship || '', + gallupTop3: Array.isArray(apiData.gallupTop3) ? apiData.gallupTop3 : [], + faceAnalysis: null, + boneAnalysis: null, + portrait: apiData.portrait || null, + hrView: apiData.hrView || null, + bossView: apiData.bossView || null, + resumeHighlights: apiData.resumeHighlights || '', + timestamp: Date.now() + } + + wx.setStorageSync('aiResult', result) + if (app.globalData) app.globalData.aiResult = result + // 测试记录已由 /api/analyze 顺带写入,无需再调 /api/test/submit + + const updates = { + isAnalyzing: false, + showResult: true, + hasError: false, + result + } + + // 记录本次测试记录ID(由 /api/analyze 返回) + if (apiData && apiData._testResultId) { + updates.testResultId = apiData._testResultId + } + + this.setData(updates) + + // 优先使用后端 /api/analyze 返回的价格信息,避免二次请求 + if (apiData._payment) { + const p = apiData._payment || {} + this.setData({ + payInfo: { + requiresPayment: !!p.requiresPayment, + isPaid: false, + amountYuan: typeof p.amountYuan === 'number' + ? p.amountYuan + : (p.amountFen ? (p.amountFen / 100) : 0) + } + }) + } + // 无 _payment 时(如从历史详情进入)不在这里调 initPayInfoFromRuntime,避免异步 getRuntimeConfig 后覆盖详情接口返回的 needPaymentToUnlock;由调用方用 detail 的 payload 单独设置 payInfo + }, + + + // PDP类型对应emoji + getPDPEmoji(type) { + const map = { + '老虎': '🐅', '孔雀': '🦚', '无尾熊': '🐨', '考拉': '🐨', + '猫头鹰': '🦉', '变色龙': '🦎' + } + return map[type] || '🐅' + }, + + /** + * 初始化付费信息:结合运行配置 + 可选的数据库记录状态 + * @param {boolean|null} recordRequires 是否从记录中读到需要付费(可为空) + * @param {boolean} recordIsPaid 记录是否已付费 + */ + initPayInfoFromRuntime(recordRequires = null, recordIsPaid = false, detailPayload = null) { + // 从历史/详情进入:优先用 test_results 的 paidAmount,需付款但金额为0 则直接可查看 + if (detailPayload && (detailPayload.paidAmount != null || detailPayload.amountYuan != null || detailPayload.needPaymentToUnlock != null)) { + const paidAmount = Number(detailPayload.paidAmount ?? 0) + const amountYuan = detailPayload.amountYuan != null ? Number(detailPayload.amountYuan) : (paidAmount > 0 ? paidAmount / 100 : 0) + const needPay = detailPayload.needPaymentToUnlock === true || (!!detailPayload.requiresPayment && !detailPayload.isPaid && paidAmount > 0) + this.setData({ + payInfo: { + requiresPayment: needPay, + isPaid: !!detailPayload.isPaid, + amountYuan: needPay ? amountYuan : 0 + } + }) + return + } + app.getRuntimeConfig() + .then((cfg) => { + const pricing = cfg.pricing || {} + const reportRequires = cfg.reportRequiresPayment || {} + const facePriceRaw = pricing.face + const facePrice = typeof facePriceRaw === 'number' + ? facePriceRaw + : Number(facePriceRaw || 0) + + const requiresByConfig = !!(reportRequires && reportRequires.face) + const requiresPayment = + typeof recordRequires === 'boolean' ? recordRequires : requiresByConfig + // 系统设置需付款但金额为0 则直接可查看,不展示付费墙 + const needPay = requiresPayment && facePrice > 0 + const amountYuan = facePrice > 0 ? facePrice : 0 + + this.setData({ + payInfo: { + requiresPayment: needPay, + isPaid: !!recordIsPaid, + amountYuan + } + }) + }) + .catch(() => { + this.setData({ + payInfo: { + requiresPayment: false, + isPaid: !!recordIsPaid, + amountYuan: 0 + } + }) + }) + }, + + // 解锁完整报告:发起人脸测试付费 + unlockFullReport() { + const { payInfo, testResultId, hasReloadedAfterPay } = this.data + if (!payInfo.requiresPayment || payInfo.isPaid) { + return + } + + app.ensureLogin().then((logged) => { + if (!logged) { + wx.showToast({ title: '请先登录后再解锁', icon: 'none' }) + return + } + payment.purchaseFaceTest({ + testResultId, + success: () => { + wx.showToast({ title: '已解锁完整报告', icon: 'success' }) + + // 本地先标记已付费,避免按钮仍然提示“需要解锁” + this.setData({ + 'payInfo.isPaid': true + }) + + // 避免重复触发刷新:只在还没刷新的情况下,延迟 0.5s 拉一次详情 + if (testResultId && !hasReloadedAfterPay) { + this.setData({ hasReloadedAfterPay: true }) + setTimeout(() => { + this.reloadFullDetail(testResultId) + }, 500) + } + }, + fail: () => { + // 支付失败或取消,这里不做额外处理 + } + }) + }) + }, + + // AI结果页付费按钮:就地触发微信手机号授权,然后调用 unlockFullReport + onGetPhoneNumberForFacePay(e) { + if (!ensureProfileCompleteAndRedirect()) return + const { code, errMsg } = e.detail || {} + if (errMsg && errMsg.indexOf('getPhoneNumber:fail') === 0) { + if (!hasPhone()) { + wx.showToast({ title: '需要授权手机号才能继续', icon: 'none' }) + return + } + this.unlockFullReport() + return + } + if (!code) { + if (hasPhone()) { + this.unlockFullReport() + } else { + wx.showToast({ title: '获取手机号失败', icon: 'none' }) + } + return + } + bindPhoneByCode(code) + .then(() => { + this.setData({ hasPhone: true }) + this.unlockFullReport() + }) + .catch(() => { + // 保持在当前页,等待用户重新点击 + }) + }, + + // 解锁成功后,根据测试记录ID重新拉取完整详情 + reloadFullDetail(id) { + if (!id) return + + const app = getApp() + const apiBase = app.globalData?.apiBase || '' + + wx.showLoading({ title: '加载完整报告...' }) + wx.request({ + url: `${apiBase}/api/test/detail`, + method: 'GET', + header: { + 'Authorization': app.globalData?.token ? `Bearer ${app.globalData.token}` : '' + }, + data: { id }, + success: (res) => { + if (res.statusCode === 200 && res.data && res.data.data) { + const payload = res.data.data + const apiData = payload.data || payload + + // 详情接口在后台会根据 isPaid 返回完整结构,这里直接复用已有处理逻辑 + this.processResult(apiData) + + // 同步数据库中的付费状态与金额(test_results.paidAmount) + const recordRequires = + typeof payload.requiresPayment === 'boolean' || typeof payload.requiresPayment === 'number' + ? !!payload.requiresPayment + : null + const recordIsPaid = !!payload.isPaid + this.initPayInfoFromRuntime(recordRequires, recordIsPaid, payload) + } else { + console.error('刷新完整报告失败', res) + } + }, + fail: (err) => { + console.error('请求完整报告失败', err) + }, + complete: () => { + wx.hideLoading() + } + }) + }, + + // 无人脸错误:显示提示并提供重新拍摄入口 + showNoFaceError(message) { + this.setData({ + isAnalyzing: false, + showResult: false, + noFaceError: true, + noFaceMessage: message || '图片中未检测到人脸,请重新拍摄清晰的正面照片' + }) + }, + + // 分析失败:不使用任何本地模拟数据 + showAnalyzeError(message) { + this.setData({ + isAnalyzing: false, + showResult: true, + hasError: true, + errorMessage: message || '分析失败,请稍后重试' + }) + }, + + // 跳转到详情性格测试选择页(MBTI / PDP / DISC 三选一) + goToMBTI() { + wx.navigateTo({ url: '/pages/test-select/index' }) + }, + + shareResult() { + wx.showShareMenu({ withShareTicket: true, menus: ['shareAppMessage', 'shareTimeline'] }) + }, + + retake() { + // camera 是 tabBar 页面,必须用 switchTab + wx.switchTab({ url: '/pages/index/camera' }) + }, + + goHome() { + // 根据当前 scope 跳对应首页 + const scope = (getApp().globalData && getApp().globalData.appScope) || 'personal' + if (scope === 'enterprise') { + wx.navigateTo({ url: '/pages/enterprise/index' }) + } else { + wx.switchTab({ url: '/pages/index/index' }) + } + }, + + onShareAppMessage() { + const r = this.data.result + const rm = this.data.reviewMode + const t = rm ? '测试结果' : (this.data.aiAnalysisText || '智能分析') + const { getSharePathByScope } = require('../../utils/share') + return { + title: `${t}:我是${r?.mbti} ${r?.pdpEmoji}${r?.pdp}型,来测测你的!`, + path: getSharePathByScope('/pages/index/index') + } + }, + + onShareTimeline() { + const r = this.data.result + const rm = this.data.reviewMode + const t = rm ? '测试结果' : (this.data.aiAnalysisText || '智能分析') + const { buildShareQuery } = require('../../utils/share') + return { + title: `${t}:我是${r?.mbti} ${r?.pdpEmoji}${r?.pdp}型,来测测你的!`, + query: buildShareQuery() + } + } +}) diff --git a/miniprogram/pages/index/result.wxml b/miniprogram/pages/index/result.wxml index e2562ca..83fed02 100644 --- a/miniprogram/pages/index/result.wxml +++ b/miniprogram/pages/index/result.wxml @@ -1,304 +1,304 @@ - - - - - - - - - {{analyzingTitle || '正在分析中'}} - {{analyzingTip}} - - - - 这个过程可能需要30秒到1分钟,请耐心等待... - - - - - - - - 🚫 - - 未检测到人脸 - {{noFaceMessage}} - - 拍摄建议: - • 确保面部完整出现在画面中 - • 保持光线充足,避免逆光 - • 正对镜头,不要遮挡面部 - • 保持适当距离,不要过近或过远 - - - 重新拍摄 - - - - - - - - - - {{hasError ? '分析出现异常' : '分析完成,以下是您的个性化报告'}} - - - - - 错误信息 - {{errorMessage}} - - 重新分析 - - - - - - 性格类型分析 - - - - {{result.mbti}} - {{result.title}} - {{result.summary}} - - - - - - PDP主性格 - {{result.pdp || '--'}} - 辅助: {{result.pdpAux}} - - - DISC类型 - {{result.disc || '--'}}型 - 辅助: {{result.discAux}} - - - - - - 主要优势 - - - - {{item}} - - - - - - 下一步:详细性格测试 → - - - - - - 性格概述 - {{result.summary}} - - - - - 盖洛普前三大优势 - - - {{index + 1}} - {{item}} - - - - - - - - 完整版性格深度解析 - 解锁后将展示完整报告内容 - - - - - 一次性解锁本次{{reportTitle || '分析报告'}},永久保存在「历史记录」中 - - - - - - - - - - - 面部特征分析 - - - {{item.label}}: - {{item.description}} - - - - - - - 面相分析 - {{result.faceAnalysisText}} - - - - - - - 骨相分析(《冰鉴》八骨) - - - 五行形相: - {{result.boneAnalysis.boneFormSummary.elementType}} - - - 骨肉关系: - {{result.boneAnalysis.boneFormSummary.boneFleshRelation}} - - - - - - - 骨相分析(《冰鉴》八骨) - {{result.boneAnalysisText}} - - - - - 人际关系与团队合作 - {{result.relationship}} - - - - - 简历亮点 - {{result.resumeHighlights}} - - - - - 职业画像 - - 核心优势 - - {{item}} - - - - 潜在风险 - - {{item}} - - - - 工作风格 - {{result.portrait.workStyle}} - - - - - - HR视角 - - 岗位推荐 - - 最适合 - - {{item}} - - - - 不适合 - - {{item}} - - - - - 成长周期 - - - 入职适应 - {{result.hrView.lifecycle.onboarding}} - - - 试用期 - {{result.hrView.lifecycle.probation}} - - - 成长期 - {{result.hrView.lifecycle.growth}} - - - 留人因素 - {{result.hrView.lifecycle.retention}} - - - - - 绩效潜力 - 潜力:{{result.hrView.performance.potential}} - - 驱动因子: - {{result.hrView.performance.drivers.join('、')}} - - - - 团队适配 - {{result.hrView.teamFit.bestTeam || ''}} - 管理建议:{{result.hrView.teamFit.manageAdvice}} - - - - - - 老板视角 - {{result.bossView.headline}} - - - {{item.label}} - {{item.value}} - - - 用人成本预判:{{result.bossView.costInsight}} - - - - - - - - - 重新分析 - - - 返回首页 - - - - 人工智能生成 - - + + + + + + + + + {{analyzingTitle || '正在分析中'}} + {{analyzingTip}} + + + + 这个过程可能需要30秒到1分钟,请耐心等待... + + + + + + + + 🚫 + + 未检测到人脸 + {{noFaceMessage}} + + 拍摄建议: + • 确保面部完整出现在画面中 + • 保持光线充足,避免逆光 + • 正对镜头,不要遮挡面部 + • 保持适当距离,不要过近或过远 + + + 重新拍摄 + + + + + + + + + + {{hasError ? '分析出现异常' : '分析完成,以下是您的个性化报告'}} + + + + + 错误信息 + {{errorMessage}} + + 重新分析 + + + + + + 性格类型分析 + + + + {{result.mbti}} + {{result.title}} + {{result.summary}} + + + + + + PDP主性格 + {{result.pdp || '--'}} + 辅助: {{result.pdpAux}} + + + DISC类型 + {{result.disc || '--'}}型 + 辅助: {{result.discAux}} + + + + + + 主要优势 + + + + {{item}} + + + + + + 下一步:详细性格测试 → + + + + + + 性格概述 + {{result.summary}} + + + + + 盖洛普前三大优势 + + + {{index + 1}} + {{item}} + + + + + + + + 完整版性格深度解析 + 解锁后将展示完整报告内容 + + + + + 一次性解锁本次{{reportTitle || '分析报告'}},永久保存在「历史记录」中 + + + + + + + + + + + 面部特征分析 + + + {{item.label}}: + {{item.description}} + + + + + + + 面相分析 + {{result.faceAnalysisText}} + + + + + + + 骨相分析(《冰鉴》八骨) + + + 五行形相: + {{result.boneAnalysis.boneFormSummary.elementType}} + + + 骨肉关系: + {{result.boneAnalysis.boneFormSummary.boneFleshRelation}} + + + + + + + 骨相分析(《冰鉴》八骨) + {{result.boneAnalysisText}} + + + + + 人际关系与团队合作 + {{result.relationship}} + + + + + 简历亮点 + {{result.resumeHighlights}} + + + + + 职业画像 + + 核心优势 + + {{item}} + + + + 潜在风险 + + {{item}} + + + + 工作风格 + {{result.portrait.workStyle}} + + + + + + HR视角 + + 岗位推荐 + + 最适合 + + {{item}} + + + + 不适合 + + {{item}} + + + + + 成长周期 + + + 入职适应 + {{result.hrView.lifecycle.onboarding}} + + + 试用期 + {{result.hrView.lifecycle.probation}} + + + 成长期 + {{result.hrView.lifecycle.growth}} + + + 留人因素 + {{result.hrView.lifecycle.retention}} + + + + + 绩效潜力 + 潜力:{{result.hrView.performance.potential}} + + 驱动因子: + {{result.hrView.performance.drivers.join('、')}} + + + + 团队适配 + {{result.hrView.teamFit.bestTeam || ''}} + 管理建议:{{result.hrView.teamFit.manageAdvice}} + + + + + + 老板视角 + {{result.bossView.headline}} + + + {{item.label}} + {{item.value}} + + + 用人成本预判:{{result.bossView.costInsight}} + + + + + + + + + 重新分析 + + + 返回首页 + + + + 人工智能生成 + + diff --git a/miniprogram/pages/index/upload.js b/miniprogram/pages/index/upload.js index ffce82a..502c0c0 100644 --- a/miniprogram/pages/index/upload.js +++ b/miniprogram/pages/index/upload.js @@ -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() + } + } + }) + } +}) diff --git a/miniprogram/pages/profile/index.js b/miniprogram/pages/profile/index.js index 79848d5..790d502 100644 --- a/miniprogram/pages/profile/index.js +++ b/miniprogram/pages/profile/index.js @@ -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() + } + } +}) diff --git a/miniprogram/pages/profile/index.wxml b/miniprogram/pages/profile/index.wxml index 1899372..e1f9cf0 100644 --- a/miniprogram/pages/profile/index.wxml +++ b/miniprogram/pages/profile/index.wxml @@ -1,212 +1,212 @@ - - - - - - 我的 - - - - - - - - - - {{avatarLetter}} - - - - 登录中... - 登录失败,点击重试 - 点击登录 - 登录后查看你的测试结果 - - - - - - - - - - - - {{avatarLetter}} - - - - - - - - {{nicknameDisplay || '点击设置昵称'}} - - - - - {{mbtiType}} - - - {{discType}}型 - - - {{pdpType}} - - - 暂无测试记录 - - - - - - - - - - - - 最新测试 - - 查看全部 - - - - - - - - - 🧠 - - MBTI性格 - {{mbtiType}} - {{mbtiTime}} - - - - - - 📊 - - DISC测评 - {{discType}}型 - {{discTime}} - - - - - - 🦁 - - PDP行为 - {{pdpType}} - {{pdpTime}} - - - - - - 👁️ - - 面相分析 - {{aiType}} - {{aiTime}} - - - - - - - - - - - 📈 - {{promoCenterTitle}} - - - - - 邀请好友 - {{promoTotalInvite}} - - - - 累计收益 - ¥{{promoTotalEarned}} - - - - 可提现 - ¥{{promoWithdrawable}} - - - - - - - - 深度服务 - - - - - 📄 - - - 专业报告 - 解锁完整的深度性格解析 - - - - - - - - - 🏢 - - - 企业版服务 - 团队测评与人才管理方案 - - - - - - - - - 🕒 - - - 测试历史 - {{testCount > 0 ? testCount + '条记录' : '查看过往所有测试记录'}} - - - - - - - - 📋 - - - 我的简历 - 查看与设置默认简历 - - - - - - - - - - + + + + + + 我的 + + + + + + + + + + {{avatarLetter}} + + + + 登录中... + 登录失败,点击重试 + 点击登录 + 登录后查看你的测试结果 + + + + + + + + + + + + {{avatarLetter}} + + + + + + + + {{nicknameDisplay || '点击设置昵称'}} + + + + + {{mbtiType}} + + + {{discType}}型 + + + {{pdpType}} + + + 暂无测试记录 + + + + + + + + + + + + 最新测试 + + 查看全部 + + + + + + + + + 🧠 + + MBTI性格 + {{mbtiType}} + {{mbtiTime}} + + + + + + 📊 + + DISC测评 + {{discType}}型 + {{discTime}} + + + + + + 🦁 + + PDP行为 + {{pdpType}} + {{pdpTime}} + + + + + + 👁️ + + 面相分析 + {{aiType}} + {{aiTime}} + + + + + + + + + + + 📈 + {{promoCenterTitle}} + + + + + 邀请好友 + {{promoTotalInvite}} + + + + 累计收益 + ¥{{promoTotalEarned}} + + + + 可提现 + ¥{{promoWithdrawable}} + + + + + + + + 深度服务 + + + + + 📄 + + + 专业报告 + 解锁完整的深度性格解析 + + + + + + + + + 🏢 + + + 企业版服务 + 团队测评与人才管理方案 + + + + + + + + + 🕒 + + + 测试历史 + {{testCount > 0 ? testCount + '条记录' : '查看过往所有测试记录'}} + + + + + + + + 📋 + + + 我的简历 + 查看与设置默认简历 + + + + + + + + + +