feat: 头像上传
This commit is contained in:
343
package-lock.json
generated
343
package-lock.json
generated
@@ -10,6 +10,7 @@
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"axios": "^1.3.6",
|
||||
"cropper-next-vue": "^0.3.0",
|
||||
"crypto-js": "^4.2.0",
|
||||
"element-plus": "^2.13.2",
|
||||
"gsap": "^3.13.0",
|
||||
@@ -21,7 +22,7 @@
|
||||
"pinia-persistedstate-plugin": "^0.1.0",
|
||||
"pinia-plugin-persistedstate": "^3.1.0",
|
||||
"swiper": "^12.1.3",
|
||||
"vue": "^3.2.47",
|
||||
"vue": "^3.5.35",
|
||||
"vue-i18n": "^11.2.8",
|
||||
"vue-router": "^4.1.6"
|
||||
},
|
||||
@@ -58,30 +59,30 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@babel/helper-string-parser": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
|
||||
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
||||
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
|
||||
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"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.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
|
||||
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/types": "^7.29.0"
|
||||
"@babel/types": "^7.29.7"
|
||||
},
|
||||
"bin": {
|
||||
"parser": "bin/babel-parser.js"
|
||||
@@ -91,13 +92,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/types": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz",
|
||||
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
|
||||
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.28.5"
|
||||
"@babel/helper-string-parser": "^7.29.7",
|
||||
"@babel/helper-validator-identifier": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -1162,21 +1163,21 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-core": {
|
||||
"version": "3.5.27",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.27.tgz",
|
||||
"integrity": "sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==",
|
||||
"version": "3.5.35",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.35.tgz",
|
||||
"integrity": "sha512-BUmHaR1J+O+CKZ9uJucdVTEr1LHsdyvv7vG3eNRhK3CczEHeMd/LtsHAuD7PbrxvI2envCY2v7HI1vC1aBRzKw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.5",
|
||||
"@vue/shared": "3.5.27",
|
||||
"entities": "^7.0.0",
|
||||
"@babel/parser": "^7.29.3",
|
||||
"@vue/shared": "3.5.35",
|
||||
"entities": "^7.0.1",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-core/node_modules/entities": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
|
||||
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==",
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
@@ -1187,40 +1188,40 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-dom": {
|
||||
"version": "3.5.27",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.27.tgz",
|
||||
"integrity": "sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==",
|
||||
"version": "3.5.35",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.35.tgz",
|
||||
"integrity": "sha512-k+bprkXxuqhVajgTx5mUHuir7TwQzUKOWR40ng1ncAqQRPnrLngGGgqVEEhOnTMlc8btHYVKmrP8s5Qyg0hvYA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-core": "3.5.27",
|
||||
"@vue/shared": "3.5.27"
|
||||
"@vue/compiler-core": "3.5.35",
|
||||
"@vue/shared": "3.5.35"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-sfc": {
|
||||
"version": "3.5.27",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.27.tgz",
|
||||
"integrity": "sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ==",
|
||||
"version": "3.5.35",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.35.tgz",
|
||||
"integrity": "sha512-G5VPMcXTSywXBgtFOZOnHKBxKSrwXUcvY1iaF5/hRcy7t0J6CH/d8ha9F4nzi00Fax1eLV0QHM7v4mQu68jydw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.28.5",
|
||||
"@vue/compiler-core": "3.5.27",
|
||||
"@vue/compiler-dom": "3.5.27",
|
||||
"@vue/compiler-ssr": "3.5.27",
|
||||
"@vue/shared": "3.5.27",
|
||||
"@babel/parser": "^7.29.3",
|
||||
"@vue/compiler-core": "3.5.35",
|
||||
"@vue/compiler-dom": "3.5.35",
|
||||
"@vue/compiler-ssr": "3.5.35",
|
||||
"@vue/shared": "3.5.35",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.21",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss": "^8.5.15",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/compiler-ssr": {
|
||||
"version": "3.5.27",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.27.tgz",
|
||||
"integrity": "sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw==",
|
||||
"version": "3.5.35",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.35.tgz",
|
||||
"integrity": "sha512-rGhAeXgdM7/ffTJGXT69rCCdTmjDewnFuUZfBQQHTdcEBeWdT5HCGY60y2ytLJr9/Dsu7IntUi5z/w0h6Rjnzw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.27",
|
||||
"@vue/shared": "3.5.27"
|
||||
"@vue/compiler-dom": "3.5.35",
|
||||
"@vue/shared": "3.5.35"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/devtools-api": {
|
||||
@@ -1267,53 +1268,53 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/reactivity": {
|
||||
"version": "3.5.27",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.27.tgz",
|
||||
"integrity": "sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==",
|
||||
"version": "3.5.35",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.35.tgz",
|
||||
"integrity": "sha512-tVc+SsHConvh/Lz64qq1pP3rYArBmK42xonovEcxY74SQtvctZodG/zhq54P5dr38cVuw25d27cPNRdlMidpGQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/shared": "3.5.27"
|
||||
"@vue/shared": "3.5.35"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-core": {
|
||||
"version": "3.5.27",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.27.tgz",
|
||||
"integrity": "sha512-fxVuX/fzgzeMPn/CLQecWeDIFNt3gQVhxM0rW02Tvp/YmZfXQgcTXlakq7IMutuZ/+Ogbn+K0oct9J3JZfyk3A==",
|
||||
"version": "3.5.35",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.35.tgz",
|
||||
"integrity": "sha512-A/xFNX9loIcWDygeQuNCfKuh0CoYBzxhqEMNah5TSFg9Z53DrFYEN2qi5CU9necjM1OWYegYREUTHmXTmhfXtg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.27",
|
||||
"@vue/shared": "3.5.27"
|
||||
"@vue/reactivity": "3.5.35",
|
||||
"@vue/shared": "3.5.35"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/runtime-dom": {
|
||||
"version": "3.5.27",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.27.tgz",
|
||||
"integrity": "sha512-/QnLslQgYqSJ5aUmb5F0z0caZPGHRB8LEAQ1s81vHFM5CBfnun63rxhvE/scVb/j3TbBuoZwkJyiLCkBluMpeg==",
|
||||
"version": "3.5.35",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.35.tgz",
|
||||
"integrity": "sha512-odrJ1C391dbGnyDRh8U+rnP7J2amIEzfmRk5vXy7xi3aZhEXofTvpi0T4HJb6jlNqQZTNPR5MPHSB3RHNkIORA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/reactivity": "3.5.27",
|
||||
"@vue/runtime-core": "3.5.27",
|
||||
"@vue/shared": "3.5.27",
|
||||
"@vue/reactivity": "3.5.35",
|
||||
"@vue/runtime-core": "3.5.35",
|
||||
"@vue/shared": "3.5.35",
|
||||
"csstype": "^3.2.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/server-renderer": {
|
||||
"version": "3.5.27",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.27.tgz",
|
||||
"integrity": "sha512-qOz/5thjeP1vAFc4+BY3Nr6wxyLhpeQgAE/8dDtKo6a6xdk+L4W46HDZgNmLOBUDEkFXV3G7pRiUqxjX0/2zWA==",
|
||||
"version": "3.5.35",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.35.tgz",
|
||||
"integrity": "sha512-NkebSOYdB97wi8OQcO3HqzZSlymJi/aWsN/7h74OSVhRTm6qGs3Jp3e0rCXynmWwSlKeRrnlIug+ilYoHBmQDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-ssr": "3.5.27",
|
||||
"@vue/shared": "3.5.27"
|
||||
"@vue/compiler-ssr": "3.5.35",
|
||||
"@vue/shared": "3.5.35"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "3.5.27"
|
||||
"vue": "3.5.35"
|
||||
}
|
||||
},
|
||||
"node_modules/@vue/shared": {
|
||||
"version": "3.5.27",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.27.tgz",
|
||||
"integrity": "sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ==",
|
||||
"version": "3.5.35",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.35.tgz",
|
||||
"integrity": "sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@vue/tsconfig": {
|
||||
@@ -2153,6 +2154,18 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/cropper-next-vue": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/cropper-next-vue/-/cropper-next-vue-0.3.0.tgz",
|
||||
"integrity": "sha512-7xw0gGGCc0bKZhtHZ1BU6cxy9QYN5j2BpgIbM27Zdw8f9+W0FekNxSDVpQ4pGbSR540hVuzA+9uO8obUV7ugeA==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=22.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vue": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
@@ -2313,7 +2326,7 @@
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
@@ -5214,9 +5227,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"version": "3.3.12",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
||||
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -6039,9 +6052,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||
"version": "8.5.15",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
||||
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -6058,7 +6071,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"nanoid": "^3.3.12",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
@@ -8391,16 +8404,16 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/vue": {
|
||||
"version": "3.5.27",
|
||||
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.27.tgz",
|
||||
"integrity": "sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==",
|
||||
"version": "3.5.35",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.35.tgz",
|
||||
"integrity": "sha512-cx89fnr+0kVGHiNFG6y6s0bdjypJRFNZn6x3WPstNdQR1bi1mbB7h4v5IBGTsPJU3nK1+0Iqj3Zf+hZWMieR4Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@vue/compiler-dom": "3.5.27",
|
||||
"@vue/compiler-sfc": "3.5.27",
|
||||
"@vue/runtime-dom": "3.5.27",
|
||||
"@vue/server-renderer": "3.5.27",
|
||||
"@vue/shared": "3.5.27"
|
||||
"@vue/compiler-dom": "3.5.35",
|
||||
"@vue/compiler-sfc": "3.5.35",
|
||||
"@vue/runtime-dom": "3.5.35",
|
||||
"@vue/server-renderer": "3.5.35",
|
||||
"@vue/shared": "3.5.35"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "*"
|
||||
@@ -8734,30 +8747,30 @@
|
||||
"dev": true
|
||||
},
|
||||
"@babel/helper-string-parser": {
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
||||
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz",
|
||||
"integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="
|
||||
},
|
||||
"@babel/helper-validator-identifier": {
|
||||
"version": "7.28.5",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
||||
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz",
|
||||
"integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="
|
||||
},
|
||||
"@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.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz",
|
||||
"integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==",
|
||||
"requires": {
|
||||
"@babel/types": "^7.29.0"
|
||||
"@babel/types": "^7.29.7"
|
||||
}
|
||||
},
|
||||
"@babel/types": {
|
||||
"version": "7.29.0",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz",
|
||||
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz",
|
||||
"integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==",
|
||||
"requires": {
|
||||
"@babel/helper-string-parser": "^7.27.1",
|
||||
"@babel/helper-validator-identifier": "^7.28.5"
|
||||
"@babel/helper-string-parser": "^7.29.7",
|
||||
"@babel/helper-validator-identifier": "^7.29.7"
|
||||
}
|
||||
},
|
||||
"@ctrl/tinycolor": {
|
||||
@@ -9428,56 +9441,56 @@
|
||||
}
|
||||
},
|
||||
"@vue/compiler-core": {
|
||||
"version": "3.5.27",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.27.tgz",
|
||||
"integrity": "sha512-gnSBQjZA+//qDZen+6a2EdHqJ68Z7uybrMf3SPjEGgG4dicklwDVmMC1AeIHxtLVPT7sn6sH1KOO+tS6gwOUeQ==",
|
||||
"version": "3.5.35",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.35.tgz",
|
||||
"integrity": "sha512-BUmHaR1J+O+CKZ9uJucdVTEr1LHsdyvv7vG3eNRhK3CczEHeMd/LtsHAuD7PbrxvI2envCY2v7HI1vC1aBRzKw==",
|
||||
"requires": {
|
||||
"@babel/parser": "^7.28.5",
|
||||
"@vue/shared": "3.5.27",
|
||||
"entities": "^7.0.0",
|
||||
"@babel/parser": "^7.29.3",
|
||||
"@vue/shared": "3.5.35",
|
||||
"entities": "^7.0.1",
|
||||
"estree-walker": "^2.0.2",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"entities": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
|
||||
"integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@vue/compiler-dom": {
|
||||
"version": "3.5.27",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.27.tgz",
|
||||
"integrity": "sha512-oAFea8dZgCtVVVTEC7fv3T5CbZW9BxpFzGGxC79xakTr6ooeEqmRuvQydIiDAkglZEAd09LgVf1RoDnL54fu5w==",
|
||||
"version": "3.5.35",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.35.tgz",
|
||||
"integrity": "sha512-k+bprkXxuqhVajgTx5mUHuir7TwQzUKOWR40ng1ncAqQRPnrLngGGgqVEEhOnTMlc8btHYVKmrP8s5Qyg0hvYA==",
|
||||
"requires": {
|
||||
"@vue/compiler-core": "3.5.27",
|
||||
"@vue/shared": "3.5.27"
|
||||
"@vue/compiler-core": "3.5.35",
|
||||
"@vue/shared": "3.5.35"
|
||||
}
|
||||
},
|
||||
"@vue/compiler-sfc": {
|
||||
"version": "3.5.27",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.27.tgz",
|
||||
"integrity": "sha512-sHZu9QyDPeDmN/MRoshhggVOWE5WlGFStKFwu8G52swATgSny27hJRWteKDSUUzUH+wp+bmeNbhJnEAel/auUQ==",
|
||||
"version": "3.5.35",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.35.tgz",
|
||||
"integrity": "sha512-G5VPMcXTSywXBgtFOZOnHKBxKSrwXUcvY1iaF5/hRcy7t0J6CH/d8ha9F4nzi00Fax1eLV0QHM7v4mQu68jydw==",
|
||||
"requires": {
|
||||
"@babel/parser": "^7.28.5",
|
||||
"@vue/compiler-core": "3.5.27",
|
||||
"@vue/compiler-dom": "3.5.27",
|
||||
"@vue/compiler-ssr": "3.5.27",
|
||||
"@vue/shared": "3.5.27",
|
||||
"@babel/parser": "^7.29.3",
|
||||
"@vue/compiler-core": "3.5.35",
|
||||
"@vue/compiler-dom": "3.5.35",
|
||||
"@vue/compiler-ssr": "3.5.35",
|
||||
"@vue/shared": "3.5.35",
|
||||
"estree-walker": "^2.0.2",
|
||||
"magic-string": "^0.30.21",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss": "^8.5.15",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
},
|
||||
"@vue/compiler-ssr": {
|
||||
"version": "3.5.27",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.27.tgz",
|
||||
"integrity": "sha512-Sj7h+JHt512fV1cTxKlYhg7qxBvack+BGncSpH+8vnN+KN95iPIcqB5rsbblX40XorP+ilO7VIKlkuu3Xq2vjw==",
|
||||
"version": "3.5.35",
|
||||
"resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.35.tgz",
|
||||
"integrity": "sha512-rGhAeXgdM7/ffTJGXT69rCCdTmjDewnFuUZfBQQHTdcEBeWdT5HCGY60y2ytLJr9/Dsu7IntUi5z/w0h6Rjnzw==",
|
||||
"requires": {
|
||||
"@vue/compiler-dom": "3.5.27",
|
||||
"@vue/shared": "3.5.27"
|
||||
"@vue/compiler-dom": "3.5.35",
|
||||
"@vue/shared": "3.5.35"
|
||||
}
|
||||
},
|
||||
"@vue/devtools-api": {
|
||||
@@ -9507,46 +9520,46 @@
|
||||
}
|
||||
},
|
||||
"@vue/reactivity": {
|
||||
"version": "3.5.27",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.27.tgz",
|
||||
"integrity": "sha512-vvorxn2KXfJ0nBEnj4GYshSgsyMNFnIQah/wczXlsNXt+ijhugmW+PpJ2cNPe4V6jpnBcs0MhCODKllWG+nvoQ==",
|
||||
"version": "3.5.35",
|
||||
"resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.35.tgz",
|
||||
"integrity": "sha512-tVc+SsHConvh/Lz64qq1pP3rYArBmK42xonovEcxY74SQtvctZodG/zhq54P5dr38cVuw25d27cPNRdlMidpGQ==",
|
||||
"requires": {
|
||||
"@vue/shared": "3.5.27"
|
||||
"@vue/shared": "3.5.35"
|
||||
}
|
||||
},
|
||||
"@vue/runtime-core": {
|
||||
"version": "3.5.27",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.27.tgz",
|
||||
"integrity": "sha512-fxVuX/fzgzeMPn/CLQecWeDIFNt3gQVhxM0rW02Tvp/YmZfXQgcTXlakq7IMutuZ/+Ogbn+K0oct9J3JZfyk3A==",
|
||||
"version": "3.5.35",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.35.tgz",
|
||||
"integrity": "sha512-A/xFNX9loIcWDygeQuNCfKuh0CoYBzxhqEMNah5TSFg9Z53DrFYEN2qi5CU9necjM1OWYegYREUTHmXTmhfXtg==",
|
||||
"requires": {
|
||||
"@vue/reactivity": "3.5.27",
|
||||
"@vue/shared": "3.5.27"
|
||||
"@vue/reactivity": "3.5.35",
|
||||
"@vue/shared": "3.5.35"
|
||||
}
|
||||
},
|
||||
"@vue/runtime-dom": {
|
||||
"version": "3.5.27",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.27.tgz",
|
||||
"integrity": "sha512-/QnLslQgYqSJ5aUmb5F0z0caZPGHRB8LEAQ1s81vHFM5CBfnun63rxhvE/scVb/j3TbBuoZwkJyiLCkBluMpeg==",
|
||||
"version": "3.5.35",
|
||||
"resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.35.tgz",
|
||||
"integrity": "sha512-odrJ1C391dbGnyDRh8U+rnP7J2amIEzfmRk5vXy7xi3aZhEXofTvpi0T4HJb6jlNqQZTNPR5MPHSB3RHNkIORA==",
|
||||
"requires": {
|
||||
"@vue/reactivity": "3.5.27",
|
||||
"@vue/runtime-core": "3.5.27",
|
||||
"@vue/shared": "3.5.27",
|
||||
"@vue/reactivity": "3.5.35",
|
||||
"@vue/runtime-core": "3.5.35",
|
||||
"@vue/shared": "3.5.35",
|
||||
"csstype": "^3.2.3"
|
||||
}
|
||||
},
|
||||
"@vue/server-renderer": {
|
||||
"version": "3.5.27",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.27.tgz",
|
||||
"integrity": "sha512-qOz/5thjeP1vAFc4+BY3Nr6wxyLhpeQgAE/8dDtKo6a6xdk+L4W46HDZgNmLOBUDEkFXV3G7pRiUqxjX0/2zWA==",
|
||||
"version": "3.5.35",
|
||||
"resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.35.tgz",
|
||||
"integrity": "sha512-NkebSOYdB97wi8OQcO3HqzZSlymJi/aWsN/7h74OSVhRTm6qGs3Jp3e0rCXynmWwSlKeRrnlIug+ilYoHBmQDA==",
|
||||
"requires": {
|
||||
"@vue/compiler-ssr": "3.5.27",
|
||||
"@vue/shared": "3.5.27"
|
||||
"@vue/compiler-ssr": "3.5.35",
|
||||
"@vue/shared": "3.5.35"
|
||||
}
|
||||
},
|
||||
"@vue/shared": {
|
||||
"version": "3.5.27",
|
||||
"resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.27.tgz",
|
||||
"integrity": "sha512-dXr/3CgqXsJkZ0n9F3I4elY8wM9jMJpP3pvRG52r6m0tu/MsAFIe6JpXVGeNMd/D9F4hQynWT8Rfuj0bdm9kFQ=="
|
||||
"version": "3.5.35",
|
||||
"resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.35.tgz",
|
||||
"integrity": "sha512-zSbjL7gRXwks2ZQLRGCajBtBXEOXW9Ddhn/HvSdrGkE2dqGnumzW8XtusRrxrE9LvqtiqDXQ+A60Hp6mvdYxfA=="
|
||||
},
|
||||
"@vue/tsconfig": {
|
||||
"version": "0.1.3",
|
||||
@@ -10148,6 +10161,12 @@
|
||||
"vary": "^1"
|
||||
}
|
||||
},
|
||||
"cropper-next-vue": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/cropper-next-vue/-/cropper-next-vue-0.3.0.tgz",
|
||||
"integrity": "sha512-7xw0gGGCc0bKZhtHZ1BU6cxy9QYN5j2BpgIbM27Zdw8f9+W0FekNxSDVpQ4pGbSR540hVuzA+9uO8obUV7ugeA==",
|
||||
"requires": {}
|
||||
},
|
||||
"cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
@@ -10260,7 +10279,7 @@
|
||||
},
|
||||
"csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="
|
||||
},
|
||||
"data-view-buffer": {
|
||||
@@ -12439,9 +12458,9 @@
|
||||
"dev": true
|
||||
},
|
||||
"nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="
|
||||
"version": "3.3.12",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
|
||||
"integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="
|
||||
},
|
||||
"nanomatch": {
|
||||
"version": "1.2.13",
|
||||
@@ -13051,11 +13070,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"postcss": {
|
||||
"version": "8.5.6",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
|
||||
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
|
||||
"version": "8.5.15",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
|
||||
"integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
|
||||
"requires": {
|
||||
"nanoid": "^3.3.11",
|
||||
"nanoid": "^3.3.12",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
}
|
||||
@@ -14817,15 +14836,15 @@
|
||||
}
|
||||
},
|
||||
"vue": {
|
||||
"version": "3.5.27",
|
||||
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.27.tgz",
|
||||
"integrity": "sha512-aJ/UtoEyFySPBGarREmN4z6qNKpbEguYHMmXSiOGk69czc+zhs0NF6tEFrY8TZKAl8N/LYAkd4JHVd5E/AsSmw==",
|
||||
"version": "3.5.35",
|
||||
"resolved": "https://registry.npmjs.org/vue/-/vue-3.5.35.tgz",
|
||||
"integrity": "sha512-cx89fnr+0kVGHiNFG6y6s0bdjypJRFNZn6x3WPstNdQR1bi1mbB7h4v5IBGTsPJU3nK1+0Iqj3Zf+hZWMieR4Q==",
|
||||
"requires": {
|
||||
"@vue/compiler-dom": "3.5.27",
|
||||
"@vue/compiler-sfc": "3.5.27",
|
||||
"@vue/runtime-dom": "3.5.27",
|
||||
"@vue/server-renderer": "3.5.27",
|
||||
"@vue/shared": "3.5.27"
|
||||
"@vue/compiler-dom": "3.5.35",
|
||||
"@vue/compiler-sfc": "3.5.35",
|
||||
"@vue/runtime-dom": "3.5.35",
|
||||
"@vue/server-renderer": "3.5.35",
|
||||
"@vue/shared": "3.5.35"
|
||||
}
|
||||
},
|
||||
"vue-eslint-parser": {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.3.6",
|
||||
"cropper-next-vue": "^0.3.0",
|
||||
"crypto-js": "^4.2.0",
|
||||
"element-plus": "^2.13.2",
|
||||
"gsap": "^3.13.0",
|
||||
@@ -25,7 +26,7 @@
|
||||
"pinia-persistedstate-plugin": "^0.1.0",
|
||||
"pinia-plugin-persistedstate": "^3.1.0",
|
||||
"swiper": "^12.1.3",
|
||||
"vue": "^3.2.47",
|
||||
"vue": "^3.5.35",
|
||||
"vue-i18n": "^11.2.8",
|
||||
"vue-router": "^4.1.6"
|
||||
},
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import request from '@/utils/request'
|
||||
export interface AxiosProgressEvent {
|
||||
loaded: number;
|
||||
total?: number;
|
||||
progress?: number;
|
||||
bytes: number;
|
||||
rate?: number;
|
||||
estimated?: number;
|
||||
upload?: boolean;
|
||||
download?: boolean;
|
||||
event?: any;
|
||||
loaded: number
|
||||
total?: number
|
||||
progress?: number
|
||||
bytes: number
|
||||
rate?: number
|
||||
estimated?: number
|
||||
upload?: boolean
|
||||
download?: boolean
|
||||
event?: any
|
||||
}
|
||||
export interface WardrobeItem {
|
||||
buyerId: number
|
||||
@@ -48,7 +48,10 @@ export interface Download {
|
||||
ids: string[]
|
||||
}
|
||||
// 下载资源
|
||||
export const fetchDownloadItemsByGet = (params: Download, onDownloadProgress?: (event: AxiosProgressEvent) => void): Promise<ApiResponse> => {
|
||||
export const fetchDownloadItemsByGet = (
|
||||
params: Download,
|
||||
onDownloadProgress?: (event: AxiosProgressEvent) => void
|
||||
): Promise<ApiResponse> => {
|
||||
return request({
|
||||
url: '/buyer/listing/mall/main-product/download',
|
||||
method: 'get',
|
||||
@@ -92,6 +95,7 @@ export interface UserProfile {
|
||||
region: string
|
||||
language: string
|
||||
email: string
|
||||
avatarUrl?: string
|
||||
oldPassword?: string
|
||||
newPassword?: string
|
||||
verifyCode?: string
|
||||
@@ -104,6 +108,18 @@ export const updateUserProfile = (data: UserProfile): Promise<ApiResponse> => {
|
||||
})
|
||||
}
|
||||
|
||||
// 设置头像
|
||||
interface AvatarData {
|
||||
avatarUrl: string
|
||||
}
|
||||
export const updateUserAvatar = (data: AvatarData): Promise<ApiResponse> => {
|
||||
return request({
|
||||
url: '/buyer/profile/setAvatar',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 获取设置页验证码
|
||||
export const fetchVerifyCode = (): Promise<ApiResponse> => {
|
||||
return request({
|
||||
@@ -136,4 +152,19 @@ export const setUserLanguage = (language: string): Promise<ApiResponse> => {
|
||||
method: 'post',
|
||||
data: { language }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// 文件上传
|
||||
export const uploadFile = (file: File): Promise<ApiResponse> => {
|
||||
const formData = new FormData()
|
||||
formData.append('file', file)
|
||||
return request({
|
||||
url: '/buyer/file/upload',
|
||||
method: 'post',
|
||||
data: formData,
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data'
|
||||
},
|
||||
timeout: 60000
|
||||
})
|
||||
}
|
||||
|
||||
@@ -80,6 +80,11 @@ export default {
|
||||
role: 'ROLE',
|
||||
roleTip: 'Select up to 2 labels that suit you.'
|
||||
},
|
||||
avatarCrop: {
|
||||
title: 'Crop Avatar',
|
||||
confirm: 'Confirm',
|
||||
processing: 'Processing...',
|
||||
},
|
||||
security: {
|
||||
title: 'Security',
|
||||
description: 'Manage your login email and password.',
|
||||
@@ -133,7 +138,12 @@ export default {
|
||||
passwordSpecial: 'Password must contain special characters',
|
||||
passwordCase: 'Password must include upper/lowercase letters and numbers',
|
||||
passwordNotSameAsOld: 'New password cannot be the same as current password',
|
||||
settingsUpdated: 'Settings updated'
|
||||
settingsUpdated: 'Settings updated',
|
||||
avatarTooLarge: 'Image is too large. Max 5MB.',
|
||||
avatarCropFailed: 'Failed to crop avatar',
|
||||
avatarUploadUrlMissing: 'Failed to get uploaded file URL',
|
||||
avatarUpdated: 'Avatar updated',
|
||||
avatarUploadFailed: 'Upload failed'
|
||||
},
|
||||
roles: {
|
||||
fashionEnthusiast: 'Fashion Enthusiast',
|
||||
|
||||
@@ -75,6 +75,13 @@ export default {
|
||||
role: '身份标签',
|
||||
roleTip: '最多选择 2 个符合你的标签。'
|
||||
},
|
||||
avatarCrop: {
|
||||
title: '裁剪头像',
|
||||
confirm: '确认',
|
||||
processing: '处理中...',
|
||||
rotateLeft: '向左旋转',
|
||||
rotateRight: '向右旋转'
|
||||
},
|
||||
security: {
|
||||
title: '安全',
|
||||
description: '管理你的登录邮箱和密码。',
|
||||
@@ -128,7 +135,12 @@ export default {
|
||||
passwordSpecial: '密码必须包含特殊符号',
|
||||
passwordCase: '密码必须包含大小写字母和数字',
|
||||
passwordNotSameAsOld: '新密码不能与旧密码相同',
|
||||
settingsUpdated: '设置已更新'
|
||||
settingsUpdated: '设置已更新',
|
||||
avatarTooLarge: '图片过大,最大 5MB。',
|
||||
avatarCropFailed: '头像裁剪失败',
|
||||
avatarUploadUrlMissing: '未获取到上传后的文件地址',
|
||||
avatarUpdated: '头像已更新',
|
||||
avatarUploadFailed: '上传失败'
|
||||
},
|
||||
roles: {
|
||||
fashionEnthusiast: '时尚爱好者',
|
||||
|
||||
99
src/views/setting/AGENTS.md
Normal file
99
src/views/setting/AGENTS.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# AGENTS.md - `views/setting`
|
||||
|
||||
## Scope
|
||||
|
||||
This file applies to `src/views/setting/**`.
|
||||
|
||||
This directory implements the account settings page at route `/settings`. The route is cached in `src/router/index.ts` with `meta: { cache: true }`, so stateful changes should handle re-entry and refresh deliberately.
|
||||
|
||||
## Project Rules
|
||||
|
||||
- Do not start local services by default. Assume the project service is already running unless the user explicitly asks you to start one, or you first ask for permission because runtime verification requires it.
|
||||
- Keep the current Vue 3 style: Composition API, `<script setup lang="ts">`, typed props/emits, and scoped Less in SFCs.
|
||||
- Prefer `rg` for code search.
|
||||
- Keep edits focused on this feature. Do not refactor unrelated settings, router, API, or language files unless the setting page change requires it.
|
||||
|
||||
## Page Architecture
|
||||
|
||||
- `index.vue` is the route-level composition surface. Keep it thin: page shell, banner, section composition, and lifecycle wiring only.
|
||||
- `useSettingsForm.ts` owns settings state, API calls, save/discard behavior, email verification state, validation, and language synchronization.
|
||||
- `types.ts` owns setting-page domain types and option constants.
|
||||
- `components/SettingsSection.vue` provides the two-column section layout.
|
||||
- `components/ProfileSection.vue` renders profile fields, role selection, and avatar upload.
|
||||
- `components/SecuritySection.vue` renders email/password editing controls and emits security actions upward.
|
||||
- `components/RegionSection.vue` renders language and region selectors.
|
||||
- `components/SettingsActions.vue` renders edit/save/verify/discard actions.
|
||||
- `components/EmailVerificationDialog.vue` owns the verification-code modal UI, countdown, local code input, and submit/resend events.
|
||||
- `components/Radio.vue` is a local button-based single/multiple select control. For role selection, it is used with `multiple` and `max="2"`.
|
||||
|
||||
## Data Flow
|
||||
|
||||
- Preserve the `sourceData` / `draftData` split in `useSettingsForm.ts`.
|
||||
- `sourceData` is the last loaded/saved server state.
|
||||
- `draftData` is the editable copy used while `isEditing` is true.
|
||||
- `displayData` switches between them based on `isEditing`.
|
||||
- Child components should receive data through props or `v-model:*` bindings and report changes with typed emits.
|
||||
- Do not let child components mutate parent objects directly.
|
||||
- Keep security-only draft fields in `securityDraft` (`newEmail`, `newPassword`, `currentPassword`) rather than mixing them into `draftData`.
|
||||
- `handleDiscard()` must reset profile draft, security draft, verification state, and section editing flags.
|
||||
|
||||
## API Contracts
|
||||
|
||||
The setting page currently depends on `src/api/user.ts`:
|
||||
|
||||
- `fetchUserProfile()` -> `/buyer/profile/getProfile`
|
||||
- `updateUserProfile(data)` -> `/buyer/profile/setProfile`
|
||||
- `updateUserAvatar({ avatarUrl })` -> `/buyer/profile/setAvatar`
|
||||
- `uploadFile(file)` -> `/buyer/profile/upload`
|
||||
- `fetchVerifyCode()` -> `/buyer/profile/sendEmailChangeCode`
|
||||
- `verifyEmailCode(verifyCode)` -> `/buyer/profile/verifyEmailChangeCode`
|
||||
|
||||
When saving settings:
|
||||
|
||||
- Trim text fields before building the payload.
|
||||
- Send `roles` as `string[]`.
|
||||
- Send `verifyCode` only when email or password is being changed.
|
||||
- Hash `oldPassword` and `newPassword` with `md5` only when changing password.
|
||||
- Keep email/password verification behavior centralized in `useSettingsForm.ts`; child components should only emit intent.
|
||||
|
||||
## Language And Region
|
||||
|
||||
- Frontend setting values are `english` and `chinese`.
|
||||
- i18n locale values are `ENGLISH` and `CHINESE_SIMPLIFIED`.
|
||||
- Backend language values are `en` and `zh-CN`.
|
||||
- Keep all language mapping in `useSettingsForm.ts` unless it becomes shared with another feature.
|
||||
- After saving a changed language, update `locale.value` and `localStorage.language`.
|
||||
- Regions come from `src/utils/area.ts`; display labels are translated through `area.<key>` in `src/lang/en.ts` and `src/lang/zh-cn.ts`.
|
||||
- When adding a language, role, message, or region label, update both language files.
|
||||
|
||||
## Validation And UX
|
||||
|
||||
- Email changes require a valid email format and a successful verification before save.
|
||||
- Password changes use `validateLength`, `validateSpecial`, and `validateCase` from `src/views/login/tools`.
|
||||
- New password must not equal the current password.
|
||||
- Password changes also require verification using the current account email.
|
||||
- `SettingsActions.vue` intentionally shows the Verify Email action before Save when `needsEmailVerification` is true.
|
||||
- `EmailVerificationDialog.vue` owns its 60-second resend countdown and clears timers on close/unmount.
|
||||
- Avoid adding visible instructional copy unless the product requirement asks for it; most labels and tips already come from i18n.
|
||||
|
||||
## Styling
|
||||
|
||||
- Existing layout uses fixed `rem` sizing, Kaisei font classes, white/gray/black styling, and Element Plus controls restyled with `:deep`.
|
||||
- Keep SFC styles scoped.
|
||||
- Reuse local Less mixins such as `.field-text()`, `.field-frame()`, and `.control-wrapper()` within components when extending matching controls.
|
||||
- Preserve the two-column settings layout: left label/description and right content area.
|
||||
- Be careful with responsive changes: this page currently has large fixed widths and desktop-oriented spacing.
|
||||
|
||||
## Known Maintenance Notes
|
||||
|
||||
- `RegionSection.vue` imports `RegionValue` from `../types`, but `types.ts` currently does not export `RegionValue`. If touching region typing, define/export a proper region type instead of using `any`.
|
||||
- `ProfileSection.vue` currently includes `console.log(file)` and `debugger` in avatar upload. Remove these when working on avatar upload.
|
||||
- `ProfileSection.vue` reads `avatarUrl` through `(displayData as any)` even though `SettingsData` does not include it. If avatar display is changed, type the field explicitly.
|
||||
- Some verification button markup in `SecuritySection.vue` is commented out. Prefer either restoring it intentionally or deleting stale comments when working in that area.
|
||||
|
||||
## Verification
|
||||
|
||||
- For type-only or state-flow changes, run `npm run type-check` when feasible.
|
||||
- For production-impacting changes, run `npm run build-typeCheck` when feasible.
|
||||
- Do not start `npm run dev` unless the user explicitly asks or has approved runtime verification.
|
||||
- If you cannot run checks, explain why in the final response.
|
||||
268
src/views/setting/components/AvatarCropDialog.vue
Normal file
268
src/views/setting/components/AvatarCropDialog.vue
Normal file
@@ -0,0 +1,268 @@
|
||||
<template>
|
||||
<div v-if="visible" class="avatar-crop-dialog" @click.self="handleCancel">
|
||||
<div class="avatar-crop-dialog__panel">
|
||||
<button type="button" class="avatar-crop-dialog__close" @click="handleCancel">
|
||||
<SvgIcon name="close" size="24" />
|
||||
</button>
|
||||
|
||||
<div class="avatar-crop-dialog__title">{{ t('Settings.avatarCrop.title') }}</div>
|
||||
|
||||
<div class="avatar-crop-dialog__body">
|
||||
<div class="cropper-shell">
|
||||
<VueCropper
|
||||
ref="cropperRef"
|
||||
:img="imageUrl"
|
||||
:wrapper="{ width: 420, height: 420 }"
|
||||
:crop-layout="{ width: 260, height: 260 }"
|
||||
:center-box="true"
|
||||
:output-type="outputType"
|
||||
:output-size="0.92"
|
||||
mode="cover"
|
||||
crop-color="#ffffff"
|
||||
@real-time="handlePreview"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="preview-column">
|
||||
<div class="avatar-preview">
|
||||
<img
|
||||
v-if="previewUrl"
|
||||
:src="previewUrl"
|
||||
:style="previewImageStyle"
|
||||
alt="avatar preview"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="cropper-tools">
|
||||
<button type="button" class="tool-btn" @click="cropperRef?.zoomOut?.()">
|
||||
-
|
||||
</button>
|
||||
<button type="button" class="tool-btn" @click="cropperRef?.zoomIn?.()">
|
||||
+
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="avatar-crop-dialog__actions">
|
||||
<button type="button" class="secondary-btn" :disabled="submitting" @click="handleCancel">
|
||||
{{ t('Settings.buttons.cancel') }}
|
||||
</button>
|
||||
<button type="button" class="primary-btn" :disabled="submitting" @click="handleConfirm">
|
||||
{{ submitting ? t('Settings.avatarCrop.processing') : t('Settings.avatarCrop.confirm') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, shallowRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { VueCropper } from 'cropper-next-vue'
|
||||
import 'cropper-next-vue/style.css'
|
||||
|
||||
type CropperInstance = InstanceType<typeof VueCropper> & {
|
||||
getCropBlob?: () => Promise<Blob>
|
||||
zoomIn?: (step?: number) => void
|
||||
zoomOut?: (step?: number) => void
|
||||
rotateLeft?: () => void
|
||||
rotateRight?: () => void
|
||||
}
|
||||
|
||||
interface PreviewPayload {
|
||||
url: string
|
||||
img: Record<string, string>
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
visible: boolean
|
||||
imageUrl: string
|
||||
fileName: string
|
||||
fileType: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'cancel'): void
|
||||
(event: 'confirm', file: File): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const cropperRef = shallowRef<CropperInstance | null>(null)
|
||||
const submitting = shallowRef(false)
|
||||
const previewUrl = shallowRef('')
|
||||
const previewImageStyle = shallowRef<Record<string, string>>({})
|
||||
|
||||
const outputType = computed(() => {
|
||||
const subtype = props.fileType.split('/')[1]
|
||||
return subtype === 'jpeg' || subtype === 'jpg' || subtype === 'webp' ? subtype : 'png'
|
||||
})
|
||||
|
||||
const cropFileType = computed(() =>
|
||||
outputType.value === 'jpg' ? 'image/jpeg' : `image/${outputType.value}`
|
||||
)
|
||||
|
||||
const cropFileName = computed(() => {
|
||||
const baseName = props.fileName.replace(/\.[^.]+$/, '') || 'avatar'
|
||||
const extension = outputType.value === 'jpeg' ? 'jpg' : outputType.value
|
||||
return `${baseName}-cropped.${extension}`
|
||||
})
|
||||
|
||||
const handlePreview = (payload: PreviewPayload) => {
|
||||
previewUrl.value = payload.url
|
||||
previewImageStyle.value = payload.img
|
||||
}
|
||||
|
||||
const handleCancel = () => {
|
||||
if (submitting.value) return
|
||||
emit('cancel')
|
||||
}
|
||||
|
||||
const handleConfirm = async () => {
|
||||
if (!cropperRef.value?.getCropBlob) return
|
||||
|
||||
submitting.value = true
|
||||
try {
|
||||
const blob = await cropperRef.value.getCropBlob()
|
||||
const file = new File([blob], cropFileName.value, {
|
||||
type: cropFileType.value,
|
||||
lastModified: Date.now()
|
||||
})
|
||||
emit('confirm', file)
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
ElMessage.error(t('Settings.messages.avatarCropFailed'))
|
||||
} finally {
|
||||
submitting.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.avatar-crop-dialog {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 2000;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.avatar-crop-dialog__panel {
|
||||
position: relative;
|
||||
width: 78rem;
|
||||
padding: 4rem 4.8rem 3.6rem;
|
||||
background: #efefef;
|
||||
box-shadow: 0 2rem 6rem rgba(35, 35, 35, 0.14);
|
||||
}
|
||||
|
||||
.avatar-crop-dialog__close {
|
||||
position: absolute;
|
||||
top: 2rem;
|
||||
right: 2rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
color: #585858;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.avatar-crop-dialog__title {
|
||||
font-family: 'KaiseiOpti-Bold';
|
||||
font-size: 3rem;
|
||||
line-height: 3.6rem;
|
||||
text-align: center;
|
||||
color: #232323;
|
||||
}
|
||||
|
||||
.avatar-crop-dialog__body {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 3.2rem;
|
||||
margin-top: 3.2rem;
|
||||
}
|
||||
|
||||
.cropper-shell {
|
||||
width: 42rem;
|
||||
height: 42rem;
|
||||
overflow: hidden;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.preview-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
row-gap: 2.4rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.avatar-preview {
|
||||
width: 260px;
|
||||
height: 260px;
|
||||
overflow: hidden;
|
||||
border: 0.1rem solid #d8d0c7;
|
||||
border-radius: 50%;
|
||||
background: #ffffff;
|
||||
|
||||
img {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
.cropper-tools {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.tool-btn,
|
||||
.primary-btn,
|
||||
.secondary-btn {
|
||||
height: 4rem;
|
||||
border: 0.1rem solid #c4c4c4;
|
||||
background: #ffffff;
|
||||
font-family: 'KaiseiOpti-Bold';
|
||||
font-size: 1.4rem;
|
||||
line-height: 2.4rem;
|
||||
color: #232323;
|
||||
cursor: pointer;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.tool-btn {
|
||||
min-width: 5.2rem;
|
||||
padding: 0 1.2rem;
|
||||
}
|
||||
|
||||
.avatar-crop-dialog__actions {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
column-gap: 1.2rem;
|
||||
margin-top: 3.2rem;
|
||||
}
|
||||
|
||||
.primary-btn {
|
||||
width: 18rem;
|
||||
border-color: #232323;
|
||||
background: #232323;
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.secondary-btn {
|
||||
width: 12rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,235 +1,359 @@
|
||||
<template>
|
||||
<SettingsSection
|
||||
:title="t('Settings.profile.title')"
|
||||
:description="t('Settings.profile.description')"
|
||||
>
|
||||
<div class="profile-header">
|
||||
<div class="avatar relative">
|
||||
<SvgIcon name="user_0" size="46" class="avatar-icon" />
|
||||
<img src="@/assets/images/edit.png" class="avatar-edit-icon" />
|
||||
</div>
|
||||
<div class="profile-summary">
|
||||
<div class="profile-name">{{ fullName }}</div>
|
||||
<div class="profile-email">{{ displayData.email }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<SettingsSection
|
||||
:title="t('Settings.profile.title')"
|
||||
:description="t('Settings.profile.description')"
|
||||
>
|
||||
<div class="profile-header">
|
||||
<div class="avatar relative" @click="handleEditAvatar">
|
||||
<img
|
||||
v-if="displayData.avatarUrl"
|
||||
:src="displayData.avatarUrl"
|
||||
alt="avatar"
|
||||
class="avatar-img"
|
||||
/>
|
||||
<SvgIcon v-else name="user_0" size="46" class="avatar-icon" />
|
||||
<img src="@/assets/images/edit.png" class="avatar-edit-icon" />
|
||||
<input
|
||||
type="file"
|
||||
ref="fileInput"
|
||||
style="display:none"
|
||||
accept="image/*"
|
||||
@change="handleFileChange"
|
||||
/>
|
||||
</div>
|
||||
<div class="profile-summary">
|
||||
<div class="profile-name">{{ fullName }}</div>
|
||||
<div class="profile-email">{{ displayData.email }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="read-section">
|
||||
<div class="read-row two-column">
|
||||
<div class="read-item">
|
||||
<div class="read-label">{{ t('Settings.profile.firstName') }}</div>
|
||||
<div v-show="!isEditing" class="read-box">{{ displayData.firstName }}</div>
|
||||
<div v-show="isEditing" class="form-item-value name">
|
||||
<el-input
|
||||
:model-value="firstName"
|
||||
:placeholder="t('Settings.profile.firstNamePlaceholder')"
|
||||
@update:model-value="emit('update:firstName', String($event))"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="read-item">
|
||||
<div class="read-label">{{ t('Settings.profile.lastName') }}</div>
|
||||
<div v-show="!isEditing" class="read-box">{{ displayData.lastName }}</div>
|
||||
<div v-show="isEditing" class="form-item-value name">
|
||||
<el-input
|
||||
:model-value="lastName"
|
||||
:placeholder="t('Settings.profile.lastNamePlaceholder')"
|
||||
@update:model-value="emit('update:lastName', String($event))"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="read-section">
|
||||
<div class="read-row two-column">
|
||||
<div class="read-item">
|
||||
<div class="read-label">{{ t('Settings.profile.firstName') }}</div>
|
||||
<div v-show="!isEditing" class="read-box">{{ displayData.firstName }}</div>
|
||||
<div v-show="isEditing" class="form-item-value name">
|
||||
<el-input
|
||||
:model-value="firstName"
|
||||
:placeholder="t('Settings.profile.firstNamePlaceholder')"
|
||||
@update:model-value="emit('update:firstName', String($event))"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="read-item">
|
||||
<div class="read-label">{{ t('Settings.profile.lastName') }}</div>
|
||||
<div v-show="!isEditing" class="read-box">{{ displayData.lastName }}</div>
|
||||
<div v-show="isEditing" class="form-item-value name">
|
||||
<el-input
|
||||
:model-value="lastName"
|
||||
:placeholder="t('Settings.profile.lastNamePlaceholder')"
|
||||
@update:model-value="emit('update:lastName', String($event))"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="read-row">
|
||||
<div class="read-label">{{ t('Settings.profile.username') }}</div>
|
||||
<div v-show="!isEditing" class="read-box">{{ displayData.username }}</div>
|
||||
<div v-show="isEditing" class="form-item-value">
|
||||
<el-input
|
||||
:model-value="username"
|
||||
:placeholder="t('Settings.profile.usernamePlaceholder')"
|
||||
@update:model-value="emit('update:username', String($event))"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="read-tip">{{ t('Settings.profile.usernameTip') }}</div>
|
||||
<div class="read-row">
|
||||
<div class="read-label">{{ t('Settings.profile.username') }}</div>
|
||||
<div v-show="!isEditing" class="read-box">{{ displayData.username }}</div>
|
||||
<div v-show="isEditing" class="form-item-value">
|
||||
<el-input
|
||||
:model-value="username"
|
||||
:placeholder="t('Settings.profile.usernamePlaceholder')"
|
||||
@update:model-value="emit('update:username', String($event))"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="read-tip">{{ t('Settings.profile.usernameTip') }}</div>
|
||||
|
||||
<div class="read-row role-row">
|
||||
<div class="read-label">{{ t('Settings.profile.role') }}</div>
|
||||
<div :class="{ 'readonly-radio-group': !isEditing }">
|
||||
<Radio multiple :max="2" v-model="roleSelection" :options="roleOptions" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="read-tip">{{ t('Settings.profile.roleTip') }}</div>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
<div class="read-row role-row">
|
||||
<div class="read-label">{{ t('Settings.profile.role') }}</div>
|
||||
<div :class="{ 'readonly-radio-group': !isEditing }">
|
||||
<Radio multiple :max="2" v-model="roleSelection" :options="roleOptions" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="read-tip">{{ t('Settings.profile.roleTip') }}</div>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
|
||||
<AvatarCropDialog
|
||||
:visible="isAvatarCropDialogVisible"
|
||||
:image-url="selectedAvatarImageUrl"
|
||||
:file-name="selectedAvatarFile?.name || 'avatar.png'"
|
||||
:file-type="selectedAvatarFile?.type || 'image/png'"
|
||||
@cancel="closeAvatarCropDialog"
|
||||
@confirm="handleAvatarCropConfirm"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import SettingsSection from './SettingsSection.vue'
|
||||
import Radio from './Radio.vue'
|
||||
import type { RoleOption, RoleValue, SettingsData } from '../types'
|
||||
import { computed, onBeforeUnmount, ref, shallowRef } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import SettingsSection from './SettingsSection.vue'
|
||||
import Radio from './Radio.vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import AvatarCropDialog from './AvatarCropDialog.vue'
|
||||
import type { RoleOption, RoleValue, SettingsData } from '../types'
|
||||
import { uploadFile, updateUserAvatar } from '@/api/user'
|
||||
|
||||
const props = defineProps<{
|
||||
displayData: SettingsData
|
||||
firstName: string
|
||||
lastName: string
|
||||
username: string
|
||||
fullName: string
|
||||
isEditing: boolean
|
||||
roleModel: RoleValue[]
|
||||
roleOptions: RoleOption[]
|
||||
}>()
|
||||
const props = defineProps<{
|
||||
displayData: SettingsData
|
||||
firstName: string
|
||||
lastName: string
|
||||
username: string
|
||||
fullName: string
|
||||
isEditing: boolean
|
||||
roleModel: RoleValue[]
|
||||
roleOptions: RoleOption[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:firstName', value: string): void
|
||||
(event: 'update:lastName', value: string): void
|
||||
(event: 'update:username', value: string): void
|
||||
(event: 'update:roleModel', value: RoleValue[]): void
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:firstName', value: string): void
|
||||
(event: 'update:lastName', value: string): void
|
||||
(event: 'update:username', value: string): void
|
||||
(event: 'update:roleModel', value: RoleValue[]): void
|
||||
(event: 'avatar-updated', url: string): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const { t } = useI18n()
|
||||
|
||||
const roleSelection = computed<RoleValue[]>({
|
||||
get: () => props.roleModel,
|
||||
set: (value) => {
|
||||
emit('update:roleModel', value)
|
||||
}
|
||||
})
|
||||
const roleSelection = computed<RoleValue[]>({
|
||||
get: () => props.roleModel,
|
||||
set: (value) => {
|
||||
emit('update:roleModel', value)
|
||||
}
|
||||
})
|
||||
|
||||
const fileInput = ref<HTMLInputElement | null>(null)
|
||||
const isAvatarCropDialogVisible = shallowRef(false)
|
||||
const selectedAvatarFile = shallowRef<File | null>(null)
|
||||
const selectedAvatarImageUrl = shallowRef('')
|
||||
|
||||
const handleEditAvatar = () => {
|
||||
fileInput.value?.click()
|
||||
}
|
||||
|
||||
const resetFileInput = () => {
|
||||
if (fileInput.value) {
|
||||
fileInput.value.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const revokeSelectedAvatarImageUrl = () => {
|
||||
if (!selectedAvatarImageUrl.value) return
|
||||
|
||||
URL.revokeObjectURL(selectedAvatarImageUrl.value)
|
||||
selectedAvatarImageUrl.value = ''
|
||||
}
|
||||
|
||||
const closeAvatarCropDialog = () => {
|
||||
isAvatarCropDialogVisible.value = false
|
||||
selectedAvatarFile.value = null
|
||||
revokeSelectedAvatarImageUrl()
|
||||
resetFileInput()
|
||||
}
|
||||
|
||||
const getUploadedFileUrl = (response: unknown): string | undefined => {
|
||||
if (typeof response === 'string') return response
|
||||
|
||||
const data = response as Record<string, unknown> | null
|
||||
if (!data) return undefined
|
||||
|
||||
if (typeof data.url === 'string') return data.url
|
||||
if (typeof data.fileUrl === 'string') return data.fileUrl
|
||||
const nestedData = data.data as Record<string, unknown> | undefined
|
||||
if (nestedData && typeof nestedData.url === 'string') return nestedData.url
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const uploadAvatarFile = async (file: File) => {
|
||||
try {
|
||||
const res = await uploadFile(file)
|
||||
const url = getUploadedFileUrl(res)
|
||||
|
||||
if (!url) {
|
||||
ElMessage.error(t('Settings.messages.avatarUploadUrlMissing'))
|
||||
return
|
||||
}
|
||||
|
||||
await updateUserAvatar({ avatarUrl: url })
|
||||
ElMessage.success(t('Settings.messages.avatarUpdated'))
|
||||
emit('avatar-updated', url)
|
||||
} catch (err) {
|
||||
console.warn(err)
|
||||
ElMessage.error(t('Settings.messages.avatarUploadFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
const handleFileChange = async (e: Event) => {
|
||||
const input = e.target as HTMLInputElement
|
||||
const file = input.files && input.files[0]
|
||||
if (!file) return
|
||||
|
||||
// optional: limit file size to 5MB
|
||||
const maxSize = 5 * 1024 * 1024
|
||||
if (file.size > maxSize) {
|
||||
ElMessage.warning(t('Settings.messages.avatarTooLarge'))
|
||||
input.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
revokeSelectedAvatarImageUrl()
|
||||
selectedAvatarFile.value = file
|
||||
selectedAvatarImageUrl.value = URL.createObjectURL(file)
|
||||
isAvatarCropDialogVisible.value = true
|
||||
}
|
||||
|
||||
const handleAvatarCropConfirm = async (file: File) => {
|
||||
closeAvatarCropDialog()
|
||||
await uploadAvatarFile(file)
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
revokeSelectedAvatarImageUrl()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.field-text() {
|
||||
font-family: 'KaiseiOpti-Regular';
|
||||
font-size: 1.6rem;
|
||||
line-height: 2.4rem;
|
||||
color: #232323;
|
||||
}
|
||||
.field-text() {
|
||||
font-family: 'KaiseiOpti-Regular';
|
||||
font-size: 1.6rem;
|
||||
line-height: 2.4rem;
|
||||
color: #232323;
|
||||
}
|
||||
|
||||
.field-frame() {
|
||||
width: 100%;
|
||||
min-height: 4rem;
|
||||
border: 0.1rem solid #979797;
|
||||
}
|
||||
.field-frame() {
|
||||
width: 100%;
|
||||
min-height: 4rem;
|
||||
border: 0.1rem solid #979797;
|
||||
}
|
||||
|
||||
.control-wrapper() {
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
.control-wrapper() {
|
||||
box-shadow: none;
|
||||
border-radius: 0;
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
.profile-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 2.6rem;
|
||||
margin-bottom: 3.6rem;
|
||||
}
|
||||
.profile-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
column-gap: 2.6rem;
|
||||
margin-bottom: 3.6rem;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 8rem;
|
||||
height: 8rem;
|
||||
border-radius: 50%;
|
||||
border: 0.1rem solid #d8d0c7;
|
||||
}
|
||||
.avatar {
|
||||
width: 8rem;
|
||||
height: 8rem;
|
||||
border-radius: 50%;
|
||||
border: 0.1rem solid #d8d0c7;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.avatar-edit-icon {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border: 0.1rem solid #fff;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
.avatar-img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.profile-summary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 0.6rem;
|
||||
}
|
||||
.avatar-edit-icon {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
border: 0.1rem solid #fff;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.profile-name {
|
||||
font-family: 'KaiseiOpti-Medium';
|
||||
font-size: 2.4rem;
|
||||
line-height: 3.6rem;
|
||||
color: #232323;
|
||||
}
|
||||
.profile-summary {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
row-gap: 0.6rem;
|
||||
}
|
||||
|
||||
.profile-email {
|
||||
font-family: 'KaiseiOpti-Regular';
|
||||
font-size: 1.8rem;
|
||||
line-height: 2.4rem;
|
||||
color: #979797;
|
||||
}
|
||||
.profile-name {
|
||||
font-family: 'KaiseiOpti-Medium';
|
||||
font-size: 2.4rem;
|
||||
line-height: 3.6rem;
|
||||
color: #232323;
|
||||
}
|
||||
|
||||
.read-section {
|
||||
width: 58rem;
|
||||
}
|
||||
.profile-email {
|
||||
font-family: 'KaiseiOpti-Regular';
|
||||
font-size: 1.8rem;
|
||||
line-height: 2.4rem;
|
||||
color: #979797;
|
||||
}
|
||||
|
||||
.read-label {
|
||||
margin-bottom: 0.8rem;
|
||||
font-family: 'KaiseiOpti-Medium';
|
||||
font-size: 1.4rem;
|
||||
line-height: 2.4rem;
|
||||
letter-spacing: 0.04em;
|
||||
color: #585858;
|
||||
}
|
||||
.read-section {
|
||||
width: 58rem;
|
||||
}
|
||||
|
||||
.read-tip {
|
||||
margin-top: 0.8rem;
|
||||
font-family: 'KaiseiOpti-Regular';
|
||||
font-size: 1.2rem;
|
||||
line-height: 1.6rem;
|
||||
color: #9f9f9f;
|
||||
}
|
||||
.read-label {
|
||||
margin-bottom: 0.8rem;
|
||||
font-family: 'KaiseiOpti-Medium';
|
||||
font-size: 1.4rem;
|
||||
line-height: 2.4rem;
|
||||
letter-spacing: 0.04em;
|
||||
color: #585858;
|
||||
}
|
||||
|
||||
.read-row + .read-row {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
.read-tip {
|
||||
margin-top: 0.8rem;
|
||||
font-family: 'KaiseiOpti-Regular';
|
||||
font-size: 1.2rem;
|
||||
line-height: 1.6rem;
|
||||
color: #9f9f9f;
|
||||
}
|
||||
|
||||
.read-row.two-column {
|
||||
display: grid;
|
||||
grid-template-columns: 28.4rem 28.4rem;
|
||||
column-gap: 2rem;
|
||||
}
|
||||
.read-row + .read-row {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
|
||||
.read-box {
|
||||
.field-frame();
|
||||
.field-text();
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.8rem 2rem;
|
||||
}
|
||||
.read-row.two-column {
|
||||
display: grid;
|
||||
grid-template-columns: 28.4rem 28.4rem;
|
||||
column-gap: 2rem;
|
||||
}
|
||||
|
||||
.form-item-value {
|
||||
.field-frame();
|
||||
.read-box {
|
||||
.field-frame();
|
||||
.field-text();
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.8rem 2rem;
|
||||
}
|
||||
|
||||
&.name {
|
||||
width: 28.4rem;
|
||||
}
|
||||
.form-item-value {
|
||||
.field-frame();
|
||||
|
||||
:deep(.el-input) {
|
||||
width: 100%;
|
||||
min-height: 4rem;
|
||||
}
|
||||
&.name {
|
||||
width: 28.4rem;
|
||||
}
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
.control-wrapper();
|
||||
min-height: 4rem;
|
||||
}
|
||||
:deep(.el-input) {
|
||||
width: 100%;
|
||||
min-height: 4rem;
|
||||
}
|
||||
|
||||
:deep(.el-input__inner) {
|
||||
.field-text();
|
||||
}
|
||||
}
|
||||
:deep(.el-input__wrapper) {
|
||||
.control-wrapper();
|
||||
min-height: 4rem;
|
||||
}
|
||||
|
||||
.readonly-radio-group {
|
||||
pointer-events: none;
|
||||
}
|
||||
:deep(.el-input__inner) {
|
||||
.field-text();
|
||||
}
|
||||
}
|
||||
|
||||
.role-row {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
.readonly-radio-group {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.role-row {
|
||||
margin-top: 3rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -50,16 +50,16 @@
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import SettingsSection from './SettingsSection.vue'
|
||||
import type { LanguageValue, RegionValue, SettingOption } from '../types'
|
||||
import type { LanguageValue, RegionOption, RegionValue, SettingOption } from '../types'
|
||||
|
||||
defineProps<{
|
||||
language: LanguageValue
|
||||
language: LanguageValue | ''
|
||||
region: RegionValue
|
||||
displayLanguageLabel: string
|
||||
displayRegionLabel: string
|
||||
isEditing: boolean
|
||||
languageOptions: SettingOption<LanguageValue>[]
|
||||
regionOptions: SettingOption<RegionValue>[]
|
||||
regionOptions: RegionOption[]
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
:full-name="fullName"
|
||||
:is-editing="isEditing"
|
||||
:role-options="roleList"
|
||||
@avatar-updated="loadUserProfile"
|
||||
/>
|
||||
|
||||
<div class="gap" />
|
||||
|
||||
@@ -17,6 +17,7 @@ export const languageValues = ['english', 'chinese'] as const
|
||||
|
||||
export type RoleValue = (typeof roleValues)[number]
|
||||
export type LanguageValue = (typeof languageValues)[number]
|
||||
export type RegionValue = string
|
||||
|
||||
export interface SettingsData {
|
||||
firstName: string
|
||||
@@ -26,6 +27,7 @@ export interface SettingsData {
|
||||
roles: RoleValue[]
|
||||
language: LanguageValue | ''
|
||||
region: string | ''
|
||||
avatarUrl?: string
|
||||
}
|
||||
|
||||
export interface SecurityDraft {
|
||||
@@ -43,3 +45,7 @@ export interface SettingOption<T extends string> {
|
||||
label: string
|
||||
value: T
|
||||
}
|
||||
|
||||
export interface RegionOption extends SettingOption<RegionValue> {
|
||||
key: string
|
||||
}
|
||||
|
||||
@@ -46,6 +46,9 @@ const backendToFrontendLanguage: Record<'en' | 'zh-CN', LanguageValue> = {
|
||||
|
||||
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
|
||||
const isRoleValue = (value: string): value is RoleValue =>
|
||||
(roleValues as readonly string[]).includes(value)
|
||||
|
||||
const createDefaultData = (): SettingsData => ({
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
@@ -53,7 +56,8 @@ const createDefaultData = (): SettingsData => ({
|
||||
username: '',
|
||||
roles: [] as RoleValue[],
|
||||
language: '',
|
||||
region: ''
|
||||
region: '',
|
||||
avatarUrl: ''
|
||||
})
|
||||
|
||||
const cloneSettingsData = (data: SettingsData): SettingsData => ({
|
||||
@@ -63,7 +67,8 @@ const cloneSettingsData = (data: SettingsData): SettingsData => ({
|
||||
username: data.username,
|
||||
roles: [...data.roles],
|
||||
language: data.language,
|
||||
region: data.region
|
||||
region: data.region,
|
||||
avatarUrl: data.avatarUrl
|
||||
})
|
||||
|
||||
const normalizeLanguage = (language: string | null | undefined): LanguageValue => {
|
||||
@@ -87,9 +92,10 @@ const buildSettingsDataFromProfile = (profile: Partial<UserProfile>): SettingsDa
|
||||
lastName: profile.lastName || '',
|
||||
username: profile.username || '',
|
||||
email: profile.email || '',
|
||||
roles: profile.roles || [],
|
||||
roles: (profile.roles || []).filter(isRoleValue),
|
||||
language: normalizeLanguage(profile.language),
|
||||
region: profile.region
|
||||
region: profile.region || '',
|
||||
avatarUrl: profile.avatarUrl || ''
|
||||
})
|
||||
|
||||
const createEmptySecurityDraft = (): SecurityDraft => ({
|
||||
|
||||
@@ -281,7 +281,6 @@
|
||||
}
|
||||
|
||||
const handleClickAction = (order) => {
|
||||
console.log(order)
|
||||
const list = []
|
||||
order.items.forEach((item) => {
|
||||
list.push({
|
||||
@@ -295,7 +294,6 @@
|
||||
})
|
||||
})
|
||||
|
||||
console.log(list)
|
||||
const params = btoa(encodeURIComponent(JSON.stringify(list)))
|
||||
ROUTER.push({
|
||||
name: 'pay',
|
||||
|
||||
Reference in New Issue
Block a user