Compare commits
160 Commits
09909552bc
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 5e10552d41 | |||
| 0f6725262b | |||
|
|
006fa85306 | ||
|
|
eb153825c3 | ||
| 73dbd34e0a | |||
| 7814e870f3 | |||
| 46d1b0e381 | |||
|
|
35c3c3aed4 | ||
| c9a848a5be | |||
| 4747489d84 | |||
|
|
5e39f5d2af | ||
|
|
e3607e43ac | ||
| 6e879123ab | |||
| 437b191b12 | |||
| aa672194ea | |||
| 8ab14e88f1 | |||
| 275d4ad0d0 | |||
|
|
d789edcc80 | ||
|
|
8cc646f4b8 | ||
|
|
e64dbd8925 | ||
| bcb04aedd7 | |||
| 1c80ba9fbc | |||
| 6b214d2abc | |||
| fb767218c4 | |||
| 29400bb23b | |||
|
|
aa7f6724b6 | ||
|
|
4aba2fc0a4 | ||
|
|
33d126bac9 | ||
|
|
e68fb8afc7 | ||
|
|
2c5ef21edb | ||
|
|
15dd98a555 | ||
|
|
792db5bff9 | ||
|
|
310e514206 | ||
| 087f8f1096 | |||
| 480c1baa4a | |||
|
|
cf0791dec7 | ||
| 4f0d17ea29 | |||
| 240b006a8f | |||
|
|
d3da32d4a6 | ||
|
|
1c26f6ae28 | ||
|
|
7e0abc7d16 | ||
|
|
ff36a56002 | ||
| 44dea76108 | |||
| 8321871e18 | |||
|
|
40132cebce | ||
|
|
183e7e551e | ||
|
|
7f08c52a98 | ||
|
|
c004308f77 | ||
|
|
c32bd02efe | ||
|
|
277457d42a | ||
|
|
f52594efdd | ||
| 16347514b5 | |||
| 8b0bcdc8ac | |||
|
|
edb8e7a4bf | ||
|
|
bf5c27d857 | ||
|
|
a4494831a1 | ||
| ad0f4f65e5 | |||
| 052764166f | |||
|
|
fdc5f9d095 | ||
|
|
965721bbdd | ||
|
|
88a5618976 | ||
| 6c4ebc26d1 | |||
| 6ac5eb9664 | |||
| 74b6045220 | |||
|
|
d90b788155 | ||
|
|
cc100caa38 | ||
|
|
b66b8df8a3 | ||
|
|
a0b31f9374 | ||
| 1cc85f1c46 | |||
| 484d15f91d | |||
|
|
8209a1d678 | ||
|
|
b7250c51bf | ||
|
|
fe04ee3d1a | ||
|
|
c3aebf85c1 | ||
|
|
2f0f875080 | ||
|
|
83cf98c8b0 | ||
| a28636d25c | |||
| b47f0b8296 | |||
|
|
2c29d2ad38 | ||
|
|
fa73f949d5 | ||
| 9948b18c26 | |||
| f203d6146c | |||
| e45fd1135c | |||
| 4e49584c60 | |||
| bc8e436c94 | |||
| 881f25ac2b | |||
|
|
242c1ae8e8 | ||
|
|
a55e6b3ea9 | ||
|
|
7057a6c8f7 | ||
|
|
7df8b38966 | ||
|
|
729755efdf | ||
|
|
b8243128d6 | ||
|
|
5ada85d05c | ||
| 8f9511966e | |||
| 248bf6b1eb | |||
| e4f1c535a7 | |||
|
|
8f23b4c387 | ||
| a4861da21a | |||
|
|
d0224e5c6d | ||
|
|
91889102ea | ||
|
|
3da18cb246 | ||
|
|
0b014f2a3b | ||
|
|
8a99048639 | ||
| 3e64912804 | |||
| dfe8099945 | |||
|
|
fc5a361bba | ||
|
|
95556b1a1f | ||
|
|
3c39959ce3 | ||
|
|
22ebbba451 | ||
|
|
92c625fb89 | ||
|
|
a0fa8cd523 | ||
| c263f1cffc | |||
|
|
1ab121a703 | ||
| b8662fd379 | |||
|
|
bcbaa47926 | ||
|
|
a4441c6107 | ||
|
|
d0dc9b2af0 | ||
| ec4c3bc81f | |||
| dc6658e1dd | |||
| 0d10954251 | |||
|
|
f402c7b2aa | ||
|
|
0e28f089dc | ||
|
|
ab556a17e7 | ||
|
|
9b86a9f65e | ||
|
|
49cec0ee28 | ||
|
|
ecf53c8353 | ||
|
|
c8fa11aa25 | ||
| c576d050db | |||
| 2a64226f35 | |||
|
|
f6f62fde8e | ||
| 99f4515e9d | |||
| 4c22311e08 | |||
|
|
3ae774bc6b | ||
|
|
2d95c3c976 | ||
|
|
3de38d2856 | ||
|
|
2f1cc8d55d | ||
|
|
5d0de56c1a | ||
|
|
1b32002af5 | ||
|
|
59803cf2ea | ||
| d772cae6bc | |||
|
|
8c080d3e22 | ||
|
|
4be692bb29 | ||
|
|
7bfae7d024 | ||
|
|
e7957532e8 | ||
| 338ee24da2 | |||
| 81b907562e | |||
|
|
31de24cc2b | ||
|
|
c18b424f99 | ||
|
|
b9be27ab85 | ||
| 5476a1f69d | |||
| 9f620ab9d5 | |||
|
|
e418bf80ad | ||
|
|
2346e079a1 | ||
|
|
6772bf0e90 | ||
|
|
26dfbd9bb5 | ||
|
|
8c8ec7846d | ||
|
|
8d441766c5 | ||
|
|
bf907a1378 | ||
|
|
de6295f2af | ||
|
|
3e0a7b8928 |
@@ -1,3 +1,4 @@
|
||||
# VITE_APP_URL = http://192.168.31.82:8771
|
||||
# VITE_APP_URL = http://18.167.251.121:10095
|
||||
VITE_APP_URL = https://www.lc-api.aida.com.hk
|
||||
# VITE_APP_URL = http://192.168.31.82:10094
|
||||
VITE_APP_URL = https://www.develop-ms.api.aida.com.hk
|
||||
# WebSocket 主机地址
|
||||
VITE_WS_HOST = 18.167.251.121:10094
|
||||
@@ -1,2 +1,4 @@
|
||||
VITE_APP_URL = https://www.lc-api.aida.com.hk
|
||||
# VITE_APP_URL = http://18.167.251.121:10095
|
||||
VITE_APP_URL = http://192.168.31.82:10094
|
||||
VITE_APP_URL = https://www.develop-ms.api.aida.com.hk
|
||||
# WebSocket 主机地址
|
||||
VITE_WS_HOST = www.develop-ms.api.aida.com.hk
|
||||
@@ -1,8 +1,10 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": false,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"trailingComma": "none"
|
||||
}
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": false,
|
||||
"tabWidth": 4,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100,
|
||||
"useTabs": true,
|
||||
"trailingComma": "none",
|
||||
"vueIndentScriptAndStyle": true
|
||||
}
|
||||
|
||||
356
package-lock.json
generated
@@ -10,9 +10,11 @@
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"axios": "^1.3.6",
|
||||
"cropper-next-vue": "^0.3.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
"element-plus": "^2.13.2",
|
||||
"gsap": "^3.13.0",
|
||||
"lodash-es": "^4.18.1",
|
||||
"markdown-it": "^14.1.0",
|
||||
"md5": "^2.3.0",
|
||||
"normalize.css": "^8.0.1",
|
||||
@@ -20,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"
|
||||
},
|
||||
@@ -57,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"
|
||||
@@ -90,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"
|
||||
@@ -1161,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": {
|
||||
@@ -1186,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": {
|
||||
@@ -1266,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": {
|
||||
@@ -2152,6 +2154,18 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/cropper-next-vue": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/cropper-next-vue/-/cropper-next-vue-0.3.1.tgz",
|
||||
"integrity": "sha512-B80WRLPavJ/xAjDEofPWLEqnMy99yzXy+Kpak3wMFf+X/TqebvhvBL3fM0P5ZHLRq0v9k0qSAqFjukBhHc3XZA==",
|
||||
"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",
|
||||
@@ -2312,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"
|
||||
},
|
||||
@@ -4790,9 +4804,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash-es": {
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.23.tgz",
|
||||
"integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==",
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
|
||||
"integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash-unified": {
|
||||
@@ -5213,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",
|
||||
@@ -6038,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",
|
||||
@@ -6057,7 +6071,7 @@
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"nanoid": "^3.3.12",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
@@ -8390,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": "*"
|
||||
@@ -8733,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": {
|
||||
@@ -9427,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": {
|
||||
@@ -9506,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",
|
||||
@@ -10147,6 +10161,12 @@
|
||||
"vary": "^1"
|
||||
}
|
||||
},
|
||||
"cropper-next-vue": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/cropper-next-vue/-/cropper-next-vue-0.3.1.tgz",
|
||||
"integrity": "sha512-B80WRLPavJ/xAjDEofPWLEqnMy99yzXy+Kpak3wMFf+X/TqebvhvBL3fM0P5ZHLRq0v9k0qSAqFjukBhHc3XZA==",
|
||||
"requires": {}
|
||||
},
|
||||
"cross-spawn": {
|
||||
"version": "7.0.3",
|
||||
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.3.tgz",
|
||||
@@ -10259,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": {
|
||||
@@ -12111,9 +12131,9 @@
|
||||
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w=="
|
||||
},
|
||||
"lodash-es": {
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.23.tgz",
|
||||
"integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg=="
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
|
||||
"integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A=="
|
||||
},
|
||||
"lodash-unified": {
|
||||
"version": "1.0.3",
|
||||
@@ -12438,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",
|
||||
@@ -13050,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"
|
||||
}
|
||||
@@ -14816,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,9 +14,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^1.3.6",
|
||||
"cropper-next-vue": "^0.3.1",
|
||||
"crypto-js": "^4.2.0",
|
||||
"element-plus": "^2.13.2",
|
||||
"gsap": "^3.13.0",
|
||||
"lodash-es": "^4.18.1",
|
||||
"markdown-it": "^14.1.0",
|
||||
"md5": "^2.3.0",
|
||||
"normalize.css": "^8.0.1",
|
||||
@@ -24,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"
|
||||
},
|
||||
|
||||
BIN
public/images/home/digital-items-1.png
Normal file
|
After Width: | Height: | Size: 201 KiB |
BIN
public/images/home/digital-items-2.jpg
Normal file
|
After Width: | Height: | Size: 176 KiB |
BIN
public/images/home/digital-items-3.png
Normal file
|
After Width: | Height: | Size: 622 KiB |
BIN
public/images/home/digital-items-4.png
Normal file
|
After Width: | Height: | Size: 199 KiB |
BIN
public/images/home/digital-items-5.png
Normal file
|
After Width: | Height: | Size: 188 KiB |
BIN
public/images/home/digital-items-6.png
Normal file
|
After Width: | Height: | Size: 177 KiB |
BIN
public/images/home/digital-items-7.png
Normal file
|
After Width: | Height: | Size: 244 KiB |
BIN
public/images/home/digital-items-8.png
Normal file
|
After Width: | Height: | Size: 158 KiB |
44
src/App.vue
@@ -7,14 +7,18 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref, onBeforeUnmount } from 'vue'
|
||||
import { computed, onMounted, ref, onBeforeUnmount, watch } from 'vue'
|
||||
import RouteCache from '@/components/RouteCache.vue'
|
||||
import MainHeader from '@/views/main-header.vue'
|
||||
import LoginDialog from '@/views/login/login-dialog.vue'
|
||||
import { useGlobalStore } from '@/stores'
|
||||
import { useGlobalStore, useUserInfoStore } from '@/stores'
|
||||
import ShoppingDrawer from '@/views/shopping-drawer.vue'
|
||||
import { wsManager } from '@/utils/websocket'
|
||||
|
||||
const globalStore = useGlobalStore()
|
||||
const userInfoStore = useUserInfoStore()
|
||||
const loading = computed(() => globalStore.state.loading)
|
||||
globalStore.setLoading(false)
|
||||
const viewRef = ref()
|
||||
const viewStyle = ref({
|
||||
'--app-view-width': '',
|
||||
@@ -25,12 +29,41 @@
|
||||
viewStyle.value['--app-view-width'] = width + 'px'
|
||||
viewStyle.value['--app-view-height'] = height + 'px'
|
||||
})
|
||||
|
||||
// 监听 token 变化,建立或关闭 WebSocket 连接
|
||||
watch(
|
||||
() => userInfoStore.state.token,
|
||||
(newToken, oldToken) => {
|
||||
if (newToken && newToken !== oldToken) {
|
||||
// 用户登录,建立 WebSocket 连接
|
||||
console.log('用户已登录,建立 WebSocket 连接')
|
||||
wsManager.connect(newToken)
|
||||
} else if (!newToken && oldToken) {
|
||||
// 用户退出登录,关闭 WebSocket 连接
|
||||
console.log('用户已退出,关闭 WebSocket 连接')
|
||||
wsManager.close()
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
observer.observe(viewRef.value)
|
||||
|
||||
// 如果已经有 token,立即建立连接
|
||||
const token = userInfoStore.state.token
|
||||
if (token) {
|
||||
console.log('应用启动时检测到 token,建立 WebSocket 连接')
|
||||
wsManager.connect(token)
|
||||
}
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
observer.disconnect()
|
||||
// 组件卸载时关闭 WebSocket 连接
|
||||
wsManager.close()
|
||||
})
|
||||
|
||||
window['onClickPrivacy'] = () => {
|
||||
const e = window.event || event
|
||||
e.stopPropagation()
|
||||
@@ -52,10 +85,9 @@
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
// background-color: rgba(0, 0, 0, 0.3);
|
||||
// display: flex;
|
||||
// align-items: center;
|
||||
// justify-content: center;
|
||||
--el-mask-color: rgba(0, 0, 0, 0.3);
|
||||
--el-color-primary: #007bff;
|
||||
--el-loading-spinner-size: 9rem;
|
||||
}
|
||||
</style>
|
||||
<style lang="less" scoped>
|
||||
|
||||
136
src/api/account.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 登录发送验证码
|
||||
* @param data - 包含邮箱的参数
|
||||
* @param data.email - 邮箱
|
||||
* @param data.password - 密码
|
||||
*/
|
||||
export const AccountSendLoginCode = (data) => {
|
||||
return request({
|
||||
url: '/buyer/account/preLogin',
|
||||
method: 'post',
|
||||
data,
|
||||
loading: true
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 登录
|
||||
* @param data - 包含邮箱的参数
|
||||
* @param data.email - 邮箱
|
||||
* @param data.password - 密码
|
||||
* @param data.emailVerifyCode - 验证码
|
||||
*/
|
||||
export const AccountLogin = (data) => {
|
||||
return request({
|
||||
url: '/buyer/account/login',
|
||||
method: 'post',
|
||||
data,
|
||||
loading: true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册
|
||||
* @param data - 包含注册信息的参数
|
||||
* @param data.email - 邮箱
|
||||
* @param data.password - 密码
|
||||
* @param data.username - 用户名
|
||||
* @param data.emailVerifyCode - 验证码
|
||||
*/
|
||||
export const AccountRegister = (data) => {
|
||||
return request({
|
||||
url: '/buyer/account/register',
|
||||
method: 'post',
|
||||
data,
|
||||
loading: true
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 注册||忘记密码:发送邮箱验证码
|
||||
* @param data - 包含邮箱的参数
|
||||
* @param data.email - 邮箱
|
||||
* @param data.operationType - 操作类型:FORGET_PWD, REGISTER
|
||||
*/
|
||||
export const AccountSendVerifyCode = (data) => {
|
||||
return request({
|
||||
url: '/buyer/account/sendCode',
|
||||
method: 'post',
|
||||
data,
|
||||
loading: true
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 忘记密码:重置密码
|
||||
* @param data - 包含邮箱的参数
|
||||
* @param data.email - 邮箱
|
||||
* @param data.password - 密码
|
||||
* @param data.emailVerifyCode - 验证码
|
||||
*/
|
||||
export const AccountResetPassword = (data) => {
|
||||
return request({
|
||||
url: '/buyer/account/resetPassword',
|
||||
method: 'post',
|
||||
data,
|
||||
loading: true
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 通用验证码校验
|
||||
* @param data - 包含邮箱的参数
|
||||
* @param data.email - 邮箱
|
||||
* @param data.emailVerifyCode - 验证码
|
||||
* @param data.operationType - 操作类型:FORGET_PWD, BIND_MAILBOX
|
||||
*/
|
||||
export const AccountVerifyCode = (data) => {
|
||||
return request({
|
||||
url: '/buyer/account/verifyCode',
|
||||
method: 'post',
|
||||
data,
|
||||
loading: true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 变更邮箱:发送新邮箱验证码
|
||||
* @param data - 包含邮箱的参数
|
||||
* @param data.oldEmail - 旧邮箱
|
||||
* @param data.newEmail - 新邮箱
|
||||
*/
|
||||
export const AccountSendEmailChangeCode = (data) => {
|
||||
return request({
|
||||
url: '/buyer/account/sendEmailChangeCode',
|
||||
method: 'post',
|
||||
data,
|
||||
loading: true
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 变更邮箱:绑定新邮箱
|
||||
* @param data - 包含邮箱的参数
|
||||
* @param data.oldEmail - 旧邮箱
|
||||
* @param data.newEmail - 新邮箱
|
||||
* @param data.emailVerifyCode - 验证码
|
||||
*/
|
||||
export const AccountBindEmail = (data) => {
|
||||
return request({
|
||||
url: '/buyer/account/bindEmail',
|
||||
method: 'post',
|
||||
data,
|
||||
loading: true
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出登录
|
||||
* @param data - 参数
|
||||
* @param data.userId - 用户ID
|
||||
*/
|
||||
export const AccountLogout = (data) => {
|
||||
return request({
|
||||
url: '/buyer/account/logout',
|
||||
method: 'post',
|
||||
data,
|
||||
loading: true
|
||||
})
|
||||
}
|
||||
35
src/api/brand.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import request from '@/utils/request'
|
||||
/**
|
||||
* 获取店铺列表
|
||||
* @param data 获取店铺列表的参数
|
||||
* @param data.keyword 模糊查询店铺
|
||||
* @returns 获取店铺列表
|
||||
*/
|
||||
export interface designerListData {
|
||||
keyword: string
|
||||
}
|
||||
export const getDesignerList = (data:designerListData,loading?:boolean) => {
|
||||
return request({
|
||||
url: '/buyer/designer/search',
|
||||
method: 'get',
|
||||
params: data,
|
||||
loading
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取店铺详情
|
||||
* @param data 获取店铺详情的参数
|
||||
* @param data.sellerId 店铺 id
|
||||
* @returns 获取店铺详情
|
||||
*/
|
||||
export interface designerDetailData {
|
||||
sellerId?: string
|
||||
}
|
||||
export const getDesignerDetail = (data:designerDetailData,loading?:boolean) => {
|
||||
return request({
|
||||
url: `/buyer/designer/shop/${data.sellerId}`,
|
||||
method: 'get',
|
||||
loading
|
||||
})
|
||||
}
|
||||
70
src/api/listing.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import request from '@/utils/request'
|
||||
/**
|
||||
* 获取店铺商品列表
|
||||
* @param data 获取店铺商品列表的参数
|
||||
* @param data.sellerId 店铺id
|
||||
* @param data.designFor 查询类型 female/male/all
|
||||
* @param data.pageNum 页码
|
||||
* @param data.pageSize 页面大小
|
||||
* @returns 获取店铺商品列表
|
||||
*/
|
||||
export interface listingListData {
|
||||
sellerId?: string
|
||||
designFor?: string
|
||||
pageNum?: number
|
||||
pageSize?: number
|
||||
}
|
||||
export const getlistingListApi = (data:listingListData,loading?:boolean) => {
|
||||
return request({
|
||||
url: '/buyer/listing/shop/seller',
|
||||
method: 'get',
|
||||
params: data,
|
||||
loading
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取商品详情
|
||||
* @param data 获取商品详情的参数
|
||||
* @param data.id 商品 id
|
||||
* @returns 获取商品详情
|
||||
*/
|
||||
export interface listingDetailData {
|
||||
id?: string
|
||||
}
|
||||
export const getListingDetailApi = (data:listingDetailData,loading?:boolean) => {
|
||||
return request({
|
||||
url: `/buyer/listing/mall/detail`,
|
||||
method: 'get',
|
||||
params: data,
|
||||
loading
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取资产数字商品列表
|
||||
* @param data 获取资产数字商品列表的参数
|
||||
* @param data.designFor 查询类型 female/male/all
|
||||
* @param data.categories 商品分类
|
||||
* @param data.sortField 排序字段 price/salesVolume/updateTime/viewCount/createTime,默认 updateTime
|
||||
* @param data.sortOrder 排序顺序:asc/desc,默认 desc
|
||||
* @param data.pageNum 页码
|
||||
* @param data.pageSize 页面大小
|
||||
* @returns 获取资产数字商品列表
|
||||
*/
|
||||
export interface listingMallData {
|
||||
designFor: string,
|
||||
categories: string[],
|
||||
sortField: string,
|
||||
sortOrder: string,
|
||||
pageNum: number,
|
||||
pageSize: number
|
||||
}
|
||||
export const getListingMallListApi = (data:listingMallData,loading?:boolean) => {
|
||||
return request({
|
||||
url: `/buyer/listing/mall`,
|
||||
method: 'post',
|
||||
data,
|
||||
loading
|
||||
})
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
interface LoginParamsType {
|
||||
name?: string // 姓名
|
||||
email: string // 邮箱
|
||||
password?: string // 密码
|
||||
operationType: 'REGISTER' | 'LOGIN' | 'FORGET_PWD'
|
||||
verifyCode?: string // 验证码
|
||||
}
|
||||
|
||||
// 发送验证码
|
||||
export const precheckEmail = (params: { email: string }): Promise<ApiResponse> => {
|
||||
return request({
|
||||
url: '/api/auth/precheckEmail',
|
||||
method: 'get',
|
||||
params
|
||||
})
|
||||
}
|
||||
|
||||
export const fetchRegisterOrLogin = (data: LoginParamsType): Promise<LoginResponse> => {
|
||||
return request({
|
||||
url: '/api/auth/registerOrLogin',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export const resetPassword = (data: LoginParamsType): Promise<ApiResponse> => {
|
||||
return request({
|
||||
url: '/api/auth/forgotPwd',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export const checkLoginStatus = (): Promise<ApiResponse<LoginResponse>> => {
|
||||
return request({
|
||||
url: '/api/auth/checkLoginStatus',
|
||||
method: 'get',
|
||||
meta: { responseAll: true }
|
||||
})
|
||||
}
|
||||
|
||||
export const LogOut = (): Promise<ApiResponse> => {
|
||||
return request({
|
||||
url: '/api/auth/logout',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// Google登录/注册参数类型
|
||||
interface GoogleAuthParamsType {
|
||||
accessToken?: string // Google ID Token (用于One Tap登录)
|
||||
}
|
||||
export const googleAuth = (data: GoogleAuthParamsType): Promise<LoginResponse> => {
|
||||
return request({
|
||||
url: '/api/auth/parseGoogleAccessToken',
|
||||
method: 'get',
|
||||
params: data
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/** 更改用户信息
|
||||
* @param data 包含用户信息的对象
|
||||
* @param data.username 用户名
|
||||
* @param data.email 邮箱
|
||||
* @param data.password 密码
|
||||
* @returns 包含更新后的用户信息的对象
|
||||
*/
|
||||
export const updateUserInfo = (data: any) => {
|
||||
return request({
|
||||
url: '/api/auth/updateUserInfo',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
40
src/api/notification.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
interface Page {
|
||||
page: number
|
||||
size: number
|
||||
type?: number
|
||||
isRead?: 0 | 1 // 0未读1已读
|
||||
keyword?: string // 关键词搜索标题Ï
|
||||
}
|
||||
export const fetchAllMessageList = (data) => {
|
||||
return request({
|
||||
url: '/buyer/buyer/message/page',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 获取所有未读消息数量
|
||||
export const fetchAllUnreadMessage = () => {
|
||||
return request({
|
||||
url: '/buyer/buyer/message/unread-count',
|
||||
method: 'get'
|
||||
})
|
||||
}
|
||||
|
||||
// 标记单条消息已读
|
||||
export const markMessageAsRead = (id: number | string) => {
|
||||
return request({
|
||||
url: `/buyer/buyer/message/${id}/read`,
|
||||
method: 'put'
|
||||
})
|
||||
}
|
||||
|
||||
// 标记全部消息已读
|
||||
export const markAllMessagesAsRead = () => {
|
||||
return request({
|
||||
url: '/buyer/buyer/message/read-all',
|
||||
method: 'put'
|
||||
})
|
||||
}
|
||||
91
src/api/shoppingCart.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import request from '@/utils/request'
|
||||
|
||||
/**
|
||||
* 加入购物车
|
||||
* @param data - 包含邮箱的参数
|
||||
* @param data.listingId - 商品ID
|
||||
* @param data.listingIds - 商品ID列表
|
||||
*/
|
||||
export const AddShoppingCart = (data, loading?: boolean) => {
|
||||
return request({
|
||||
url: '/buyer/buyer/cart/add',
|
||||
method: 'post',
|
||||
data,
|
||||
loading
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空购物车
|
||||
*/
|
||||
export const ClearShoppingCart = (loading?: boolean) => {
|
||||
return request({
|
||||
url: '/buyer/buyer/cart/clear',
|
||||
method: 'delete',
|
||||
loading
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取购物车列表
|
||||
* @param loading - 是否显示loading
|
||||
* @returns 购物车列表数据
|
||||
*/
|
||||
export const GetShoppingCartList = (loading?: boolean) => {
|
||||
return request({
|
||||
url: '/buyer/buyer/cart/list',
|
||||
method: 'get',
|
||||
loading
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 从购物车移除商品
|
||||
* @param params - 包含邮箱的参数
|
||||
* @param params.listingId - 商品ID
|
||||
*/
|
||||
export const RemoveShoppingCartItem = (params, loading?: boolean) => {
|
||||
return request({
|
||||
url: '/buyer/buyer/cart/remove',
|
||||
method: 'delete',
|
||||
params,
|
||||
loading
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建订单
|
||||
* @param { Array } data - 商品id数组
|
||||
* @param loading - 是否显示loading
|
||||
* @returns
|
||||
*/
|
||||
export const CreateOrder = (data, loading?: boolean) => {
|
||||
return request({
|
||||
url: '/buyer/buyer/order/create',
|
||||
method: 'post',
|
||||
data,
|
||||
loading
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订单状态
|
||||
* @param { String } paymentId - 订单ID
|
||||
* @param loading - 是否显示loading
|
||||
* @returns
|
||||
*/
|
||||
export const GetOrderStatus = (paymentId: string) => {
|
||||
return request({
|
||||
url: `/buyer/buyer/payment/status/${paymentId}`,
|
||||
method: 'get',
|
||||
loading: true,
|
||||
})
|
||||
}
|
||||
export const ORDER_STATUS = {
|
||||
PENDING: 0,//待支付
|
||||
SUCCESS: 1,//支付成功
|
||||
FAILED: 2,//支付失败
|
||||
CANCELED: 3,//已取消支付
|
||||
PARTIAL_REFUND: 4,//已部分退款
|
||||
FULL_REFUND: 5,//已全额退款
|
||||
}
|
||||
172
src/api/user.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
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
|
||||
}
|
||||
export interface WardrobeItem {
|
||||
buyerId: number
|
||||
categories: string[]
|
||||
designFor: 'female' | 'male' | 'all'
|
||||
page: number
|
||||
size: number
|
||||
}
|
||||
// 获取我的衣橱assets
|
||||
export const fetchMyWardrobe = (data: WardrobeItem): Promise<ApiResponse> => {
|
||||
return request({
|
||||
url: '/buyer/buyer/order/assets/page',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
export interface OrderItem {
|
||||
status?: number // 0未支付 1已支付 2已取消 不传查全部
|
||||
page: number
|
||||
size: number
|
||||
}
|
||||
|
||||
export interface OrdersPageResponse {
|
||||
content: any[]
|
||||
}
|
||||
// 获取我的衣橱 orders
|
||||
export const fetchMyOrders = (data: OrderItem): Promise<OrdersPageResponse> => {
|
||||
return request({
|
||||
url: '/buyer/buyer/order/page',
|
||||
method: 'get',
|
||||
params: data
|
||||
})
|
||||
}
|
||||
|
||||
export interface Download {
|
||||
ids: string[]
|
||||
}
|
||||
// 下载资源
|
||||
export const fetchDownloadItemsByGet = (
|
||||
params: Download,
|
||||
onDownloadProgress?: (event: AxiosProgressEvent) => void
|
||||
): Promise<ApiResponse> => {
|
||||
return request({
|
||||
url: '/buyer/listing/mall/main-product/download',
|
||||
method: 'get',
|
||||
responseType: 'blob',
|
||||
timeout: 600000,
|
||||
params,
|
||||
paramsSerializer: (p: any) => {
|
||||
const usp = new URLSearchParams()
|
||||
if (p && p.ids && Array.isArray(p.ids)) {
|
||||
p.ids.forEach((id: any) => usp.append('ids', String(id)))
|
||||
} else if (p) {
|
||||
Object.keys(p).forEach((k) => {
|
||||
const v = (p as any)[k]
|
||||
if (Array.isArray(v)) {
|
||||
v.forEach((x) => usp.append(k, String(x)))
|
||||
} else if (v !== undefined && v !== null) {
|
||||
usp.append(k, String(v))
|
||||
}
|
||||
})
|
||||
}
|
||||
return usp.toString()
|
||||
},
|
||||
onDownloadProgress
|
||||
})
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
export const fetchUserProfile = (): Promise<ApiResponse> => {
|
||||
return request({
|
||||
url: '/buyer/profile/getProfile',
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
// 设置用户信息
|
||||
export interface UserProfile {
|
||||
firstName: string
|
||||
lastName: string
|
||||
username: string
|
||||
roles: string[]
|
||||
region: string
|
||||
language: string
|
||||
email: string
|
||||
avatarUrl?: string
|
||||
oldPassword?: string
|
||||
newPassword?: string
|
||||
verifyCode?: string
|
||||
}
|
||||
export const updateUserProfile = (data: UserProfile): Promise<ApiResponse> => {
|
||||
return request({
|
||||
url: '/buyer/profile/setProfile',
|
||||
method: 'post',
|
||||
data
|
||||
})
|
||||
}
|
||||
|
||||
// 设置头像
|
||||
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({
|
||||
url: '/buyer/profile/sendEmailChangeCode',
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
// 验证设置页验证码
|
||||
export const verifyEmailCode = (verifyCode: string): Promise<ApiResponse> => {
|
||||
return request({
|
||||
url: '/buyer/profile/verifyEmailChangeCode',
|
||||
method: 'post',
|
||||
data: { verifyCode }
|
||||
})
|
||||
}
|
||||
|
||||
// 获取用户语言设置
|
||||
export const getUserLanguage = (): Promise<ApiResponse> => {
|
||||
return request({
|
||||
url: '/buyer/profile/getLanguage',
|
||||
method: 'post'
|
||||
})
|
||||
}
|
||||
|
||||
// 设置语言
|
||||
export const setUserLanguage = (
|
||||
language: 'ENGLISH' | 'CHINESE_SIMPLIFIED'
|
||||
): Promise<ApiResponse> => {
|
||||
return request({
|
||||
url: '/buyer/profile/setLanguage',
|
||||
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
|
||||
})
|
||||
}
|
||||
@@ -7,145 +7,152 @@ h1,
|
||||
h2,
|
||||
h3,
|
||||
p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body,
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
font-family: 'KaiseiOpti-Medium';
|
||||
color: #232323;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
font-family: 'KaiseiOpti-Medium';
|
||||
color: #232323;
|
||||
}
|
||||
@keyframes loading {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@keyframes opacity-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@keyframes z-index-10to-1 {
|
||||
0% {
|
||||
z-index: 10;
|
||||
}
|
||||
100% {
|
||||
z-index: -1;
|
||||
}
|
||||
0% {
|
||||
z-index: 10;
|
||||
}
|
||||
100% {
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
.flex {
|
||||
display: flex;
|
||||
display: flex;
|
||||
}
|
||||
.flex-center {
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.flex-1 {
|
||||
flex: 1;
|
||||
flex: 1;
|
||||
}
|
||||
.flex-col {
|
||||
flex-direction: column;
|
||||
flex-direction: column;
|
||||
}
|
||||
.align-center {
|
||||
align-items: center;
|
||||
align-items: center;
|
||||
}
|
||||
.space-between {
|
||||
justify-content: space-between;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.justify-center {
|
||||
justify-content: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.relative {
|
||||
position: relative;
|
||||
position: relative;
|
||||
}
|
||||
.el-overlay {
|
||||
--el-color-primary: #ff7a51;
|
||||
--el-color-primary-light-3: #ffa785;
|
||||
--el-color-primary-light-5: #ffc2aa;
|
||||
--el-color-primary-light-7: #ffddcf;
|
||||
--el-color-primary-light-8: #ffe8df;
|
||||
--el-color-primary-light-9: #fff2ec;
|
||||
--el-color-primary-dark-2: #cc6241;
|
||||
--el-color-primary: #ff7a51;
|
||||
--el-color-primary-light-3: #ffa785;
|
||||
--el-color-primary-light-5: #ffc2aa;
|
||||
--el-color-primary-light-7: #ffddcf;
|
||||
--el-color-primary-light-8: #ffe8df;
|
||||
--el-color-primary-light-9: #fff2ec;
|
||||
--el-color-primary-dark-2: #cc6241;
|
||||
}
|
||||
.el-select,
|
||||
.el-popper {
|
||||
--el-color-primary: #6c6c6c;
|
||||
/* 主灰色 */
|
||||
--el-color-primary-light-3: #8a8a8a;
|
||||
/* 较浅的灰色(混合20%白) */
|
||||
--el-color-primary-light-5: #a8a8a8;
|
||||
/* 更浅的灰色(混合33%白) */
|
||||
--el-color-primary-light-7: #c6c6c6;
|
||||
/* 浅灰色(混合47%白) */
|
||||
--el-color-primary-light-8: #d4d4d4;
|
||||
/* 很浅的灰色(混合53%白) */
|
||||
--el-color-primary-light-9: #e3e3e3;
|
||||
/* 极浅的灰色(混合60%白) */
|
||||
--el-color-primary-dark-2: #565656;
|
||||
/* 深灰色(加深20%) */
|
||||
--el-color-primary: #6c6c6c;
|
||||
/* 主灰色 */
|
||||
--el-color-primary-light-3: #8a8a8a;
|
||||
/* 较浅的灰色(混合20%白) */
|
||||
--el-color-primary-light-5: #a8a8a8;
|
||||
/* 更浅的灰色(混合33%白) */
|
||||
--el-color-primary-light-7: #c6c6c6;
|
||||
/* 浅灰色(混合47%白) */
|
||||
--el-color-primary-light-8: #d4d4d4;
|
||||
/* 很浅的灰色(混合53%白) */
|
||||
--el-color-primary-light-9: #e3e3e3;
|
||||
/* 极浅的灰色(混合60%白) */
|
||||
--el-color-primary-dark-2: #565656;
|
||||
/* 深灰色(加深20%) */
|
||||
}
|
||||
.mini-scrollbar {
|
||||
--mini-scrollbar-width: 0.4rem;
|
||||
--mini-scrollbar-width: 0.4rem;
|
||||
}
|
||||
.mini-scrollbar::-webkit-scrollbar {
|
||||
width: var(--mini-scrollbar-width);
|
||||
width: var(--mini-scrollbar-width);
|
||||
}
|
||||
.mini-scrollbar::-webkit-scrollbar-thumb {
|
||||
border-radius: 0.4rem;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
border-radius: 0.4rem;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
.mosaic-bg {
|
||||
--mosaic-bg-size: 1rem;
|
||||
--mosaic-bg-color1: #efefef;
|
||||
--mosaic-bg-color2: #fff;
|
||||
background-image: repeating-conic-gradient(var(--mosaic-bg-color1) 0% 25%, var(--mosaic-bg-color2) 0% 50%);
|
||||
background-repeat: repeat;
|
||||
background-position: 50% 50%;
|
||||
background-size: var(--mosaic-bg-size) var(--mosaic-bg-size);
|
||||
--mosaic-bg-size: 1rem;
|
||||
--mosaic-bg-color1: #efefef;
|
||||
--mosaic-bg-color2: #fff;
|
||||
background-image: repeating-conic-gradient(
|
||||
var(--mosaic-bg-color1) 0% 25%,
|
||||
var(--mosaic-bg-color2) 0% 50%
|
||||
);
|
||||
background-repeat: repeat;
|
||||
background-position: 50% 50%;
|
||||
background-size: var(--mosaic-bg-size) var(--mosaic-bg-size);
|
||||
}
|
||||
button[custom],
|
||||
button[custom="white"] {
|
||||
min-width: 19.4rem;
|
||||
height: 5rem;
|
||||
padding: 0 1rem;
|
||||
border-radius: 0;
|
||||
font-family: KaiseiOpti-Bold;
|
||||
font-size: var(--button-font-size, 2rem);
|
||||
color: var(--button-color, #232323);
|
||||
background: var(--button-bgcolor, #fff);
|
||||
border: var(--button-border, none);
|
||||
cursor: pointer;
|
||||
button[custom='white'] {
|
||||
min-width: 19.4rem;
|
||||
height: 5rem;
|
||||
padding: 0 1rem;
|
||||
border-radius: 0;
|
||||
font-family: KaiseiOpti-Bold;
|
||||
font-size: var(--button-font-size, 2rem);
|
||||
color: var(--button-color, #232323);
|
||||
background: var(--button-bgcolor, #fff);
|
||||
border: var(--button-border, none);
|
||||
cursor: pointer;
|
||||
}
|
||||
button[custom]:active,
|
||||
button[custom="white"]:active {
|
||||
background: var(--button-click-bgcolor, #e4e4e4);
|
||||
color: var(--button-click-color, #232323);
|
||||
button[custom='white']:active {
|
||||
background: var(--button-click-bgcolor, #e4e4e4);
|
||||
color: var(--button-click-color, #232323);
|
||||
}
|
||||
button[custom="black"] {
|
||||
--button-bgcolor: #232323;
|
||||
--button-color: #fff;
|
||||
--button-click-bgcolor: #333;
|
||||
--button-click-color: #fff;
|
||||
--button-font-size: 1.6rem;
|
||||
button[custom='black'] {
|
||||
--button-bgcolor: #232323;
|
||||
--button-color: #fff;
|
||||
--button-click-bgcolor: #333;
|
||||
--button-click-color: #fff;
|
||||
--button-font-size: 1.6rem;
|
||||
}
|
||||
button[custom="black-box"] {
|
||||
--button-bgcolor: transparent;
|
||||
--button-color: #232323;
|
||||
--button-border: 0.2rem solid #979797;
|
||||
--button-click-bgcolor: #979797;
|
||||
--button-click-color: #fff;
|
||||
--button-font-size: 1.6rem;
|
||||
button[custom='black-box'] {
|
||||
--button-bgcolor: transparent;
|
||||
--button-color: #232323;
|
||||
--button-border: 0.2rem solid #979797;
|
||||
--button-click-bgcolor: #979797;
|
||||
--button-click-color: #fff;
|
||||
--button-font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.el-select-dropdown .el-select-dropdown__item {
|
||||
padding: 0 2rem;
|
||||
}
|
||||
|
||||
3
src/assets/icons/card.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="20" height="16" viewBox="0 0 20 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 0H2C0.9 0 0 0.9 0 2V14C0 15.1 0.9 16 2 16H18C19.1 16 20 15.1 20 14V2C20 0.9 19.1 0 18 0ZM19 14C19 14.5515 18.5515 15 18 15H2C1.4485 15 1 14.5515 1 14V11H19V14ZM19 9H1V2C1 1.4485 1.4485 1 2 1H18C18.5515 1 19 1.4485 19 2V9Z" fill="#232323"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 356 B |
@@ -1,5 +1,34 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 6.4C20 5.3 19.1 4 18 4H7.5C7.4 4 7.2 4 7.1 4C5.9 4.2 5 5.2 5 6.4L4 9C4 10.4 5.1 11.5 6.5 11.5C7.9 11.5 8.1 11.1 8.5 10.5C8.9 11.1 9.7 11.5 10.5 11.5C11.3 11.5 12.1 11.1 12.5 10.5C12.9 11.1 13.7 11.5 14.5 11.5C15.3 11.5 16.1 11.1 16.5 10.5C16.9 11.1 17.7 11.5 18.5 11.5C19.9 11.5 21 10.4 21 9M5.9 6.4C5.9 5.6 6.5 5 7.2 4.9C7.2 4.9 7.3 4.9 7.4 4.9H17.9C18.5 4.9 19 5.8 19 6.4C19 7 20 9.1 20 9.1C20 10 19.3 10.7 18.4 10.7C17.5 10.7 16.8 10 16.8 9.1V7.6H15.9V9.1C15.9 10 15.2 10.7 14.3 10.7C13.4 10.7 12.7 10 12.7 9.1V7.6H11.8V9.1C11.8 10 11.1 10.7 10.2 10.7C9.3 10.7 8.6 10 8.6 9.1V7.6H7.7V9.1C7.7 10 7.50938 10.7 6.60938 10.7C5.48438 10.7 4.9 9.9 4.9 9L5.9 6.4Z" fill="#232323"/>
|
||||
<path d="M6 11.2V19.3C6 19.9 6.4 20.3 7 20.3H18C18.6 20.3 19 19.9 19 19.3V13.5" stroke="#232323" stroke-linecap="round"/>
|
||||
<path d="M17 15.5V13.5C17 13.2239 16.7761 13 16.5 13C16.2239 13 16 13.2239 16 13.5V15.5C16 15.7761 16.2239 16 16.5 16C16.7761 16 17 15.7761 17 15.5Z" fill="#232323"/>
|
||||
<?xml version="1.0" standalone="no"?>
|
||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
|
||||
preserveAspectRatio="xMidYMid meet">
|
||||
|
||||
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
|
||||
fill="#000000" stroke="none">
|
||||
<path d="M1466 4255 c-92 -26 -177 -75 -247 -145 -80 -80 -124 -165 -140 -271
|
||||
-7 -49 -46 -167 -115 -349 -110 -291 -116 -319 -93 -420 31 -136 153 -289 276
|
||||
-346 l62 -29 3 -885 3 -885 23 -47 c13 -26 43 -65 67 -87 82 -76 1 -72 1386
|
||||
-69 l1234 3 55 30 c60 33 114 96 130 153 6 24 10 274 10 699 l0 662 -22 21
|
||||
c-25 23 -56 26 -84 6 -18 -14 -19 -36 -24 -678 -6 -738 -3 -708 -74 -742 -31
|
||||
-15 -145 -16 -1253 -16 -1204 0 -1218 0 -1251 20 -64 39 -62 3 -62 938 l0 849
|
||||
108 5 c166 9 244 46 316 151 l37 54 57 -56 c99 -98 230 -151 374 -151 141 0
|
||||
287 60 376 154 l49 51 57 -57 c103 -104 250 -156 409 -145 138 9 257 64 347
|
||||
160 l40 43 63 -61 c98 -97 221 -145 367 -145 263 0 486 190 521 445 l11 80
|
||||
-102 265 c-72 186 -107 290 -116 348 -39 236 -188 417 -373 452 -35 6 -467 10
|
||||
-1215 9 -939 -1 -1170 -3 -1210 -14z m2388 -186 c97 -27 177 -151 202 -313 16
|
||||
-108 55 -221 147 -426 52 -115 61 -143 60 -189 -2 -72 -49 -167 -109 -220 -70
|
||||
-61 -140 -84 -244 -79 -75 3 -94 8 -150 39 -43 24 -78 52 -102 85 -63 82 -70
|
||||
115 -75 337 l-5 197 -94 0 -94 0 0 -195 c0 -174 -2 -200 -21 -252 -27 -70 -95
|
||||
-145 -164 -181 -72 -37 -197 -43 -277 -12 -75 28 -150 96 -185 168 -27 56 -28
|
||||
61 -31 265 l-4 207 -93 0 -94 0 -3 -203 -3 -203 -33 -66 c-40 -81 -102 -139
|
||||
-181 -169 -80 -30 -204 -24 -279 15 -64 33 -123 92 -154 154 -21 43 -23 63
|
||||
-28 257 l-5 210 -95 0 -95 0 -5 -240 c-5 -223 -7 -243 -28 -290 -47 -101 -102
|
||||
-130 -234 -123 -65 4 -96 11 -141 33 -107 53 -174 152 -184 270 -5 57 0 72 98
|
||||
325 56 146 106 291 110 323 20 132 105 232 234 273 53 17 2299 20 2359 3z"/>
|
||||
<path d="M3494 2343 c-12 -2 -34 -18 -50 -34 l-29 -30 -3 -211 c-2 -116 -1
|
||||
-231 3 -256 14 -97 108 -135 177 -73 l33 29 3 241 c3 205 1 245 -13 274 -21
|
||||
45 -71 70 -121 60z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 2.1 KiB |
5
src/assets/icons/order/warning.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10 20C8.02219 20 6.08879 19.4135 4.4443 18.3147C2.79981 17.2159 1.51809 15.6541 0.761209 13.8268C0.00433286 11.9996 -0.193701 9.98891 0.192152 8.0491C0.578004 6.10929 1.53041 4.32746 2.92894 2.92894C4.32746 1.53041 6.10929 0.578004 8.0491 0.192152C9.98891 -0.193701 11.9996 0.00433286 13.8268 0.761209C15.6541 1.51809 17.2159 2.79981 18.3147 4.4443C19.4135 6.08879 20 8.02219 20 10C19.9971 12.6513 18.9426 15.1932 17.0679 17.0679C15.1932 18.9426 12.6513 19.9971 10 20ZM10 1.66667C8.35183 1.66667 6.74066 2.15541 5.37025 3.07109C3.99984 3.98677 2.93174 5.28826 2.30101 6.81098C1.67028 8.33369 1.50525 10.0092 1.82679 11.6258C2.14834 13.2423 2.94201 14.7271 4.10745 15.8926C5.27289 17.058 6.75774 17.8517 8.37425 18.1732C9.99076 18.4948 11.6663 18.3297 13.189 17.699C14.7118 17.0683 16.0132 16.0002 16.9289 14.6298C17.8446 13.2593 18.3333 11.6482 18.3333 10C18.3309 7.79061 17.4522 5.67241 15.8899 4.11013C14.3276 2.54785 12.2094 1.6691 10 1.66667Z" fill="#232323"/>
|
||||
<path d="M11.6654 15.8333H9.9987V9.99998H8.33203V8.33331H9.9987C10.4407 8.33331 10.8646 8.50891 11.1772 8.82147C11.4898 9.13403 11.6654 9.55795 11.6654 9.99998V15.8333Z" fill="#232323"/>
|
||||
<path d="M10 6.66666C10.6904 6.66666 11.25 6.10701 11.25 5.41666C11.25 4.7263 10.6904 4.16666 10 4.16666C9.30964 4.16666 8.75 4.7263 8.75 5.41666C8.75 6.10701 9.30964 6.66666 10 6.66666Z" fill="#232323"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
9
src/assets/icons/pay/stripe.svg
Normal file
|
After Width: | Height: | Size: 65 KiB |
5
src/assets/icons/shop.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M20 6.4C20 5.3 19.1 4 18 4H7.5C7.4 4 7.2 4 7.1 4C5.9 4.2 5 5.2 5 6.4L4 9C4 10.4 5.1 11.5 6.5 11.5C7.9 11.5 8.1 11.1 8.5 10.5C8.9 11.1 9.7 11.5 10.5 11.5C11.3 11.5 12.1 11.1 12.5 10.5C12.9 11.1 13.7 11.5 14.5 11.5C15.3 11.5 16.1 11.1 16.5 10.5C16.9 11.1 17.7 11.5 18.5 11.5C19.9 11.5 21 10.4 21 9M5.9 6.4C5.9 5.6 6.5 5 7.2 4.9C7.2 4.9 7.3 4.9 7.4 4.9H17.9C18.5 4.9 19 5.8 19 6.4C19 7 20 9.1 20 9.1C20 10 19.3 10.7 18.4 10.7C17.5 10.7 16.8 10 16.8 9.1V7.6H15.9V9.1C15.9 10 15.2 10.7 14.3 10.7C13.4 10.7 12.7 10 12.7 9.1V7.6H11.8V9.1C11.8 10 11.1 10.7 10.2 10.7C9.3 10.7 8.6 10 8.6 9.1V7.6H7.7V9.1C7.7 10 7.50938 10.7 6.60938 10.7C5.48438 10.7 4.9 9.9 4.9 9L5.9 6.4Z" fill="#232323"/>
|
||||
<path d="M6 11.2V19.3C6 19.9 6.4 20.3 7 20.3H18C18.6 20.3 19 19.9 19 19.3V13.5" stroke="#232323" stroke-width="0.75" stroke-linecap="round"/>
|
||||
<path d="M17 15.5V13.5C17 13.2239 16.7761 13 16.5 13C16.2239 13 16 13.2239 16 13.5V15.5C16 15.7761 16.2239 16 16.5 16C16.7761 16 17 15.7761 17 15.5Z" fill="#232323"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
5
src/assets/icons/statement.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M9.99326 1.66675C5.39812 1.66675 1.66406 5.40495 1.66406 10.0001C1.66406 14.5952 5.40226 18.3334 9.99326 18.3334C14.5842 18.3334 18.3224 14.5952 18.3224 10.0001C18.3224 5.40495 14.5884 1.66675 9.99326 1.66675ZM9.99326 17.402C5.91145 17.402 2.59137 14.0819 2.59137 10.0001C2.59137 5.91828 5.91145 2.59819 9.99326 2.59819C14.0751 2.59819 17.3951 5.91828 17.3951 10.0001C17.3951 14.0819 14.0751 17.402 9.99326 17.402Z" fill="#979797"/>
|
||||
<path d="M10.5115 9.17225C10.5115 8.88646 10.2798 8.65479 9.99403 8.65479C9.70824 8.65479 9.47656 8.88646 9.47656 9.17225V14.14C9.47656 14.4258 9.70824 14.6574 9.99403 14.6574C10.2798 14.6574 10.5115 14.4258 10.5115 14.14V9.17225Z" fill="#979797"/>
|
||||
<path d="M9.99592 6.68813C10.4513 6.68813 10.8239 6.31555 10.8239 5.86018C10.8239 5.4048 10.4513 5.03223 9.99592 5.03223C9.54055 5.03223 9.16797 5.4048 9.16797 5.86018C9.16797 6.31555 9.54055 6.68813 9.99592 6.68813Z" fill="#979797"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
BIN
src/assets/images/avatar.png
Normal file
|
After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 2.4 MiB |
|
Before Width: | Height: | Size: 713 KiB After Width: | Height: | Size: 121 KiB |
BIN
src/assets/images/example.png
Normal file
|
After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 1.3 MiB |
BIN
src/assets/images/icons/youtube.png
Normal file
|
After Width: | Height: | Size: 500 B |
BIN
src/assets/images/pay/stripe.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
src/assets/images/pay/success.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
src/assets/images/wardrobe/settings_bg.jpg
Normal file
|
After Width: | Height: | Size: 144 KiB |
@@ -1,104 +1,120 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, reactive, toRefs } from 'vue'
|
||||
const props = defineProps({
|
||||
url: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: 'aaa'
|
||||
},
|
||||
price: {
|
||||
type: String,
|
||||
default: '111'
|
||||
},
|
||||
download: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showPrice: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
const emit = defineEmits(['addShopping', 'openDetail', 'download'])
|
||||
let data = reactive({})
|
||||
const addShopping = () => {
|
||||
if (props.download) {
|
||||
emit('download')
|
||||
} else {
|
||||
emit('addShopping')
|
||||
}
|
||||
}
|
||||
const openDetail = () => {
|
||||
emit('openDetail')
|
||||
}
|
||||
onMounted(() => {})
|
||||
onUnmounted(() => {})
|
||||
defineExpose({})
|
||||
const {} = toRefs(data)
|
||||
import { ref, onMounted, onUnmounted, reactive, toRefs } from 'vue'
|
||||
const props = defineProps({
|
||||
url: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
name: {
|
||||
type: String,
|
||||
default: 'aaa'
|
||||
},
|
||||
price: {
|
||||
type: [String, Number],
|
||||
default: ''
|
||||
},
|
||||
download: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
},
|
||||
showPrice: {
|
||||
type: Boolean,
|
||||
default: true
|
||||
}
|
||||
})
|
||||
const emit = defineEmits(['addShopping', 'openDetail', 'download'])
|
||||
let data = reactive({})
|
||||
const addShopping = () => {
|
||||
if (props.download) {
|
||||
emit('download')
|
||||
} else {
|
||||
emit('addShopping')
|
||||
}
|
||||
}
|
||||
const openDetail = () => {
|
||||
emit('openDetail')
|
||||
}
|
||||
onMounted(() => {})
|
||||
onUnmounted(() => {})
|
||||
defineExpose({})
|
||||
const {} = toRefs(data)
|
||||
</script>
|
||||
<template>
|
||||
<div class="commodity-item" :class="{ 'is-download': download }">
|
||||
<img :src="props.url" alt="" @click="openDetail" />
|
||||
<div class="detail">
|
||||
<div class="text">
|
||||
<div class="name">
|
||||
{{ props.name }}
|
||||
</div>
|
||||
<div class="price" :class="{ 'is-download': download }" v-if="props.showPrice">
|
||||
{{ props.price }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn" @click="addShopping">
|
||||
<div class="text">
|
||||
<SvgIcon :name="download ? 'download' : 'add'" size="26" color="#232323"></SvgIcon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="commodity-item" :class="{ 'is-download': download }">
|
||||
<img v-loadimg="props.url" alt="" @click="openDetail" />
|
||||
<div class="detail">
|
||||
<div class="text">
|
||||
<div class="name">
|
||||
{{ props.name }}
|
||||
</div>
|
||||
<div
|
||||
class="price"
|
||||
:class="{ 'is-download': download }"
|
||||
v-if="props.showPrice && (props.price || props.price === 0)"
|
||||
>
|
||||
HK${{ props.price }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="btn" @click="addShopping">
|
||||
<div class="text">
|
||||
<SvgIcon
|
||||
:name="download ? 'download' : 'add'"
|
||||
size="26"
|
||||
color="#232323"
|
||||
></SvgIcon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="less" scoped>
|
||||
.commodity-item {
|
||||
width: var(--commodity-width, 100%);
|
||||
&.is-download{
|
||||
img{
|
||||
cursor: initial;
|
||||
}
|
||||
}
|
||||
> img {
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
height: var(--commodity-height, auto);
|
||||
margin-bottom: var(--commodity-marginBottom, 1rem);
|
||||
}
|
||||
> .detail {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
> .text {
|
||||
color: #232323;
|
||||
> .name {
|
||||
font-family: 'KaiseiOpti-Regular';
|
||||
font-weight: 400;
|
||||
font-size: var(--commodity-name-fontSize, 1.6rem);
|
||||
line-height: var(--commodity-name-lineHeight, 2.3rem);
|
||||
margin-bottom: var(--commodity-name-marginBottom, 0rem);
|
||||
}
|
||||
> .price {
|
||||
font-family: 'KaiseiOpti-Regular';
|
||||
font-weight: 400;
|
||||
font-size: var(--commodity-price-fontSize, 1.4rem);
|
||||
line-height: var(--commodity-price-lineHeight, 2.3rem);
|
||||
&.is-download {
|
||||
color: #979797;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
.commodity-item {
|
||||
width: var(--commodity-width, 100%);
|
||||
&.is-download {
|
||||
img {
|
||||
cursor: initial;
|
||||
}
|
||||
}
|
||||
> img {
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
height: var(--commodity-height, auto);
|
||||
aspect-ratio: 0.8/1;
|
||||
object-fit: cover;
|
||||
margin-bottom: var(--commodity-marginBottom, 1rem);
|
||||
}
|
||||
> .detail {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
> .text {
|
||||
color: #232323;
|
||||
overflow: hidden;
|
||||
> .name {
|
||||
font-family: 'KaiseiOpti-Regular';
|
||||
font-weight: 700;
|
||||
font-size: var(--commodity-name-fontSize, 2rem);
|
||||
line-height: var(--commodity-name-lineHeight, 2.3rem);
|
||||
margin-bottom: var(--commodity-name-marginBottom, 0rem);
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
> .price {
|
||||
font-family: 'KaiseiOpti-Regular';
|
||||
font-weight: 400;
|
||||
font-size: var(--commodity-price-fontSize, 1.6rem);
|
||||
line-height: var(--commodity-price-lineHeight, 2.3rem);
|
||||
margin-top: .8rem;
|
||||
&.is-download {
|
||||
color: #979797;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.btn {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,21 +1,26 @@
|
||||
<template>
|
||||
<section class="section-footer">
|
||||
<div class="footer">
|
||||
<div class="left">
|
||||
<div>About</div>
|
||||
<div>Privacy Policy</div>
|
||||
<div>Terms of Use</div>
|
||||
<div>Disclaimer</div>
|
||||
<div>Site Map</div>
|
||||
<div class="left" v-show="!isHome">
|
||||
<div @click="skip('about')">{{ $t('footer.About') }}</div>
|
||||
<div @click="skip('privacy-policy')">{{ $t('footer.PrivacyPolicy') }}</div>
|
||||
<div @click="skip('terms-of-use')">{{ $t('footer.TermsOfUse') }}</div>
|
||||
<div @click="skip('disclaimer')">{{ $t('footer.Disclaimer') }}</div>
|
||||
<div @click="skip('site-map')">{{ $t('footer.SiteMap') }}</div>
|
||||
</div>
|
||||
<div class="left" v-show="isHome">
|
||||
<div class="text" @click="skip('code-create')">© Code-Create 2026</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<img src="@/assets/images/icons/xiaohongshu.png" />
|
||||
<img src="@/assets/images/icons/linkedin.png" />
|
||||
<img src="@/assets/images/icons/instagram.png" />
|
||||
<img src="@/assets/images/icons/facebook.png" />
|
||||
<img src="@/assets/images/icons/douyin.png" />
|
||||
<img src="@/assets/images/icons/wechat.png" />
|
||||
<div class="text">© Code-Create 2026</div>
|
||||
<img src="@/assets/images/icons/xiaohongshu.png" @click="skip('xiaohongshu')" />
|
||||
<img src="@/assets/images/icons/linkedin.png" @click="skip('linkedin')" />
|
||||
<img src="@/assets/images/icons/instagram.png" @click="skip('instagram')" />
|
||||
<img src="@/assets/images/icons/facebook.png" @click="skip('facebook')" />
|
||||
<img src="@/assets/images/icons/douyin.png" @click="skip('tiktok')" />
|
||||
<img src="@/assets/images/icons/youtube.png" @click="skip('youtube')" />
|
||||
<div v-show="!isHome" class="text" @click="skip('code-create')">
|
||||
© Code-Create 2026
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -23,6 +28,29 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
const props = defineProps({
|
||||
isHome: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
let urlList = ref({
|
||||
xiaohongshu: 'https://xhslink.com/m/5Ony2FapizV',
|
||||
linkedin: 'https://www.linkedin.com/company/code-create-limited/posts?feedView=all',
|
||||
instagram: 'https://www.instagram.com/aida_codecreate?igsh=MzRlODBiNWFlZA== ',
|
||||
facebook: 'https://www.facebook.com/CodeCreateAI',
|
||||
tiktok: 'https://www.tiktok.com/@aida_codecreate',
|
||||
youtube: 'https://www.youtube.com/@Code-Create_AiDA',
|
||||
'code-create': 'https://www.code-create.com/',
|
||||
about: 'https://code-create.com.hk/about-us/ ',
|
||||
'privacy-policy': 'https://code-create.com.hk/privacy-policy/',
|
||||
'terms-of-use': 'https://code-create.com.hk/terms-of-use/',
|
||||
disclaimer: 'https://code-create.com.hk/disclaimer/',
|
||||
'site-map': 'https://code-create.com.hk/site-map/'
|
||||
})
|
||||
const skip = (name: string) => {
|
||||
window.open(urlList.value[name], '_blank')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
@@ -41,6 +69,9 @@
|
||||
color: #585858;
|
||||
display: flex;
|
||||
gap: 4.5rem;
|
||||
> div {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
> .right {
|
||||
display: flex;
|
||||
@@ -49,8 +80,9 @@
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
margin-left: 2rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.text{
|
||||
.text {
|
||||
margin-left: 4rem;
|
||||
font-family: KaiseiOpti-Regular;
|
||||
font-size: 1.2rem;
|
||||
|
||||
@@ -1,50 +1,50 @@
|
||||
<template>
|
||||
<div class="c-svg">
|
||||
<svg
|
||||
:class="svgClass"
|
||||
v-bind="$attrs"
|
||||
:style="{ color: color, fontSize: size/10 + 'rem' }"
|
||||
>
|
||||
<use :href="iconName"></use>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="c-svg">
|
||||
<svg
|
||||
:class="svgClass"
|
||||
v-bind="$attrs"
|
||||
:style="{ color: color, fontSize: size / 10 + 'rem' }"
|
||||
>
|
||||
<use :href="iconName"></use>
|
||||
</svg>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from "vue";
|
||||
const props = defineProps({
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
size: {
|
||||
type: [Number, String],
|
||||
default: 16,
|
||||
},
|
||||
});
|
||||
const iconName = computed(() => `#icon-${props.name}`);
|
||||
const svgClass = computed(() => {
|
||||
if (props.name) return `svg-icon icon-${props.name}`;
|
||||
return "svg-icon";
|
||||
});
|
||||
import { computed } from 'vue'
|
||||
const props = defineProps({
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: ''
|
||||
},
|
||||
size: {
|
||||
type: [Number, String],
|
||||
default: 16
|
||||
}
|
||||
})
|
||||
const iconName = computed(() => `#icon-${props.name}`)
|
||||
const svgClass = computed(() => {
|
||||
if (props.name) return `svg-icon icon-${props.name}`
|
||||
return 'svg-icon'
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.svg-icon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
fill: currentColor;
|
||||
color: var(--svg-icon-color);
|
||||
}
|
||||
.c-svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.svg-icon {
|
||||
width: var(--svg-icon-width, 1em);
|
||||
height: var(--svg-icon-height, 1em);
|
||||
fill: currentColor;
|
||||
color: var(--svg-icon-color);
|
||||
}
|
||||
.c-svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
</style>
|
||||
|
||||
91
src/components/checked-gender.vue
Normal file
@@ -0,0 +1,91 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted, onUnmounted, reactive, toRefs, computed } from "vue";
|
||||
const props = defineProps({
|
||||
list:{
|
||||
type:Array,
|
||||
default:()=>[]
|
||||
},
|
||||
selected:{
|
||||
type:Array,
|
||||
default:()=>[]
|
||||
}
|
||||
})
|
||||
const emit = defineEmits([
|
||||
'update:selected','change'
|
||||
])
|
||||
const checkList = ref([])
|
||||
const checkAll = ref(false)
|
||||
|
||||
watch(()=>props.selected, (newVal, oldVal) => {
|
||||
if(newVal[0] === 'all' && newVal.length === 1){
|
||||
checkList.value = []
|
||||
checkAll.value = true
|
||||
}else{
|
||||
checkList.value = [...newVal]
|
||||
checkAll.value = false
|
||||
}
|
||||
},{immediate:true})
|
||||
|
||||
const handleChange = (val) => {
|
||||
let data = val.filter(item => item !== 'all' && props.selected?.[0] != item)
|
||||
emit('update:selected', data)
|
||||
emit('change', data)
|
||||
}
|
||||
const handleCheckAllChange = (val) => {
|
||||
let data = ['all']
|
||||
emit('update:selected', data)
|
||||
emit('change', data)
|
||||
|
||||
}
|
||||
let data = reactive({
|
||||
})
|
||||
onMounted(()=>{
|
||||
})
|
||||
onUnmounted(()=>{
|
||||
})
|
||||
defineExpose({})
|
||||
const {} = toRefs(data);
|
||||
</script>
|
||||
<template>
|
||||
<div class="all">
|
||||
<el-checkbox
|
||||
v-model="checkAll"
|
||||
@change="handleCheckAllChange"
|
||||
>
|
||||
{{ $t('checked.All') }}
|
||||
</el-checkbox>
|
||||
</div>
|
||||
<el-checkbox-group v-model="checkList" @change="handleChange">
|
||||
<el-checkbox
|
||||
v-for="item in props.list"
|
||||
:key="item.value"
|
||||
:value="item.value"
|
||||
>
|
||||
{{ item.label }}
|
||||
</el-checkbox>
|
||||
</el-checkbox-group>
|
||||
</template>
|
||||
<style lang="less" scoped>
|
||||
.all{
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
.el-checkbox-group{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.2rem;
|
||||
}
|
||||
label{
|
||||
--el-checkbox-font-size: 1.6rem;
|
||||
--el-checkbox-checked-text-color: #232323;
|
||||
--el-checkbox-font-weight: 400;
|
||||
--el-checkbox-height: 2rem;
|
||||
--el-checkbox-checked-bg-color: #232323;
|
||||
--el-checkbox-checked-input-border-color: #232323;
|
||||
--el-checkbox-input-border: 1px solid #232323;
|
||||
font-family: "KaiseiOpti-Regular";
|
||||
line-height: 2rem;
|
||||
.el-checkbox__label{
|
||||
padding-left: 1.4rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,37 +1,52 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, reactive, toRefs, computed } from "vue";
|
||||
import { ref, watch, onMounted, onUnmounted, reactive, toRefs, computed } from "vue";
|
||||
const props = defineProps({
|
||||
list:{
|
||||
type:Array,
|
||||
default:()=>[]
|
||||
},
|
||||
selected:{
|
||||
type:String,
|
||||
default:()=>''
|
||||
type:Array,
|
||||
default:()=>[]
|
||||
}
|
||||
})
|
||||
const emit = defineEmits([
|
||||
'update:selected'
|
||||
'update:selected','change'
|
||||
])
|
||||
const checkList = computed(()=>{
|
||||
if(props.selected[0] === ''){
|
||||
return props.list.map(item => item.value)
|
||||
const checkList = ref([])
|
||||
const checkAll = ref(false)
|
||||
|
||||
watch(()=>props.selected, (newVal, oldVal) => {
|
||||
if(newVal[0] === 'all' && newVal.length === 1){
|
||||
checkList.value = []
|
||||
checkAll.value = true
|
||||
}else{
|
||||
return [...props.selected]
|
||||
checkList.value = [...newVal]
|
||||
checkAll.value = false
|
||||
}
|
||||
})
|
||||
},{immediate:true})
|
||||
const handleChange = (val) => {
|
||||
emit('update:selected', val)
|
||||
}
|
||||
const checkAll = computed(()=>{
|
||||
return checkList.value.length === props.list.length
|
||||
})
|
||||
const handleCheckAllChange = (val) => {
|
||||
if(val){
|
||||
emit('update:selected', props.list.map(item => item.value))
|
||||
let data = val.filter(item => item !== 'all')
|
||||
if(data.length == props.list.length || data.length == 0){
|
||||
data = ['all']
|
||||
}else{
|
||||
emit('update:selected', [])
|
||||
data = [...val]
|
||||
}
|
||||
emit('update:selected', data)
|
||||
emit('change', data)
|
||||
}
|
||||
|
||||
const handleCheckAllChange = (val) => {
|
||||
let data = []
|
||||
if(val && props.selected[0] !== 'all'){
|
||||
data = ['all']
|
||||
// data = props.list.map(item => item.value)
|
||||
emit('update:selected', data)
|
||||
emit('change', data)
|
||||
}else{
|
||||
data = []
|
||||
}
|
||||
|
||||
}
|
||||
let data = reactive({
|
||||
})
|
||||
@@ -48,7 +63,7 @@ const {} = toRefs(data);
|
||||
v-model="checkAll"
|
||||
@change="handleCheckAllChange"
|
||||
>
|
||||
All
|
||||
{{ $t('checked.All') }}
|
||||
</el-checkbox>
|
||||
</div>
|
||||
<el-checkbox-group v-model="checkList" @change="handleChange">
|
||||
|
||||
27
src/directives/avatarLoad.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// 加载图片
|
||||
import avatar from '@/assets/images/avatar.png'
|
||||
export default {
|
||||
name: 'avatarLoad',
|
||||
mounted(el, binding) {
|
||||
loadImage(el, binding.value)
|
||||
},
|
||||
updated(el, binding) {
|
||||
// 当 binding.value 发生变化时重新加载
|
||||
if (binding.oldValue !== binding.value) {
|
||||
loadImage(el, binding.value)
|
||||
}
|
||||
},
|
||||
};
|
||||
function loadImage(el, src) {
|
||||
if (!src) return
|
||||
if (el.src === src) return
|
||||
const img = new Image()
|
||||
img.src = src
|
||||
img.onload = () => {
|
||||
el.src = src
|
||||
}
|
||||
img.onerror = () => {
|
||||
el.src = avatar // 默认头像
|
||||
console.log('图片加载失败:', src)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
export default {
|
||||
install(app) {
|
||||
const directivesList = import.meta.glob('./*.js', { eager: true });
|
||||
const directivesList1 = import.meta.glob('./*.js', { eager: true });
|
||||
const directivesList2 = import.meta.glob('./*.ts', { eager: true });
|
||||
const directivesList = { ...directivesList1, ...directivesList2 };
|
||||
|
||||
// 遍历指令文件实现自动注册
|
||||
Object.keys(directivesList).forEach(key => {
|
||||
app.directive(directivesList[key].default.name, directivesList[key].default);
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import img from '@/assets/images/shopping-cart-null.png'
|
||||
// 加载图片
|
||||
export default {
|
||||
name: 'loadimg',
|
||||
beforeMount(el, binding) {
|
||||
if (!el.src) el.src = img
|
||||
},
|
||||
mounted(el, binding) {
|
||||
const src = binding.value
|
||||
if (el.src === src) return
|
||||
|
||||
307
src/lang/en.ts
@@ -1,6 +1,10 @@
|
||||
import { add } from "lodash-es";
|
||||
|
||||
export default {
|
||||
Login: {
|
||||
signup: 'Sign up',
|
||||
login: 'Log in',
|
||||
logoff: 'Log off',
|
||||
register: 'Register',
|
||||
loginTip: 'Platform integrated with AiDA.<br />AiDA account login required.',
|
||||
name: 'Name',
|
||||
@@ -11,11 +15,12 @@ export default {
|
||||
enterEmail: 'Enter your email',
|
||||
enterPassword: 'Enter your password',
|
||||
enterPasswordAgain: 'Enter your password again',
|
||||
passwordTip: 'You must satisfy ALL password conditions to register.',
|
||||
forgotPassword: 'Forget password?',
|
||||
pleaseInputName: 'Please input the name',
|
||||
nameLengthError: 'Name length must be between {min} and {max} characters',
|
||||
passwordSpecial: 'Must contain special characters',
|
||||
passwordCase: 'Mix of uppercase, lowercase and numbers',
|
||||
passwordCase: 'A combination of numbers and letters',
|
||||
pleaseInputEmail: 'Please input the email',
|
||||
emailFormatError: 'Please input the email again',
|
||||
pleaseInputPassword: 'Please input the password',
|
||||
@@ -37,7 +42,28 @@ export default {
|
||||
wechatLogin: 'Sign in with Wechat',
|
||||
indexTip: 'A multi-agent canvas for rapid, trend driven design iteration.',
|
||||
sendCodeError: 'Send code error',
|
||||
retrievePassword: 'Retrieve password'
|
||||
retrievePassword: 'Retrieve password',
|
||||
emailVerification: 'Email Verification',
|
||||
retrievePasswordTitle: 'Please enter your email address below to verify your identity.',
|
||||
submit: 'Submit',
|
||||
enterNewPassword: 'Enter a new password for<br/><span>{email}</span>',
|
||||
passwordsDoNotMatch: 'Passwords do not match',
|
||||
logOffTip: 'Are you sure to log off?',
|
||||
pleaseLogTip: 'Please log in and try again.'
|
||||
},
|
||||
RegisterSuccess: {
|
||||
title1: 'Welcome to Stylish Parade!',
|
||||
title2: 'Please switch to the Login tab to log in.',
|
||||
title3: 'What awaits you in Stylish Parade',
|
||||
item1title: 'Behind the design',
|
||||
item1tip:
|
||||
'Discover how designers bring ideas to life with AiDA — from first sketch to final look.',
|
||||
item2title: 'Creative digital works',
|
||||
item2tip:
|
||||
'Unlock a growing library of inspiring digital works to refresh your creative mind.',
|
||||
item3title: 'A fashion community',
|
||||
item3tip:
|
||||
'Join a space where fashion speaks — exchange ideas and connect with creators worldwide.'
|
||||
},
|
||||
Settings: {
|
||||
title: 'Settings',
|
||||
@@ -53,7 +79,12 @@ export default {
|
||||
usernamePlaceholder: 'Username',
|
||||
usernameTip: 'Your public username on Stylish Parade.',
|
||||
role: 'ROLE',
|
||||
roleTip: 'Select up to 2 labels that suit you.',
|
||||
roleTip: 'Select up to 2 labels that suit you.'
|
||||
},
|
||||
avatarCrop: {
|
||||
title: 'Crop Avatar',
|
||||
confirm: 'Confirm',
|
||||
processing: 'Processing...'
|
||||
},
|
||||
security: {
|
||||
title: 'Security',
|
||||
@@ -84,6 +115,7 @@ export default {
|
||||
discard: 'DISCARD',
|
||||
edit: 'EDIT',
|
||||
saveChange: 'SAVE CHANGE',
|
||||
verifyEmail: 'VERIFY EMAIL',
|
||||
saving: 'SAVING...'
|
||||
},
|
||||
dialog: {
|
||||
@@ -94,7 +126,7 @@ export default {
|
||||
resendCodeIn: 'Resend Code in {time}'
|
||||
},
|
||||
messages: {
|
||||
enterNewEmailFirst: 'Please enter your new email address first',
|
||||
enterNewEmailFirst: 'Please enter your email address first',
|
||||
invalidEmail: 'Please enter a valid email address',
|
||||
sameEmail: 'Please enter a different email address',
|
||||
alreadyVerified: 'This email has already been verified',
|
||||
@@ -102,7 +134,17 @@ export default {
|
||||
enterVerificationCode: 'Please enter the 6-digit verification code',
|
||||
verificationCompleted: 'Email verification completed',
|
||||
verifyEmailBeforeSave: 'Please verify your new email before saving',
|
||||
settingsUpdated: 'Settings updated'
|
||||
currentPasswordRequired: 'Please enter your current password',
|
||||
passwordLengthError: 'Password length must be between {min} and {max} characters',
|
||||
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',
|
||||
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',
|
||||
@@ -118,8 +160,8 @@ export default {
|
||||
other: 'Other'
|
||||
},
|
||||
languages: {
|
||||
english: 'English',
|
||||
chinese: 'Chinese',
|
||||
ENGLISH: 'English',
|
||||
CHINESE_SIMPLIFIED: 'Chinese Simplified'
|
||||
},
|
||||
regions: {
|
||||
hongKongSar: 'Hong Kong SAR',
|
||||
@@ -127,5 +169,256 @@ export default {
|
||||
singapore: 'Singapore',
|
||||
unitedKingdom: 'United Kingdom'
|
||||
}
|
||||
},
|
||||
Wardrobe: {
|
||||
title: 'My Wardrobe',
|
||||
subtitle: 'Your digital pieces, all in one place',
|
||||
common: {
|
||||
all: 'All'
|
||||
},
|
||||
tabs: {
|
||||
ariaLabel: 'Wardrobe tabs',
|
||||
assets: 'Assets',
|
||||
orders: 'Orders'
|
||||
},
|
||||
sort: {
|
||||
label: 'Sort by',
|
||||
placeholder: 'Select',
|
||||
dateAdded: 'Date Added',
|
||||
selectedFirst: 'Selected First'
|
||||
},
|
||||
assets: {
|
||||
filters: 'Filters',
|
||||
clear: 'Clear',
|
||||
categories: 'Categories',
|
||||
gender: 'Gender',
|
||||
selectedCount: '{count} Selected',
|
||||
selectAll: 'Select All',
|
||||
deselectAll: 'Deselect All',
|
||||
downloadSelected: 'Download Selected',
|
||||
genders: {
|
||||
male: 'Male',
|
||||
female: 'Female'
|
||||
}
|
||||
},
|
||||
orders: {
|
||||
moreItems: '+{count} more',
|
||||
statuses: {
|
||||
all: 'All',
|
||||
paid: 'Paid',
|
||||
unpaid: 'Unpaid',
|
||||
cancelled: 'Canceled'
|
||||
},
|
||||
statusBadges: {
|
||||
paid: 'PAID',
|
||||
unpaid: 'UNPAID',
|
||||
cancelled: 'CANCELED'
|
||||
},
|
||||
actions: {
|
||||
invoice: 'Invoice',
|
||||
completePayment: 'Complete Payment',
|
||||
buyAgain: 'Buy Again'
|
||||
},
|
||||
invoiceMessage: 'Invoice is being prepared. Please refresh later.'
|
||||
},
|
||||
empty: {
|
||||
title: 'Nothing in Wardrobe yet',
|
||||
description: 'Explore the digital item and add pieces to your collection.',
|
||||
action: 'Explore Digital Items'
|
||||
}
|
||||
},
|
||||
ClothesCategories: {
|
||||
blouse: 'Blouse',
|
||||
dress: 'Dress',
|
||||
trousers: 'Trousers',
|
||||
skirt: 'Skirt',
|
||||
tops: 'Tops',
|
||||
bottoms: 'Bottoms',
|
||||
outwear: 'Outwear',
|
||||
others: 'Others'
|
||||
},
|
||||
collectionStory: {
|
||||
back: 'Back to Home',
|
||||
title: 'We’re Seeking',
|
||||
description: 'Fashion Voice Worth Featuring.',
|
||||
button: 'Contact Us if Interested',
|
||||
joinUs: {
|
||||
title: 'Join Our Designer Community',
|
||||
info: 'Join our community of visionaries and publish your collection story.',
|
||||
info2: 'We are currently seeking collections that deeply integrate the AiDA creative workflow, specifically those that resonate through powerful core concepts and evocative inspiration. ',
|
||||
info3: 'This architecture is designed to elevate your exposure through profound "propositional expression," ensuring that soulful, story-driven designs achieve higher market premiums and superior sales conversion.'
|
||||
}
|
||||
},
|
||||
footer: {
|
||||
About: 'About',
|
||||
PrivacyPolicy: 'Privacy Policy',
|
||||
TermsOfUse: 'Terms of Use',
|
||||
Disclaimer: 'Disclaimer',
|
||||
SiteMap: 'Site Map'
|
||||
},
|
||||
brand: {
|
||||
title: 'Brand',
|
||||
description: "Every brand, every story — discover who's behind the collections.",
|
||||
search: 'Enter a brand name...',
|
||||
noFound: 'Brand No Found',
|
||||
noFoundTip: 'Try using another keywords.',
|
||||
searchHistory: 'Searching History',
|
||||
brandItem: {
|
||||
viewProfile: 'View Profile'
|
||||
}
|
||||
},
|
||||
brandDetail: {
|
||||
addShoppingTip: 'Please log in first.',
|
||||
merchantInfo: {
|
||||
Contact: 'Contact',
|
||||
About: 'About'
|
||||
},
|
||||
All: 'All'
|
||||
},
|
||||
digitalItem: {
|
||||
BestSelling: 'Best Selling',
|
||||
Price: 'Price: Low to High',
|
||||
SelectedFirst: 'Selected First',
|
||||
DateAdded: 'Date Added',
|
||||
NewestFirst: 'Newest First',
|
||||
title: 'Digital Item',
|
||||
info: 'Virtual fashion creations collected in your personal archive',
|
||||
sortBy: 'Sort By',
|
||||
noData: 'Nothing in Digital Item',
|
||||
noDataTip: 'Try adjusting your filters or refreshing the page.',
|
||||
MerchantInfo: {
|
||||
Filters: 'Filters',
|
||||
Clear: 'Clear',
|
||||
Categories: 'Categories',
|
||||
Gender: 'Gender'
|
||||
}
|
||||
},
|
||||
checked: {
|
||||
All: 'All'
|
||||
},
|
||||
MainHeader: {
|
||||
Home: 'Home',
|
||||
CollectionStory: 'Collection Story',
|
||||
Brand: 'Brand',
|
||||
DigitalItem: 'Digital Item',
|
||||
HiName: 'Hi, {name}',
|
||||
MyWardrobe: 'My Wardrobe',
|
||||
Notifications: 'Notifications',
|
||||
Settings: 'Settings'
|
||||
},
|
||||
ShoppingCart: {
|
||||
title: 'Shopping Cart',
|
||||
listNullTitle: 'Your Cart is empty',
|
||||
listNullTip: 'Discover new fashion assets and add them to your cart.',
|
||||
dateTimeFormat: 'SM D, YYYY, h:mm A',
|
||||
noLongerAvailable: 'No Longer Available',
|
||||
delistedFromMarketplace: 'Delisted from marketplace',
|
||||
remove: 'Remove',
|
||||
removeTip: 'Are you sure to remove this item?',
|
||||
total: 'Total',
|
||||
digitalAssets: 'Digital assets. Creator retains copyright.',
|
||||
checkout: 'Checkout',
|
||||
exploreDigitalItems: 'Explore Digital Items',
|
||||
orderSummary: 'Order Summary',
|
||||
selected: 'Selected',
|
||||
brands: 'Brands',
|
||||
item: 'item',
|
||||
checkoutSelected: 'Checkout Selected'
|
||||
},
|
||||
digitalDetail: {
|
||||
Sketch: 'Sketch',
|
||||
Illustration: 'Illustration',
|
||||
Product: 'Product Image',
|
||||
EditorialVisual: 'Editorial Visual',
|
||||
Back: 'Back',
|
||||
ReleaseIn: 'Release in',
|
||||
CopyrightLicenseNotice: 'Copyright & License Notice',
|
||||
LicenseIncludedInAsset: 'License Included in Asset',
|
||||
LicenseIncludedInAssetInfo:
|
||||
'All products on this platform are digital assets, not physical goods. Purchase grants a usage license only; copyright and intellectual property rights remain with the original creator, unless otherwise stated.',
|
||||
BuyNow: 'Buy Now',
|
||||
AddToCart: 'Add to Cart'
|
||||
},
|
||||
Home: {
|
||||
IndexTitle:
|
||||
'We’re Seeking<br /><span>Fashion Voice</span><br /><span class="small">Worth Featuring.</span>',
|
||||
IndexTip:
|
||||
'Discover collections through the stories behind their creation. A curated space connecting designers, narratives, and fashion commerce.',
|
||||
DesignerTitle: 'Designer Community',
|
||||
DesignerTip:
|
||||
'Discover the designers shaping AiDA’s creative landscape. <br />Each month, we will showcase a curated selection of their most distinguished works.',
|
||||
SearchBrands: 'Search Brands',
|
||||
AidaTitle: 'Design with AiDA',
|
||||
AidaTip:
|
||||
'Each garment on this platform is where designer vision blooms through AiDA. A tool that nurtures your creativity, never overshadows it. Let your ideas flourish.',
|
||||
TryNow: 'Try Now',
|
||||
DigitalItems: 'Digital Items',
|
||||
DigitalItemsTip1:
|
||||
'AiDA captures your boldest thoughts and transforms them into vivid<br/>digital visions—a virtual realm where creativity collides and evolves.',
|
||||
DigitalItemsTip2:
|
||||
'AiDA accelerates style innovation, shaping daily pieces that keep<br/>your wardrobe in sync with modern fashion’s rhythm.',
|
||||
FooterTip:
|
||||
"Stylish Parade is a commerce platform for designers, serving as AiDA's commercial extension.",
|
||||
FooterAidaTip: 'Bloom your Creativity with AiDA!',
|
||||
Help: 'Help',
|
||||
FAQ: 'FAQ',
|
||||
MyAccount: 'My Account',
|
||||
MyOrders: 'My Orders',
|
||||
PaymentInvoices: 'Payment and Invoices',
|
||||
CopyrightLicense: 'Copyright Licence',
|
||||
Polices: 'Policies',
|
||||
Legal: 'Legal',
|
||||
PrivacyPolicy: 'Privacy Policy',
|
||||
CookiesSettings: 'Cookies Settings',
|
||||
PurchaseConditions: 'Purchase Conditions',
|
||||
Company: 'Company',
|
||||
AboutUs: 'About Us',
|
||||
Offices: 'Offices',
|
||||
JoinWithUs: 'Join with Us'
|
||||
},
|
||||
addShoppingCart: {
|
||||
title: 'Added to your Shopping Cart',
|
||||
statement: 'Digital Assets Only. No physical product included.',
|
||||
button: 'See Shopping Cart',
|
||||
status1: 'The product has been purchased. You can view it in your orders.',
|
||||
status0: 'The order has been placed but not yet paid for.'
|
||||
},
|
||||
area: {
|
||||
chinaMainland: 'China Mainland',
|
||||
hongKongSar: 'Hong Kong SAR',
|
||||
macauSar: 'Macau SAR',
|
||||
taiwan: 'Taiwan',
|
||||
japan: 'Japan',
|
||||
southKorea: 'South Korea',
|
||||
singapore: 'Singapore',
|
||||
unitedStates: 'United States',
|
||||
unitedKingdom: 'United Kingdom',
|
||||
france: 'France',
|
||||
italy: 'Italy',
|
||||
germany: 'Germany',
|
||||
australia: 'Australia',
|
||||
canada: 'Canada'
|
||||
},
|
||||
Pay: {
|
||||
OrderSummary: 'Order Summary',
|
||||
PaymentDetails: 'Payment Details',
|
||||
CreditDebitCard: 'Credit / Debit Card',
|
||||
AgreementText:
|
||||
'I agree to the <span onclick="{onTermsClick}">Terms & Conditions</span> and <span onclick="{onPrivacyClick}">Privacy Policy</span>. All digital item sales are final and non-refundable.',
|
||||
PayWithStripe: 'Pay with Stripe',
|
||||
PayWith: 'Pay with',
|
||||
Cancel: 'Cancel',
|
||||
IHaveCompletedPayment: 'I Have Completed payment',
|
||||
Back: 'Back',
|
||||
PayTip1:
|
||||
"You'll be redirected to a Stripe popup to log in and confirm. No card details are shared with Stylish Parade — Stripe handles all payment security.",
|
||||
PayTip2:
|
||||
'Please keep the window open until the payment is completed. If you are to open the payment window, please check your browser settings to see if pop-ups are being blocked. Points may be delayed after successful payment. Please wait 1-3 minutes and click the credits refresh button.',
|
||||
PurchaseSuccessful: 'Purchase Successful',
|
||||
PurchaseSuccessfulTip:
|
||||
'Your digital items are now available and have been saved in Personal Center → My Wardrobe.',
|
||||
DownloadAllAssets: 'download all Assets',
|
||||
ExportInvoice: 'Export Invoice',
|
||||
ContinueShopping: 'Continue Shopping'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
export default {
|
||||
Login: {
|
||||
signup: '注册',
|
||||
login: '登录',
|
||||
logoff: '退出登录',
|
||||
register: '注册',
|
||||
loginTip: '与 AiDA 集成的平台。<br />需要登录 AiDA 账户。',
|
||||
name: '姓名',
|
||||
@@ -11,11 +13,12 @@ export default {
|
||||
enterEmail: '请输入邮箱',
|
||||
enterPassword: '请输入密码',
|
||||
enterPasswordAgain: '请输入密码确认',
|
||||
passwordTip: '你必须满足所有密码条件才能注册。',
|
||||
forgotPassword: '忘记密码?',
|
||||
pleaseInputName: '请输入姓名',
|
||||
nameLengthError: '姓名长度必须在 {min} 到 {max} 个字符之间',
|
||||
passwordSpecial: '必须包含特殊符号',
|
||||
passwordCase: '大小写字母与数字混合组合',
|
||||
passwordCase: '字母和数字组合',
|
||||
pleaseInputEmail: '请输入邮箱',
|
||||
emailFormatError: '请输入正确的邮箱',
|
||||
pleaseInputPassword: '请输入密码',
|
||||
@@ -37,7 +40,25 @@ export default {
|
||||
wechatLogin: '使用微信登录',
|
||||
indexTip: '一个多智能体画布,用于快速、趋势驱动的设计迭代。',
|
||||
sendCodeError: '发送验证码失败',
|
||||
retrievePassword: '找回密码'
|
||||
retrievePassword: '找回密码',
|
||||
emailVerification: '邮箱验证',
|
||||
retrievePasswordTitle: '请输入您的邮箱地址以验证您的身份。',
|
||||
submit: '提交',
|
||||
enterNewPassword: '请输入新密码<br/><span>{email}</span>',
|
||||
passwordsDoNotMatch: '两次输入密码不一致',
|
||||
logOffTip: '确定退出登录吗?',
|
||||
pleaseLogTip: '请重新登录并重试。'
|
||||
},
|
||||
RegisterSuccess: {
|
||||
title1: '欢迎来到 Stylish Parade!',
|
||||
title2: '请切换到登录选项卡以登录。',
|
||||
title3: '在 Stylish Parade 中等待你的发现',
|
||||
item1title: '设计灵感',
|
||||
item1tip: '了解设计师是如何借助 AiDA 将创意变为现实的——从最初的草图到最终的成品。',
|
||||
item2title: '创意数字作品',
|
||||
item2tip: '解锁一个增长的数字作品库,刷新你的创意。',
|
||||
item3title: '时尚社区',
|
||||
item3tip: '加入一个全球的时尚社区,与设计师分享创意。'
|
||||
},
|
||||
Settings: {
|
||||
title: '设置',
|
||||
@@ -53,7 +74,12 @@ export default {
|
||||
usernamePlaceholder: '请输入用户名',
|
||||
usernameTip: '这是你在 Stylish Parade 上公开显示的用户名。',
|
||||
role: '身份标签',
|
||||
roleTip: '最多选择 2 个符合你的标签。',
|
||||
roleTip: '最多选择 2 个符合你的标签。'
|
||||
},
|
||||
avatarCrop: {
|
||||
title: '裁剪头像',
|
||||
confirm: '确认',
|
||||
processing: '处理中...'
|
||||
},
|
||||
security: {
|
||||
title: '安全',
|
||||
@@ -84,6 +110,7 @@ export default {
|
||||
discard: '放弃',
|
||||
edit: '编辑',
|
||||
saveChange: '保存更改',
|
||||
verifyEmail: '验证邮箱',
|
||||
saving: '保存中...'
|
||||
},
|
||||
dialog: {
|
||||
@@ -102,7 +129,17 @@ export default {
|
||||
enterVerificationCode: '请输入 6 位验证码',
|
||||
verificationCompleted: '邮箱验证完成',
|
||||
verifyEmailBeforeSave: '请先完成新邮箱验证再保存',
|
||||
settingsUpdated: '设置已更新'
|
||||
currentPasswordRequired: '请输入当前密码',
|
||||
passwordLengthError: '密码长度必须在 {min} 到 {max} 个字符之间',
|
||||
passwordSpecial: '密码必须包含特殊符号',
|
||||
passwordCase: '密码必须包含大小写字母和数字',
|
||||
passwordNotSameAsOld: '新密码不能与旧密码相同',
|
||||
settingsUpdated: '设置已更新',
|
||||
avatarTooLarge: '图片过大,最大 5MB。',
|
||||
avatarCropFailed: '头像裁剪失败',
|
||||
avatarUploadUrlMissing: '未获取到上传后的文件地址',
|
||||
avatarUpdated: '头像已更新',
|
||||
avatarUploadFailed: '上传失败'
|
||||
},
|
||||
roles: {
|
||||
fashionEnthusiast: '时尚爱好者',
|
||||
@@ -118,8 +155,8 @@ export default {
|
||||
other: '其他'
|
||||
},
|
||||
languages: {
|
||||
english: '英文',
|
||||
chinese: '中文',
|
||||
ENGLISH: '英文',
|
||||
CHINESE_SIMPLIFIED: '简体中文'
|
||||
},
|
||||
regions: {
|
||||
hongKongSar: '中国香港特别行政区',
|
||||
@@ -127,5 +164,252 @@ export default {
|
||||
singapore: '新加坡',
|
||||
unitedKingdom: '英国'
|
||||
}
|
||||
},
|
||||
Wardrobe: {
|
||||
title: '我的衣橱',
|
||||
subtitle: '你的数字单品尽在此处',
|
||||
common: {
|
||||
all: '全部'
|
||||
},
|
||||
tabs: {
|
||||
ariaLabel: '衣橱标签页',
|
||||
assets: '资产',
|
||||
orders: '订单'
|
||||
},
|
||||
sort: {
|
||||
label: '排序',
|
||||
placeholder: '请选择',
|
||||
dateAdded: '添加日期',
|
||||
selectedFirst: '已选优先'
|
||||
},
|
||||
assets: {
|
||||
filters: '筛选',
|
||||
clear: '清除',
|
||||
categories: '类别',
|
||||
gender: '性别',
|
||||
selectedCount: '已选择 {count} 件',
|
||||
selectAll: '全选',
|
||||
deselectAll: '取消全选',
|
||||
downloadSelected: '下载已选',
|
||||
genders: {
|
||||
male: '男',
|
||||
female: '女'
|
||||
}
|
||||
},
|
||||
orders: {
|
||||
moreItems: '还有 {count} 件',
|
||||
statuses: {
|
||||
all: '全部',
|
||||
paid: '已支付',
|
||||
unpaid: '待支付',
|
||||
cancelled: '已取消'
|
||||
},
|
||||
statusBadges: {
|
||||
paid: '已支付',
|
||||
unpaid: '待支付',
|
||||
cancelled: '已取消'
|
||||
},
|
||||
actions: {
|
||||
invoice: '发票',
|
||||
completePayment: '完成付款',
|
||||
buyAgain: '再次购买'
|
||||
},
|
||||
invoiceMessage: '发票生成中,请稍后刷新页面查看。'
|
||||
},
|
||||
empty: {
|
||||
title: '衣橱暂无内容',
|
||||
description: '探索数字单品,并添加到你的收藏。',
|
||||
action: '探索数字单品'
|
||||
}
|
||||
},
|
||||
ClothesCategories: {
|
||||
blouse: '衬衫',
|
||||
dress: '连衣裙',
|
||||
trousers: '裤子',
|
||||
skirt: '短裙',
|
||||
tops: '上装',
|
||||
bottoms: '下装',
|
||||
outwear: '外套',
|
||||
others: '其他'
|
||||
},
|
||||
collectionStory: {
|
||||
back: '返回首页',
|
||||
title: '我们在寻找',
|
||||
description: '值得被听见的时尚之声',
|
||||
button: '如有兴趣,请联系我们',
|
||||
joinUs: {
|
||||
title: '加入我们的设计师社区,',
|
||||
info: '加入我们的远见者社区,发表你的系列故事。',
|
||||
info2: '我们目前正在寻找深度整合 AiDA 创意工作流程的系列作品,特别是那些通过强大的核心理念和富有感染力的灵感而产生共鸣的作品。',
|
||||
info3: '这一架构旨在通过深刻的‘命题式表达’提升你的曝光度,确保那些有灵魂、由故事驱动的设计能获得更高的市场溢价和卓越的销售转化率。'
|
||||
}
|
||||
},
|
||||
footer: {
|
||||
About: '关于我们',
|
||||
PrivacyPolicy: '隐私政策',
|
||||
TermsOfUse: '条款与条件',
|
||||
Disclaimer: '免责声明',
|
||||
SiteMap: '地图'
|
||||
},
|
||||
brand: {
|
||||
title: '品牌',
|
||||
description: '每一个品牌,每一个故事 — 发现系列作品背后的缔造者。',
|
||||
search: '输入一个品牌名字',
|
||||
noFound: '未找到品牌',
|
||||
noFoundTip: '请尝试使用其他关键词。',
|
||||
searchHistory: '搜索历史',
|
||||
brandItem: {
|
||||
viewProfile: '查看简介'
|
||||
}
|
||||
},
|
||||
brandDetail: {
|
||||
addShoppingTip: '请先登录。',
|
||||
merchantInfo: {
|
||||
Contact: '联系方式',
|
||||
About: '关于我们'
|
||||
},
|
||||
All: '全部'
|
||||
},
|
||||
digitalItem: {
|
||||
BestSelling: '畅销优先',
|
||||
Price: '价格:从低到高',
|
||||
SelectedFirst: '已选优先',
|
||||
DateAdded: '添加日期',
|
||||
NewestFirst: '最新优先',
|
||||
title: '数字藏品',
|
||||
info: '收藏于个人档案中的虚拟时装作品',
|
||||
sortBy: '排序方式',
|
||||
noData: '暂无数字藏品',
|
||||
noDataTip: '请尝试调整筛选条件或刷新页面。',
|
||||
MerchantInfo: {
|
||||
Filters: '筛选',
|
||||
Clear: '清空',
|
||||
Categories: '分类',
|
||||
Gender: '适用性别'
|
||||
}
|
||||
},
|
||||
checked: {
|
||||
All: '全部'
|
||||
},
|
||||
MainHeader: {
|
||||
Home: '首页',
|
||||
CollectionStory: '系列故事',
|
||||
Brand: '品牌',
|
||||
DigitalItem: '数字藏品',
|
||||
HiName: '你好,{name}',
|
||||
MyWardrobe: '我的衣橱',
|
||||
Notifications: '通知',
|
||||
Settings: '设置'
|
||||
},
|
||||
ShoppingCart: {
|
||||
title: '购物车',
|
||||
listNullTitle: '你的购物车为空',
|
||||
listNullTip: '发现新的时尚资产并将其添加到你的购物车。',
|
||||
dateTimeFormat: 'YYYY-MM-DD HH:mm',
|
||||
noLongerAvailable: '已下架',
|
||||
delistedFromMarketplace: '已从市场移除',
|
||||
remove: '删除',
|
||||
removeTip: '确认删除吗?',
|
||||
total: '总金额',
|
||||
digitalAssets: '数字资产。创作者保留版权。',
|
||||
checkout: '结账',
|
||||
exploreDigitalItems: '探索数字单品',
|
||||
orderSummary: '订单信息',
|
||||
selected: '已选',
|
||||
brands: '品牌',
|
||||
item: '数字藏品',
|
||||
checkoutSelected: '结账'
|
||||
},
|
||||
digitalDetail: {
|
||||
Sketch: '草图',
|
||||
Illustration: '插画',
|
||||
Product: '产品',
|
||||
EditorialVisual: '编辑视觉',
|
||||
Back: '返回',
|
||||
ReleaseIn: '发布于',
|
||||
CopyrightLicenseNotice: '版权与许可声明',
|
||||
LicenseIncludedInAsset: '资产包含许可',
|
||||
LicenseIncludedInAssetInfo:
|
||||
'本平台所有产品均为数字资产,非实物商品。购买仅授予使用许可;版权及知识产权仍归原作者所有,除非另有说明。',
|
||||
BuyNow: '立即购买',
|
||||
AddToCart: '加入购物车'
|
||||
},
|
||||
addShoppingCart: {
|
||||
title: '已添加到您的购物车',
|
||||
statement: '仅限数字资产。不包含实体产品。',
|
||||
button: '去购物车',
|
||||
status1: '商品已经购买,可在订单中看到',
|
||||
status0: '商品已下单尚未付款'
|
||||
},
|
||||
area: {
|
||||
chinaMainland: '中国大陆',
|
||||
hongKongSar: '中国香港特别行政区',
|
||||
macauSar: '中国澳门特别行政区',
|
||||
taiwan: '中国台湾',
|
||||
japan: '日本',
|
||||
southKorea: '韩国',
|
||||
singapore: '新加坡',
|
||||
unitedStates: '美国',
|
||||
unitedKingdom: '英国',
|
||||
france: '法国',
|
||||
italy: '意大利',
|
||||
germany: '德国',
|
||||
australia: '澳大利亚',
|
||||
canada: '加拿大'
|
||||
},
|
||||
Home: {
|
||||
IndexTitle: '我们正在寻<br />找值得推广<br />的时尚之声。',
|
||||
IndexTip:
|
||||
'通过了解每件作品背后的故事来探索其收藏品。这是一个精心打造的场所,将设计师、故事和时尚商业紧密相连。',
|
||||
DesignerTitle: '设计师社区',
|
||||
DesignerTip: '发现 AiDA 创意社区的设计师。<br />每月我们都会展示他们最杰出的作品。',
|
||||
SearchBrands: '搜索品牌',
|
||||
AidaTitle: '使用 AiDA 设计',
|
||||
AidaTip:
|
||||
'在这个平台上,每一件服装都是设计师创意在 AiDA 软件中绽放的成果。AiDA 是一款能激发您的创造力的工具,它从不掩盖您的才华。让您的创意尽情绽放吧。',
|
||||
TryNow: '立即试用',
|
||||
DigitalItems: '数字藏品',
|
||||
DigitalItemsTip1:
|
||||
'AiDA 能够捕捉您最激昂的想法,并将其转化为生动的形象。<br />“数字愿景”一个充满创造力碰撞与发展的虚拟世界。',
|
||||
DigitalItemsTip2:
|
||||
'AiDA 风格的创新,打造出实用性强的日常服饰,这些服饰能够经久耐用。<br/>你的衣橱与现代时尚的节奏保持同步。',
|
||||
FooterTip: 'Stylish Parade 是一个为设计师提供的商业平台,它作为 AiDA 商业的商业扩展。',
|
||||
FooterAidaTip: '使用 AiDA 设计',
|
||||
Help: '帮助',
|
||||
FAQ: '常见问题',
|
||||
MyAccount: '我的账户',
|
||||
MyOrders: '我的订单',
|
||||
PaymentInvoices: '支付发票',
|
||||
CopyrightLicense: '版权与许可声明',
|
||||
Polices: '政策',
|
||||
Legal: '法律',
|
||||
PrivacyPolicy: '隐私政策',
|
||||
CookiesSettings: 'Cookie 设置',
|
||||
PurchaseConditions: '购买条件',
|
||||
Company: '公司',
|
||||
AboutUs: '关于我们',
|
||||
Offices: '办公室',
|
||||
JoinWithUs: '加入我们'
|
||||
},
|
||||
Pay: {
|
||||
OrderSummary: '订单信息',
|
||||
PaymentDetails: '支付详情',
|
||||
CreditDebitCard: '信用卡/借记卡',
|
||||
AgreementText:
|
||||
'我同意 <span onclick="{onTermsClick}">使用条款与条件</span> 和 <span onclick="{onPrivacyClick}">隐私政策</span>。所有数字资产销售均为最终销售,不可退款。',
|
||||
PayWithStripe: '使用 Stripe 支付',
|
||||
PayWith: '支付',
|
||||
Cancel: '取消',
|
||||
IHaveCompletedPayment: '我已完成支付',
|
||||
Back: '返回',
|
||||
PayTip1:
|
||||
'您将被重定向到 Stripe 弹窗以登录并确认支付。您的信用卡信息不会被 Stylish Parade 收集。Stripe 处理所有支付安全。',
|
||||
PayTip2:
|
||||
'请保持窗口打开,直到支付完成。如果您打开支付窗口,请检查浏览器设置以查看是否已阻止弹窗。支付完成后,积分可能会有延迟。请等待 1-3 分钟并点击积分刷新按钮。',
|
||||
PurchaseSuccessful: '购买成功',
|
||||
PurchaseSuccessfulTip: '您的数字资产已保存在个人中心的我的衣橱中。',
|
||||
DownloadAllAssets: '下载所有资产',
|
||||
ExportInvoice: '导出发票',
|
||||
ContinueShopping: '继续购物'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,83 +1,168 @@
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import { useUserInfoStore } from '@/stores/userInfo'
|
||||
import { useGlobalStore } from '@/stores/global'
|
||||
import { getUserLanguage, fetchUserProfile } from '@/api/user'
|
||||
import { fetchAllUnreadMessage } from '@/api/notification'
|
||||
import i18n from '@/lang/index'
|
||||
import myEvent from '@/utils/myEvent'
|
||||
|
||||
// 语言映射:后端格式 -> i18n 格式
|
||||
const backendToI18nLanguage: Record<string, string> = {
|
||||
en: 'ENGLISH',
|
||||
'zh-CN': 'CHINESE_SIMPLIFIED'
|
||||
}
|
||||
|
||||
// 语言同步状态缓存(避免每次路由切换都请求)
|
||||
let languageSynced = false
|
||||
|
||||
/**
|
||||
* 路由缓存机制:
|
||||
* 1. 设置路由的meta属性为{ cache: true },表示需要缓存
|
||||
* 2. App.vue中使用RouteCache组件,通过路由的name来进行匹配
|
||||
* 3. 路由的name默认是文件名,如果文件名与name不一致,通过defineOptions({ name: 'componentName' })来设置
|
||||
*
|
||||
* 需要登录路由: meta={ login:true }
|
||||
*/
|
||||
const router = createRouter({
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: () => import('../views/home/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/collectionStory',
|
||||
name: 'collectionStory',
|
||||
component: () => import('../views/collectionStory/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/brand',
|
||||
name: 'brand',
|
||||
component: () => import('../views/brand/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/brand/:id',
|
||||
name: 'brandDetail',
|
||||
component: () => import('../views/brandDetail/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/digitalItem',
|
||||
name: 'digitalItem',
|
||||
component: () => import('../views/digitalItem/index.vue'),
|
||||
meta: { cache: true }
|
||||
},
|
||||
{
|
||||
path: '/digitalItem/:id',
|
||||
name: 'digitalItemDetail',
|
||||
component: () => import('../views/digitalDetail/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
name: 'settings',
|
||||
component: () => import('@/views/setting/index.vue'),
|
||||
meta: { cache: true }
|
||||
},
|
||||
{
|
||||
path: '/shoppingCart', // 购物车
|
||||
name: 'shoppingCart',
|
||||
component: () => import('@/views/shoppingCart/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/notifications',
|
||||
name: 'notifications',
|
||||
component: () => import('@/views/notifications/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/wardrobe',
|
||||
name: 'wardrobe',
|
||||
component: () => import('@/views/wardrobe/index.vue')
|
||||
},
|
||||
{
|
||||
path:'/account',
|
||||
name:'account',
|
||||
component:()=>import('@/views/account/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)',
|
||||
name: '404',
|
||||
component: () => import('../views/404.vue')
|
||||
}
|
||||
],
|
||||
history: createWebHistory('/')
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: () => import('../views/home/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/collectionStory',
|
||||
name: 'collectionStory',
|
||||
component: () => import('../views/collectionStory/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/brand',
|
||||
name: 'brand',
|
||||
component: () => import('../views/brand/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/brand/:id',
|
||||
name: 'brandDetail',
|
||||
component: () => import('../views/brandDetail/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/digitalItem',
|
||||
name: 'digitalItem',
|
||||
component: () => import('../views/digitalItem/index.vue'),
|
||||
meta: { cache: true }
|
||||
},
|
||||
{
|
||||
path: '/digitalItem/:id',
|
||||
name: 'digitalItemDetail',
|
||||
component: () => import('../views/digitalDetail/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/settings',
|
||||
name: 'settings',
|
||||
component: () => import('@/views/setting/index.vue'),
|
||||
meta: { cache: true }
|
||||
},
|
||||
{
|
||||
path: '/shoppingCart', // 购物车
|
||||
name: 'shoppingCart',
|
||||
component: () => import('@/views/shoppingCart/index.vue'),
|
||||
meta: { login: true }
|
||||
},
|
||||
{
|
||||
path: '/notifications',
|
||||
name: 'notifications',
|
||||
component: () => import('@/views/notifications/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/wardrobe',
|
||||
name: 'wardrobe',
|
||||
component: () => import('@/views/wardrobe/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/account',
|
||||
name: 'account',
|
||||
component: () => import('@/views/account/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/pay',
|
||||
name: 'pay',
|
||||
component: () => import('@/views/pay/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)',
|
||||
name: '404',
|
||||
component: () => import('../views/404.vue')
|
||||
}
|
||||
],
|
||||
history: createWebHistory('/')
|
||||
})
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
next()
|
||||
if (to.meta?.login && !useUserInfoStore().state.token) {
|
||||
myEvent.emit('openLoginDialog')
|
||||
}
|
||||
next()
|
||||
})
|
||||
|
||||
router.afterEach(() => {})
|
||||
router.afterEach(async () => {
|
||||
const userInfoStore = useUserInfoStore()
|
||||
const token = userInfoStore.state.token
|
||||
|
||||
if (!token) {
|
||||
languageSynced = false
|
||||
return
|
||||
}
|
||||
|
||||
const avatarUrl = userInfoStore.state.userInfo.avatarUrl
|
||||
if (!avatarUrl) {
|
||||
fetchUserProfile().then((res) => {
|
||||
const profile = res as any
|
||||
if (profile.avatarUrl) {
|
||||
userInfoStore.setAvatarUrl(profile.avatarUrl)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const globalStore = useGlobalStore()
|
||||
fetchAllUnreadMessage().then((res) => {
|
||||
globalStore.setUnredCount(res.totalUnread)
|
||||
})
|
||||
|
||||
if (languageSynced) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取用户语言设置
|
||||
const response = await getUserLanguage()
|
||||
const userLanguage = (response as any)?.language // 后端返回 'en' 或 'zh-CN'
|
||||
|
||||
if (!userLanguage) {
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为 i18n 格式
|
||||
const i18nLanguage = backendToI18nLanguage[userLanguage]
|
||||
|
||||
if (!i18nLanguage) {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前 i18n 语言
|
||||
const currentLocale = i18n.global.locale.value
|
||||
|
||||
// 如果用户语言和本地 i18n 不一致,更新 i18n
|
||||
if (i18nLanguage !== currentLocale) {
|
||||
i18n.global.locale.value = i18nLanguage as 'ENGLISH' | 'CHINESE_SIMPLIFIED'
|
||||
localStorage.setItem('language', i18nLanguage)
|
||||
}
|
||||
|
||||
// 标记已同步
|
||||
languageSynced = true
|
||||
} catch (error) {
|
||||
// 静默失败,不影响页面正常加载
|
||||
console.warn('Failed to sync user language:', error)
|
||||
}
|
||||
})
|
||||
|
||||
export default router
|
||||
|
||||
@@ -2,13 +2,20 @@ import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
export const useGlobalStore = defineStore('global', () => {
|
||||
const state = ref({
|
||||
loading: false
|
||||
loading: false,
|
||||
unReadMessageCount: 0
|
||||
})
|
||||
|
||||
const setLoading = (v: boolean) => { state.value.loading = v }
|
||||
const setLoading = (v: boolean) => {
|
||||
state.value.loading = v
|
||||
}
|
||||
|
||||
const setUnredCount = (number: number) => {
|
||||
state.value.unReadMessageCount = number
|
||||
}
|
||||
return {
|
||||
state,
|
||||
setLoading,
|
||||
setUnredCount
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,70 +1,61 @@
|
||||
// 每一个存储的模块,命名规则use开头,store结尾
|
||||
import { AccountLogout } from '@/api/account'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { removeLocal, setLocal } from '@/utils/local'
|
||||
import MyEvent from '@/utils/myEvent'
|
||||
import router from '@/router'
|
||||
export const useUserInfoStore = defineStore('userInfo', () => {
|
||||
const state = ref({
|
||||
userInfo: {},
|
||||
token: '',
|
||||
generateParams: {
|
||||
stylist: '',
|
||||
sex: '',
|
||||
stylistImage: ''
|
||||
}
|
||||
})
|
||||
const state = ref({
|
||||
userInfo: {
|
||||
userId: "",
|
||||
email: "",
|
||||
username: "",
|
||||
accessToken: "",
|
||||
expiresIn: "",
|
||||
avatarUrl: "",
|
||||
},
|
||||
token: ''
|
||||
})
|
||||
|
||||
// getters
|
||||
const getUserInfo = computed(() => state.value.userInfo)
|
||||
// actions
|
||||
const setUserInfo = (data: any) => {
|
||||
state.value.userInfo = data
|
||||
setToken(data.accessToken)
|
||||
}
|
||||
|
||||
// actions
|
||||
const setUserInfo = (data: any) => {
|
||||
state.value.userInfo = data
|
||||
}
|
||||
const setToken = (data: string) => {
|
||||
state.value.token = data
|
||||
setLocal(data, 'token')
|
||||
}
|
||||
|
||||
const setToken = (data: string) => {
|
||||
state.value.token = data
|
||||
setLocal(data, 'token')
|
||||
}
|
||||
const setAvatarUrl = (url: string) => {
|
||||
state.value.userInfo.avatarUrl = url
|
||||
}
|
||||
|
||||
const getGenerateParams = () => {
|
||||
return state.value.generateParams
|
||||
}
|
||||
const logout = async (reload: boolean = false) => {
|
||||
// 处理退出登录的一些逻辑
|
||||
const userId = state.value.userInfo.userId
|
||||
if (userId) await AccountLogout({ userId })
|
||||
state.value.userInfo = {
|
||||
userId: "",
|
||||
email: "",
|
||||
username: "",
|
||||
accessToken: "",
|
||||
expiresIn: "",
|
||||
avatarUrl: "",
|
||||
}
|
||||
state.value.token = ''
|
||||
removeLocal('token')
|
||||
if (reload) router.go(0)
|
||||
|
||||
const setGenerateParams = (data: any) => {
|
||||
state.value.generateParams = data
|
||||
}
|
||||
}
|
||||
|
||||
const resetGenerateParams = () => {
|
||||
state.value.generateParams = {
|
||||
stylist: '',
|
||||
sex: '',
|
||||
stylistImage: ''
|
||||
}
|
||||
}
|
||||
|
||||
const logOut = () => {
|
||||
// 处理退出登录的一些逻辑
|
||||
return new Promise((resolve) => {
|
||||
state.value.token = ''
|
||||
state.value.userInfo = {}
|
||||
removeLocal('token')
|
||||
resetGenerateParams()
|
||||
MyEvent.emit('clear-generate-state')
|
||||
MyEvent.emit('clear-client-state')
|
||||
MyEvent.emit('clearAllCache')
|
||||
resolve('')
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
state,
|
||||
getUserInfo,
|
||||
setToken,
|
||||
setUserInfo,
|
||||
setGenerateParams,
|
||||
getGenerateParams,
|
||||
resetGenerateParams,
|
||||
logOut
|
||||
}
|
||||
})
|
||||
return {
|
||||
state,
|
||||
setToken,
|
||||
setUserInfo,
|
||||
setAvatarUrl,
|
||||
logout
|
||||
}
|
||||
})
|
||||
71
src/utils/ClothesCategory.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import i18n from '@/lang'
|
||||
|
||||
type Translate = (key: string) => string
|
||||
|
||||
const clothesCategoryConfigs = [
|
||||
{
|
||||
key: 'blouse',
|
||||
value: 'blouse'
|
||||
},
|
||||
{
|
||||
key: 'dress',
|
||||
value: 'dress'
|
||||
},
|
||||
{
|
||||
key: 'trousers',
|
||||
value: 'trousers'
|
||||
},
|
||||
{
|
||||
key: 'skirt',
|
||||
value: 'skirt'
|
||||
},
|
||||
{
|
||||
key: 'tops',
|
||||
value: 'tops'
|
||||
},
|
||||
{
|
||||
key: 'bottoms',
|
||||
value: 'bottoms'
|
||||
},
|
||||
{
|
||||
key: 'outwear',
|
||||
value: 'outwear'
|
||||
},
|
||||
{
|
||||
key: 'others',
|
||||
value: 'others'
|
||||
}
|
||||
] as const
|
||||
|
||||
export type ClothesCategoryValue = (typeof clothesCategoryConfigs)[number]['value']
|
||||
|
||||
export interface ClothesCategory {
|
||||
name: string
|
||||
label: string
|
||||
value: ClothesCategoryValue
|
||||
}
|
||||
|
||||
export const createClothesCategories = (t: Translate = i18n.global.t): ClothesCategory[] =>
|
||||
clothesCategoryConfigs.map(({ key, value }) => ({
|
||||
name: t(`ClothesCategories.${key}`),
|
||||
label: t(`ClothesCategories.${key}`),
|
||||
value
|
||||
}))
|
||||
|
||||
export const ClothesCategories: ClothesCategory[] = clothesCategoryConfigs.map(({ key, value }) => ({
|
||||
get name() {
|
||||
return i18n.global.t(`ClothesCategories.${key}`)
|
||||
},
|
||||
get label() {
|
||||
return i18n.global.t(`ClothesCategories.${key}`)
|
||||
},
|
||||
value
|
||||
}))
|
||||
|
||||
export const useClothesCategories = () => {
|
||||
const { t } = useI18n({ useScope: 'global' })
|
||||
|
||||
return computed(() => createClothesCategories(t))
|
||||
}
|
||||
12
src/utils/UrlList.js
Normal file
@@ -0,0 +1,12 @@
|
||||
export const UrlList = {
|
||||
aida: 'https://www.aida.com.hk/',
|
||||
codeCreate: 'https://www.code-create.com/',
|
||||
terms: 'https://www.code-create.com.hk/terms-of-use/',
|
||||
privacy: 'https://www.code-create.com.hk/privacy-policy/',
|
||||
faq: 'https://code-create.com.hk/help-centre/',
|
||||
aboutUs: 'https://code-create.com.hk/about-us/',
|
||||
joinWithUs: 'https://code-create.com.hk/contact-us/',
|
||||
}
|
||||
export const openView = (url) => {
|
||||
window.open(url, '_blank')
|
||||
}
|
||||
86
src/utils/area.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
export default [
|
||||
{
|
||||
key: 'chinaMainland',
|
||||
name: 'China Mainland',
|
||||
label: 'China Mainland',
|
||||
value: 'China Mainland'
|
||||
},
|
||||
{
|
||||
key: 'hongKongSar',
|
||||
name: 'Hong Kong SAR',
|
||||
label: 'Hong Kong SAR',
|
||||
value: 'Hong Kong SAR'
|
||||
},
|
||||
{
|
||||
key: 'macauSar',
|
||||
name: 'Macau SAR',
|
||||
label: 'Macau SAR',
|
||||
value: 'Macau SAR'
|
||||
},
|
||||
{
|
||||
key: 'taiwan',
|
||||
name: 'Taiwan',
|
||||
label: 'Taiwan',
|
||||
value: 'Taiwan'
|
||||
},
|
||||
{
|
||||
key: 'japan',
|
||||
name: 'Japan',
|
||||
label: 'Japan',
|
||||
value: 'Japan'
|
||||
},
|
||||
{
|
||||
key: 'southKorea',
|
||||
name: 'South Korea',
|
||||
label: 'South Korea',
|
||||
value: 'South Korea'
|
||||
},
|
||||
{
|
||||
key: 'singapore',
|
||||
name: 'Singapore',
|
||||
label: 'Singapore',
|
||||
value: 'Singapore'
|
||||
},
|
||||
{
|
||||
key: 'unitedStates',
|
||||
name: 'United States',
|
||||
label: 'United States',
|
||||
value: 'United States'
|
||||
},
|
||||
{
|
||||
key: 'unitedKingdom',
|
||||
name: 'United Kingdom',
|
||||
label: 'United Kingdom',
|
||||
value: 'United Kingdom'
|
||||
},
|
||||
{
|
||||
key: 'france',
|
||||
name: 'France',
|
||||
label: 'France',
|
||||
value: 'France'
|
||||
},
|
||||
{
|
||||
key: 'italy',
|
||||
name: 'Italy',
|
||||
label: 'Italy',
|
||||
value: 'Italy'
|
||||
},
|
||||
{
|
||||
key: 'germany',
|
||||
name: 'Germany',
|
||||
label: 'Germany',
|
||||
value: 'Germany'
|
||||
},
|
||||
{
|
||||
key: 'australia',
|
||||
name: 'Australia',
|
||||
label: 'Australia',
|
||||
value: 'Australia'
|
||||
},
|
||||
{
|
||||
key: 'canada',
|
||||
name: 'Canada',
|
||||
label: 'Canada',
|
||||
value: 'Canada'
|
||||
}
|
||||
]
|
||||
@@ -2,6 +2,8 @@ import axios from 'axios'
|
||||
import router from '@/router/index'
|
||||
import { useGlobalStore, useUserInfoStore } from '@/stores'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import i18n from '@/lang'
|
||||
const t = i18n.global.t
|
||||
|
||||
// 扩展 AxiosRequestConfig 接口
|
||||
declare module 'axios' {
|
||||
@@ -41,7 +43,7 @@ service.interceptors.request.use(
|
||||
// Do something before request is sent
|
||||
const token = useUserInfoStore().state.token
|
||||
if (token) {
|
||||
config.headers.Authorization = 'Bearer ' + token // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改
|
||||
config.headers.Authorization = token // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改
|
||||
// config.headers['X-Token'] = getLocal('token') // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改
|
||||
}
|
||||
return config
|
||||
@@ -65,6 +67,16 @@ service.interceptors.response.use(
|
||||
if (response.config.url.includes('llm/streamChat')) {
|
||||
return response
|
||||
}
|
||||
// 如果是二进制下载(blob/arraybuffer),直接返回原始 response 以便调用方处理文件
|
||||
if (
|
||||
response.config.responseType === 'blob' ||
|
||||
response.config.responseType === 'arraybuffer' ||
|
||||
response.headers['content-type'] === 'application/octet-stream'
|
||||
) {
|
||||
removePending(response.config)
|
||||
if (response.config.loading) closeLoading()
|
||||
return response
|
||||
}
|
||||
|
||||
// 已完成请求的删除请求中数组
|
||||
removePending(response.config)
|
||||
@@ -75,9 +87,10 @@ service.interceptors.response.use(
|
||||
const res = response.data
|
||||
// 处理异常的情况
|
||||
// console.log(res)
|
||||
if (res.code != 200) {
|
||||
ElMessage.error(res.message)
|
||||
return Promise.reject(new Error(res.errMsg || res.message || 'error'))
|
||||
if (res.errCode != 0) {
|
||||
let msg = res.errMsg || res.message || 'error'
|
||||
ElMessage.error(msg)
|
||||
return Promise.reject(new Error(msg))
|
||||
} else {
|
||||
// 默认只返回data,不返回状态码和message
|
||||
// 通过 meta 中的 responseAll 配置来取决后台是否返回所有数据(包括状态码,message和data)
|
||||
@@ -106,11 +119,10 @@ service.interceptors.response.use(
|
||||
// })
|
||||
ElMessage({
|
||||
type: 'error',
|
||||
message: 'Please log in and try again.',
|
||||
message: t("Login.pleaseLogTip"),
|
||||
duration: 5000
|
||||
})
|
||||
router.push('/login')
|
||||
useUserInfoStore().logOut(false)
|
||||
useUserInfoStore().logout()
|
||||
return Promise.reject(false)
|
||||
}
|
||||
error.config && removePending(error.config)
|
||||
|
||||
@@ -210,4 +210,4 @@ export function FormatBytes(bytes, options: { decimals?: number, unitBig?: boole
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
const value = bytes / Math.pow(k, i);
|
||||
return `${Number(value.toFixed(decimals))} ${sizes[i]}`;
|
||||
}
|
||||
}
|
||||
|
||||
252
src/utils/websocket.ts
Normal file
@@ -0,0 +1,252 @@
|
||||
/**
|
||||
* WebSocket 管理类
|
||||
*/
|
||||
import { useGlobalStore } from '@/stores'
|
||||
|
||||
class WebSocketManager {
|
||||
private ws: WebSocket | null = null
|
||||
private url: string = ''
|
||||
private reconnectTimer: number | null = null
|
||||
private reconnectAttempts: number = 0
|
||||
private maxReconnectAttempts: number = 5
|
||||
private reconnectInterval: number = 3000
|
||||
private heartbeatTimer: number | null = null
|
||||
private heartbeatInterval: number = 30000
|
||||
private isManualClose: boolean = false
|
||||
|
||||
/**
|
||||
* 连接 WebSocket
|
||||
* @param token 用户 token
|
||||
*/
|
||||
connect(token: string) {
|
||||
if (!token) {
|
||||
console.warn('WebSocket: token 为空,无法建立连接')
|
||||
return
|
||||
}
|
||||
|
||||
// 如果已经有连接在进行中,先关闭
|
||||
if (this.ws) {
|
||||
if (this.ws.readyState === WebSocket.CONNECTING) {
|
||||
console.log('WebSocket 正在连接中,跳过重复连接')
|
||||
return
|
||||
}
|
||||
if (this.ws.readyState === WebSocket.OPEN) {
|
||||
console.log('WebSocket 已连接,跳过重复连接')
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 根据当前页面协议自动选择 ws 或 wss
|
||||
// const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const protocol = 'wss:'
|
||||
|
||||
// 从环境变量获取 WebSocket 主机地址
|
||||
// const wsHost = import.meta.env.VITE_WS_HOST || '18.167.251.121:10094'
|
||||
const wsHost = 'www.develop-ms.api.aida.com.hk'
|
||||
|
||||
this.url = `${protocol}//${wsHost}/ws?token=${token}`
|
||||
this.isManualClose = false
|
||||
|
||||
console.log('WebSocket 开始连接:', this.url.replace(/token=.+/, 'token=***'))
|
||||
|
||||
try {
|
||||
this.ws = new WebSocket(this.url)
|
||||
this.initEventHandlers()
|
||||
} catch (error) {
|
||||
console.error('WebSocket 连接失败:', error)
|
||||
this.reconnect(token)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化事件处理器
|
||||
*/
|
||||
private initEventHandlers() {
|
||||
if (!this.ws) return
|
||||
|
||||
this.ws.onopen = () => {
|
||||
console.log('✅ WebSocket 连接成功')
|
||||
this.reconnectAttempts = 0
|
||||
this.startHeartbeat()
|
||||
}
|
||||
|
||||
this.ws.onmessage = (event) => {
|
||||
console.log('📨 WebSocket 收到消息:', event.data)
|
||||
try {
|
||||
const data = JSON.parse(event.data)
|
||||
this.handleMessage(data)
|
||||
} catch (error) {
|
||||
console.error('WebSocket 消息解析失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
this.ws.onerror = (error) => {
|
||||
console.error('❌ WebSocket 错误:', error)
|
||||
console.error('可能的原因:')
|
||||
console.error('1. 服务器未运行或地址错误')
|
||||
console.error('2. Token 无效或已过期')
|
||||
console.error('3. 网络连接问题')
|
||||
console.error('4. 服务器拒绝连接')
|
||||
}
|
||||
|
||||
this.ws.onclose = (event) => {
|
||||
const closeReasons: Record<number, string> = {
|
||||
1000: '正常关闭',
|
||||
1001: '端点离开',
|
||||
1002: '协议错误',
|
||||
1003: '不支持的数据类型',
|
||||
1006: '连接异常关闭(可能是网络问题或服务器未响应)',
|
||||
1007: '数据格式错误',
|
||||
1008: '违反策略',
|
||||
1009: '消息过大',
|
||||
1010: '扩展协商失败',
|
||||
1011: '服务器错误',
|
||||
1015: 'TLS 握手失败'
|
||||
}
|
||||
|
||||
const reason = closeReasons[event.code] || '未知原因'
|
||||
console.log(`🔌 WebSocket 连接关闭 [代码: ${event.code}] ${reason}`)
|
||||
if (event.reason) {
|
||||
console.log('关闭原因:', event.reason)
|
||||
}
|
||||
|
||||
this.stopHeartbeat()
|
||||
|
||||
// 如果是异常关闭(非正常关闭码),才尝试重连
|
||||
if (!this.isManualClose && event.code !== 1000) {
|
||||
const token = this.extractTokenFromUrl()
|
||||
if (token) {
|
||||
this.reconnect(token)
|
||||
}
|
||||
} else if (event.code === 1000) {
|
||||
console.log('连接正常关闭,不进行重连')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理接收到的消息
|
||||
* @param data 消息数据
|
||||
*/
|
||||
private handleMessage(data: any) {
|
||||
// 这里可以根据消息类型进行不同的处理
|
||||
// 例如:通知、消息推送等
|
||||
if (data.type === 'unread') {
|
||||
// 处理通知消息
|
||||
const info = data.data
|
||||
// console.log('收到通知-----:', info)
|
||||
useGlobalStore().setUnredCount(info.unreadCount)
|
||||
// 可以触发自定义事件或调用回调函数
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
* @param data 要发送的数据
|
||||
*/
|
||||
send(data: any) {
|
||||
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
||||
const message = typeof data === 'string' ? data : JSON.stringify(data)
|
||||
this.ws.send(message)
|
||||
} else {
|
||||
console.warn('WebSocket 未连接,无法发送消息')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重连
|
||||
* @param token 用户 token
|
||||
*/
|
||||
private reconnect(token: string) {
|
||||
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
||||
console.error('❌ WebSocket 重连次数已达上限,停止重连')
|
||||
console.error('请检查:')
|
||||
console.error('1. 服务器是否正常运行')
|
||||
console.error('2. Token 是否有效')
|
||||
console.error('3. 网络连接是否正常')
|
||||
return
|
||||
}
|
||||
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer)
|
||||
}
|
||||
|
||||
this.reconnectAttempts++
|
||||
const delay = this.reconnectInterval * this.reconnectAttempts // 递增延迟
|
||||
console.log(
|
||||
`🔄 WebSocket 将在 ${delay / 1000} 秒后尝试重连 (${this.reconnectAttempts}/${
|
||||
this.maxReconnectAttempts
|
||||
})`
|
||||
)
|
||||
|
||||
this.reconnectTimer = window.setTimeout(() => {
|
||||
this.connect(token)
|
||||
}, delay)
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始心跳
|
||||
*/
|
||||
private startHeartbeat() {
|
||||
this.stopHeartbeat()
|
||||
this.heartbeatTimer = window.setInterval(() => {
|
||||
this.send({ type: 'heartbeat', timestamp: Date.now() })
|
||||
}, this.heartbeatInterval)
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止心跳
|
||||
*/
|
||||
private stopHeartbeat() {
|
||||
if (this.heartbeatTimer) {
|
||||
clearInterval(this.heartbeatTimer)
|
||||
this.heartbeatTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从 URL 中提取 token
|
||||
*/
|
||||
private extractTokenFromUrl(): string | null {
|
||||
if (!this.url) return null
|
||||
const match = this.url.match(/token=([^&]+)/)
|
||||
return match ? match[1] : null
|
||||
}
|
||||
|
||||
/**
|
||||
* 关闭连接
|
||||
*/
|
||||
close() {
|
||||
this.isManualClose = true
|
||||
this.stopHeartbeat()
|
||||
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer)
|
||||
this.reconnectTimer = null
|
||||
}
|
||||
|
||||
if (this.ws) {
|
||||
this.ws.close()
|
||||
this.ws = null
|
||||
}
|
||||
|
||||
console.log('WebSocket 已手动关闭')
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接状态
|
||||
*/
|
||||
getReadyState(): number {
|
||||
return this.ws ? this.ws.readyState : WebSocket.CLOSED
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否已连接
|
||||
*/
|
||||
isConnected(): boolean {
|
||||
return this.ws !== null && this.ws.readyState === WebSocket.OPEN
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
export const wsManager = new WebSocketManager()
|
||||
@@ -25,25 +25,25 @@ const {} = toRefs(data);
|
||||
<div class="item">
|
||||
<div class="left">
|
||||
<div class="portrait">
|
||||
<img :src="item.portrait" alt="">
|
||||
<img v-avatarLoad="item.avatar" alt="">
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="name">{{ item.name }}</div>
|
||||
<div class="name">{{ item.shopName }}</div>
|
||||
<div class="collection">
|
||||
{{ item.collectionsName }} |
|
||||
{{ item?.collections?.length || 0 }} Collections
|
||||
{{ item.ownerName }} |
|
||||
{{ item?.listingTotal || 0 }} Collections
|
||||
</div>
|
||||
<div class="view-profile" @click="viewProfile(item)">View Profile</div>
|
||||
<div class="view-profile" @click="viewProfile(item)">{{ $t('brand.brandItem.viewProfile') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="img-list">
|
||||
<div class="img-item" v-for="itemImg in item?.collections?.slice(0,5)" :key="item.id">
|
||||
<div class="img-item" v-for="itemImg in item?.covers" :key="itemImg">
|
||||
<img :src="itemImg" alt="">
|
||||
</div>
|
||||
</div>
|
||||
<div class="more">
|
||||
<div class="icon" v-show="item?.collections?.length > 5">
|
||||
<div class="icon" v-show="item?.covers?.length == 5">
|
||||
<svgIcon name="brand-more" size="24" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,8 +4,8 @@ import { useRouter } from "vue-router";
|
||||
import myEvent from '@/utils/myEvent'
|
||||
import scListNull from '@/views/shoppingCart/sc-list-null.vue'
|
||||
import brandItem from '@/views/brand/brand-item.vue'
|
||||
import { getDesignerList } from '@/api/brand'
|
||||
|
||||
import img from '@/assets/images/collectionStory/Rectangle.png'
|
||||
//const props = defineProps({
|
||||
//})
|
||||
//const emit = defineEmits([
|
||||
@@ -23,87 +23,99 @@ const getMerchantData = reactive({
|
||||
isShowMark:false,
|
||||
isNoData:false,
|
||||
})
|
||||
const list = ref([
|
||||
' 1',
|
||||
'Brand 2',
|
||||
'Brand 3',
|
||||
'1213123 4',
|
||||
'Brand 4',
|
||||
'2222 4',
|
||||
'B23rand 4',
|
||||
'Bran112222d 4',
|
||||
' 4',
|
||||
const searchHistory = ref([
|
||||
|
||||
])
|
||||
|
||||
let changeSearchBrandTime = null
|
||||
|
||||
const changeSearchBrand = () => {
|
||||
if(!searchBrand.value)return
|
||||
clearTimeout(changeSearchBrandTime)
|
||||
changeSearchBrandTime = setTimeout(()=>{
|
||||
getMerchantData.pageNum = 1
|
||||
merchantList.value = []
|
||||
getMerchantData.isShowMark = false
|
||||
getMerchantData.isNoData = false
|
||||
},300)
|
||||
getDesignerList({
|
||||
keyword: searchBrand.value,
|
||||
}).then((res)=>{
|
||||
merchantList.value.push(...res)
|
||||
})
|
||||
},500)
|
||||
|
||||
// changeSearchBrandTime = setTimeout(()=>{
|
||||
// getMerchantData.pageNum = 1
|
||||
// getMerchantData.isShowMark = false
|
||||
// getMerchantData.isNoData = false
|
||||
// },300)
|
||||
}
|
||||
|
||||
const getBrandList = async () => {
|
||||
if(getMerchantData.isShowMark && !getMerchantData.isNoData)return
|
||||
getMerchantData.isShowMark = true
|
||||
let value = {
|
||||
pageSize: getMerchantData.pageSize,
|
||||
pageNum: getMerchantData.pageNum,
|
||||
status: 1,
|
||||
}
|
||||
setTimeout(()=>{
|
||||
if(merchantList.value.length >= 5){
|
||||
getMerchantData.isNoData = true
|
||||
merchantList.value = []
|
||||
return
|
||||
}
|
||||
getMerchantData.pageNum += 1
|
||||
merchantList.value.push({
|
||||
name:'Roaming Clouds',
|
||||
portrait: img,
|
||||
collectionsName:'by Lian Su ',
|
||||
collections:[
|
||||
img,img,img,
|
||||
],
|
||||
})
|
||||
getMerchantData.isShowMark = false
|
||||
},1000)
|
||||
// await getPublishList(value).then((res)=>{
|
||||
// if(res.content.length == 0)getMerchantData.isNoData = true
|
||||
// getMerchantData.pageNum += 1
|
||||
// list.value.push(...res.content)
|
||||
// })
|
||||
}
|
||||
const vObserve = {
|
||||
mounted (el,binding) {
|
||||
getMerchantData.isShowMark = false
|
||||
getMerchantData.isNoData = false
|
||||
new IntersectionObserver(
|
||||
(entries, observer) => {
|
||||
// 如果不是相交,则直接返回
|
||||
// console.log(entries[0]);
|
||||
if (!entries[0].intersectionRatio) return;
|
||||
getMerchantData.pageNum += 1
|
||||
binding.value()
|
||||
},
|
||||
// { root:worksPage }
|
||||
).observe(el);
|
||||
}
|
||||
}
|
||||
// const getBrandList = async () => {
|
||||
// if(getMerchantData.isShowMark && !getMerchantData.isNoData)return
|
||||
// getMerchantData.isShowMark = true
|
||||
// let value = {
|
||||
// pageSize: getMerchantData.pageSize,
|
||||
// pageNum: getMerchantData.pageNum,
|
||||
// status: 1,
|
||||
// }
|
||||
// setTimeout(()=>{
|
||||
// if(merchantList.value.length >= 5){
|
||||
// getMerchantData.isNoData = true
|
||||
// merchantList.value = []
|
||||
// return
|
||||
// }
|
||||
// getMerchantData.pageNum += 1
|
||||
// merchantList.value.push({
|
||||
// name:'Roaming Clouds',
|
||||
// portrait: img,
|
||||
// collectionsName:'by Lian Su ',
|
||||
// collections:[
|
||||
// img,img,img,
|
||||
// ],
|
||||
// })
|
||||
// getMerchantData.isShowMark = false
|
||||
// },1000)
|
||||
// // await getPublishList(value).then((res)=>{
|
||||
// // if(res.content.length == 0)getMerchantData.isNoData = true
|
||||
// // getMerchantData.pageNum += 1
|
||||
// // list.value.push(...res.content)
|
||||
// // })
|
||||
// }
|
||||
// const vObserve = {
|
||||
// mounted (el,binding) {
|
||||
// getMerchantData.isShowMark = false
|
||||
// getMerchantData.isNoData = false
|
||||
// new IntersectionObserver(
|
||||
// (entries, observer) => {
|
||||
// // 如果不是相交,则直接返回
|
||||
// // console.log(entries[0]);
|
||||
// if (!entries[0].intersectionRatio) return;
|
||||
// getMerchantData.pageNum += 1
|
||||
// binding.value()
|
||||
// },
|
||||
// // { root:worksPage }
|
||||
// ).observe(el);
|
||||
// }
|
||||
// }
|
||||
|
||||
const deleteHistory = (item) => {
|
||||
list.value = list.value.filter((i) => i != item)
|
||||
searchHistory.value = searchHistory.value.filter((i) => i != item)
|
||||
localStorage.setItem('brandSearchHistory', JSON.stringify(searchHistory.value));
|
||||
}
|
||||
const viewProfile = (item) => {
|
||||
if(!searchHistory.value.includes(searchBrand.value))searchHistory.value.push(searchBrand.value)
|
||||
localStorage.setItem('brandSearchHistory', JSON.stringify(searchHistory.value));
|
||||
router.push({
|
||||
path:'/brand/1',
|
||||
path:'/brand/'+item.sellerId,
|
||||
})
|
||||
}
|
||||
|
||||
const setSearchHistory = (item) => {
|
||||
searchBrand.value = item
|
||||
changeSearchBrand()
|
||||
}
|
||||
|
||||
onMounted(()=>{
|
||||
const value = localStorage.getItem('brandSearchHistory');
|
||||
if(value)searchHistory.value = JSON.parse(value)
|
||||
})
|
||||
onUnmounted(()=>{
|
||||
})
|
||||
@@ -111,35 +123,35 @@ defineExpose({})
|
||||
const {} = toRefs(data);
|
||||
</script>
|
||||
<template>
|
||||
<div class="brand">
|
||||
<div class="brand">
|
||||
<div class="header-img" :class="{'active': searchBrand.length > 0}">
|
||||
<img src="@/assets/images/brand/brandBg.png" alt="">
|
||||
<div class="text-box">
|
||||
<div class="title">Brand</div>
|
||||
<span>Every brand, every story — discover who's behind the collections.</span>
|
||||
<div class="title">{{ $t('brand.title') }}</div>
|
||||
<span>{{ $t('brand.description') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="input">
|
||||
<input type="text" v-model="searchBrand" @input="changeSearchBrand" placeholder="Search brand">
|
||||
<div class="icon">
|
||||
<input type="text" v-model="searchBrand" @input="changeSearchBrand" :placeholder="$t('brand.search')">
|
||||
<div class="icon" @click="changeSearchBrand">
|
||||
<SvgIcon name="brand-search" size="32" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="merchantList" v-if="searchBrand.length > 0">
|
||||
<brand-item v-for="item in merchantList" :key="item.name" :item="item" @viewProfile="viewProfile"></brand-item>
|
||||
<brand-item v-for="item in merchantList" :key="item.sellerId" :item="item" @viewProfile="viewProfile"></brand-item>
|
||||
<div class="end" v-show="!getMerchantData.isNoData && !getMerchantData.isShowMark">- The End-</div>
|
||||
<div v-show="!getMerchantData.isNoData" class="material_content_list_loding">
|
||||
<!-- <div v-show="!getMerchantData.isNoData" class="material_content_list_loding">
|
||||
<span class="page_loading" v-show="!getMerchantData.isShowMark" v-observe="getBrandList"></span>
|
||||
<img v-if="getMerchantData.isShowMark" src="@/assets/images/brand/brandLoading.gif" alt="">
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="merchantListNull" v-if="getMerchantData.isNoData && searchBrand.length > 0">
|
||||
<sc-list-null
|
||||
nullImage="brand"
|
||||
:showButton="false"
|
||||
title="Brand No Found"
|
||||
tip="Try using another keywords."
|
||||
:title="$t('brand.noFound')"
|
||||
:tip="$t('brand.noFoundTip')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -148,10 +160,10 @@ const {} = toRefs(data);
|
||||
<div class="icon">
|
||||
<SvgIcon name="brand-time" size="20" />
|
||||
</div>
|
||||
<span>Searching History</span>
|
||||
<span>{{ $t('brand.searchHistory') }}</span>
|
||||
</div>
|
||||
<div class="history">
|
||||
<div v-for="item in list" :key="item" @click.stop="searchBrand = item" class="item">
|
||||
<div v-for="item in searchHistory" :key="item" @click.stop="setSearchHistory(item)" class="item">
|
||||
<span>{{item}}</span>
|
||||
<div class="icon" @click.stop="deleteHistory(item)">
|
||||
<SvgIcon name="brand-delete" size="18" />
|
||||
@@ -176,6 +188,7 @@ const {} = toRefs(data);
|
||||
position: relative;
|
||||
height: 34.4rem;
|
||||
transition: all .3s;
|
||||
flex-shrink: 0;
|
||||
&.active{
|
||||
height: 14.7rem;
|
||||
}
|
||||
@@ -215,7 +228,7 @@ const {} = toRefs(data);
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
// overflow: hidden;
|
||||
> .input{
|
||||
width: 66.6rem;
|
||||
display: flex;
|
||||
@@ -274,18 +287,21 @@ const {} = toRefs(data);
|
||||
> .merchantList{
|
||||
width: 121.8rem;
|
||||
margin-top: 6rem;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
gap: 3.2rem;
|
||||
// flex: 1;
|
||||
// overflow-y: auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
::-webkit-scrollbar{
|
||||
display: none;
|
||||
}
|
||||
:deep(.item){
|
||||
margin-top: 2.1rem;
|
||||
padding-bottom: 2.1rem;
|
||||
}
|
||||
.end{
|
||||
font-family: 'KaiseiOpti-Regular';
|
||||
font-weight: 400;
|
||||
font-size: 12px;
|
||||
font-size: 1.2rem;
|
||||
line-height: 140%;
|
||||
height: 7rem;
|
||||
display: flex;
|
||||
|
||||
@@ -1,54 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
|
||||
import img from "@/assets/images/collectionStory/Rectangle.png";
|
||||
//const props = defineProps({
|
||||
//})
|
||||
const emit = defineEmits([
|
||||
'addShopping'
|
||||
])
|
||||
let data = reactive({
|
||||
})
|
||||
const list = ref([
|
||||
{
|
||||
url: img,
|
||||
title: "Windswept Burden",
|
||||
price: "$100.00",
|
||||
},{
|
||||
url: img,
|
||||
title: "Windswept Burden",
|
||||
price: "$100.00",
|
||||
},{
|
||||
url: img,
|
||||
title: "Windswept Burden",
|
||||
price: "$100.00",
|
||||
},{
|
||||
url: img,
|
||||
title: "Windswept Burden",
|
||||
price: "$100.00",
|
||||
},{
|
||||
url: img,
|
||||
title: "Windswept Burden",
|
||||
price: "$100.00",
|
||||
},{
|
||||
url: img,
|
||||
title: "Windswept Burden",
|
||||
price: "$100.00",
|
||||
},{
|
||||
url: img,
|
||||
title: "Windswept Burden",
|
||||
price: "$100.00",
|
||||
import { getlistingListApi } from "@/api/listing";
|
||||
|
||||
const props = defineProps({
|
||||
id: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
const emit = defineEmits([
|
||||
'addShopping','openDetail'
|
||||
])
|
||||
const type = ref('All')
|
||||
const getMerchantData = reactive({
|
||||
pageSize: 10,
|
||||
pageNum: 1,
|
||||
isShowMark:false,
|
||||
isNoData:false,
|
||||
})
|
||||
const listingList = ref([
|
||||
])
|
||||
const type = ref('all')
|
||||
const addShopping = (item) => {
|
||||
emit('addShopping', item)
|
||||
}
|
||||
const setType = (val) => {
|
||||
type.value = val
|
||||
listingList.value = []
|
||||
getMerchantData.pageNum = 1
|
||||
getMerchantData.isShowMark = false
|
||||
getMerchantData.isNoData = false
|
||||
}
|
||||
const vObserve = {
|
||||
mounted (el,binding) {
|
||||
getMerchantData.isShowMark = false
|
||||
getMerchantData.isNoData = false
|
||||
new IntersectionObserver(
|
||||
(entries, observer) => {
|
||||
// 如果不是相交,则直接返回
|
||||
// console.log(entries[0]);
|
||||
if (!entries[0].intersectionRatio) return;
|
||||
binding.value()
|
||||
getMerchantData.pageNum += 1
|
||||
},
|
||||
// { root:worksPage }
|
||||
).observe(el);
|
||||
}
|
||||
}
|
||||
const getListingList = () => {
|
||||
getMerchantData.isShowMark = true
|
||||
getMerchantData.isNoData = false
|
||||
getlistingListApi({
|
||||
sellerId: props.id,
|
||||
designFor: type.value,
|
||||
pageNum: getMerchantData.pageNum,
|
||||
pageSize: getMerchantData.pageSize,
|
||||
}).then((res)=>{
|
||||
if(res.content.length == 0)getMerchantData.isNoData = true
|
||||
listingList.value.push(...res.content)
|
||||
getMerchantData.isShowMark = false
|
||||
})
|
||||
}
|
||||
onMounted(()=>{
|
||||
})
|
||||
onUnmounted(()=>{
|
||||
})
|
||||
defineExpose({})
|
||||
const {} = toRefs(data);
|
||||
</script>
|
||||
<template>
|
||||
<div class="commodityList">
|
||||
@@ -57,14 +73,18 @@ const {} = toRefs(data);
|
||||
Items
|
||||
</div>
|
||||
<div class="menu">
|
||||
<div :class="{'active': type === 'All'}" @click="type = 'All'">All</div>
|
||||
<div :class="{'active': type === 'Male'}" @click="type = 'Male'">Male</div>
|
||||
<div :class="{'active': type === 'Female'}" @click="type = 'Female'">Female</div>
|
||||
<div :class="{'active': type === 'all'}" @click="setType('all')">{{ $t('brandDetail.All') }}</div>
|
||||
<div :class="{'active': type === 'male'}" @click="setType('male')">{{ $t('Wardrobe.assets.genders.male') }}</div>
|
||||
<div :class="{'active': type === 'female'}" @click="setType('female')">{{ $t('Wardrobe.assets.genders.female') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="list">
|
||||
<div class="item" v-for="item in list" :key="item.url">
|
||||
<CommodityItem :url="item.url" :name="item.title" :price="item.price" @addShopping="addShopping(item)"></CommodityItem>
|
||||
<div class="item" v-for="item in listingList" :key="item.url">
|
||||
<CommodityItem :url="item.cover" :name="item.title" :price="item.price" @addShopping="addShopping(item)" @openDetail="$emit('openDetail', item)"></CommodityItem>
|
||||
</div>
|
||||
<div v-show="!getMerchantData.isNoData" class="material_content_list_loding">
|
||||
<span class="page_loading" v-show="!getMerchantData.isShowMark" v-observe="getListingList"></span>
|
||||
<img v-if="getMerchantData.isShowMark" src="@/assets/images/brand/brandLoading.gif" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -77,8 +97,8 @@ const {} = toRefs(data);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.header{
|
||||
position: sticky;
|
||||
top: 0;
|
||||
// position: sticky;
|
||||
// top: 0;
|
||||
z-index: 2;
|
||||
background-color: #fff;
|
||||
.title{
|
||||
@@ -130,10 +150,9 @@ const {} = toRefs(data);
|
||||
// align-content: start;
|
||||
// grid-template-columns: repeat(3, 1fr);
|
||||
overflow: hidden;
|
||||
|
||||
display: grid;
|
||||
align-content: start;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 28rem), 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(100%, 28rem), 1fr));
|
||||
border-top: 0.5px solid #585858;
|
||||
padding: .5px 0 0 .5px;
|
||||
/* 垂直线(右边框) */
|
||||
@@ -144,6 +163,26 @@ const {} = toRefs(data);
|
||||
border-right: 0.5px solid #585858;
|
||||
margin-right: -1px;
|
||||
margin-bottom: -1px;
|
||||
min-width: 0;
|
||||
}
|
||||
> .material_content_list_loding{
|
||||
width: 100%;
|
||||
height: 5rem;
|
||||
aspect-ratio: 1/1;
|
||||
grid-column: 1 / -1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
justify-content: center;
|
||||
> .page_loading{
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
}
|
||||
> img{
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,39 +2,72 @@
|
||||
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
|
||||
import CommodityList from "./commodity-list.vue";
|
||||
import MerchantInfo from "./merchant-info.vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import { useRouter, useRoute } from "vue-router";
|
||||
import myEvent from '@/utils/myEvent'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { getDesignerDetail } from '@/api/brand'
|
||||
import { AddShoppingCart } from '@/api/shoppingCart'
|
||||
import brandDetailBg from '@/assets/images/brand/brandDetailBg.png'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
//const props = defineProps({
|
||||
//})
|
||||
//const emit = defineEmits([
|
||||
//])
|
||||
const router = useRouter()
|
||||
let data = reactive({
|
||||
const route = useRoute()
|
||||
const { t, locale } = useI18n()
|
||||
|
||||
const designerDetail = ref({
|
||||
avatar: '',
|
||||
brandBanner: '',
|
||||
description: '',
|
||||
email: '',
|
||||
mobile: '',
|
||||
ownerName: '',
|
||||
shopName: '',
|
||||
productStatus: 2,
|
||||
socialLinks: '[]',
|
||||
})
|
||||
|
||||
const addShopping = (item) => {
|
||||
myEvent.emit('addShopping', item)
|
||||
if(!item.price) return ElMessage.warning(t('brandDetail.addShoppingTip'))
|
||||
if(item.productStatus == 0) return ElMessage.warning(t('addShoppingCart.status0'))
|
||||
if(item.productStatus == 1) return ElMessage.warning(t('addShoppingCart.status1'))
|
||||
AddShoppingCart({listingIds:[item.id]}).then((res)=>{
|
||||
item.shopName = designerDetail.value.shopName
|
||||
myEvent.emit('addShopping', item)
|
||||
})
|
||||
}
|
||||
const openDetail = (item) => {
|
||||
router.push({name: 'digitalDetail', params: {id: item.id}})
|
||||
router.push({name: 'digitalItemDetail', params: {id: item.id}})
|
||||
}
|
||||
const getDetail = ()=>{
|
||||
let data = {
|
||||
sellerId: route.params.id,
|
||||
}
|
||||
getDesignerDetail(data,true).then((res)=>{
|
||||
designerDetail.value = res
|
||||
})
|
||||
}
|
||||
onMounted(()=>{
|
||||
getDetail()
|
||||
})
|
||||
onUnmounted(()=>{
|
||||
})
|
||||
defineExpose({})
|
||||
const {} = toRefs(data);
|
||||
</script>
|
||||
<template>
|
||||
<div class="brand">
|
||||
<div class="header-img">
|
||||
<img src="@/assets/images/brand/brandDetailBg.png" alt="">
|
||||
<img :src="designerDetail.brandBanner || brandDetailBg" alt="">
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="merchant-info">
|
||||
<MerchantInfo></MerchantInfo>
|
||||
<MerchantInfo :designerDetail="designerDetail"></MerchantInfo>
|
||||
</div>
|
||||
<div class="commodity-list">
|
||||
<CommodityList @addShopping="addShopping" @openDetail="openDetail"></CommodityList>
|
||||
<CommodityList :id="route.params.id" @addShopping="addShopping" @openDetail="openDetail"></CommodityList>
|
||||
</div>
|
||||
</div>
|
||||
<Footer></Footer>
|
||||
@@ -48,14 +81,18 @@ const {} = toRefs(data);
|
||||
overflow-y: auto;
|
||||
.header-img{
|
||||
width: 100%;
|
||||
border-bottom: 1px solid #585858;
|
||||
>img{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
.content{
|
||||
display: flex;
|
||||
height: auto;
|
||||
align-items: flex-start;
|
||||
// align-items: flex-start;
|
||||
.merchant-info{
|
||||
width: 40rem;
|
||||
padding-left: 12.7rem;
|
||||
|
||||
@@ -1,7 +1,22 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
|
||||
//const props = defineProps({
|
||||
//})
|
||||
const props = defineProps({
|
||||
designerDetail: {
|
||||
type: Object,
|
||||
default: () => {
|
||||
return {
|
||||
avatar: '',
|
||||
brandBanner: '',
|
||||
description: '',
|
||||
email: '',
|
||||
mobile: '',
|
||||
ownerName: '',
|
||||
shopName: '',
|
||||
socialLinks: '[]',
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
//const emit = defineEmits([
|
||||
//])
|
||||
let data = reactive({
|
||||
@@ -16,43 +31,39 @@ const {} = toRefs(data);
|
||||
<template>
|
||||
<div class="merchantInfo">
|
||||
<div class="profile">
|
||||
<img src="@/assets/images/collectionStory/Rectangle.png" alt="">
|
||||
<img v-avatarLoad="designerDetail.avatar" alt="">
|
||||
</div>
|
||||
<div class="info">
|
||||
<div class="detail">
|
||||
<div class="name">Lian Su</div>
|
||||
<div class="title">Roaming Clouds</div>
|
||||
<div class="name">{{ designerDetail.ownerName }}</div>
|
||||
<div class="title">{{ designerDetail.shopName }}</div>
|
||||
</div>
|
||||
<div class="contact">
|
||||
<div class="title">Contact</div>
|
||||
<div class="title">{{ $t('brandDetail.merchantInfo.Contact') }}</div>
|
||||
<div class="email label">
|
||||
<div class="icon">
|
||||
<svg-icon name="brand-email" size="24" />
|
||||
</div>
|
||||
<div>lian.su@urieworweoo.com</div>
|
||||
<div class="text">{{ designerDetail.email }}</div>
|
||||
</div>
|
||||
<div class="phone label">
|
||||
<div class="icon">
|
||||
<svg-icon name="brand-call" size="24" />
|
||||
</div>
|
||||
<div>+86 139 4829 7710</div>
|
||||
<div class="text">{{ designerDetail.mobile }}</div>
|
||||
</div>
|
||||
<div class="address label">
|
||||
<div class="address label" v-for="value in JSON.parse(designerDetail.socialLinks)">
|
||||
<div class="icon">
|
||||
<svg-icon name="brand-link" size="24" />
|
||||
</div>
|
||||
<div>746312432</div>
|
||||
</div>
|
||||
<div class="website label">
|
||||
<div class="icon">
|
||||
<svg-icon name="brand-link" size="24" />
|
||||
</div>
|
||||
<div>https://urieworweoo.com</div>
|
||||
<div class="text">{{value}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="about">
|
||||
<div class="title">About</div>
|
||||
<div class="content">Lian Su’s work weaves understated ethnic influences into contemporary minimalism. She explores materials and silhouettes that bridge heritage and modern sensibilities. Her designs reflect a quiet dialogue between cultural memory and forward-looking innovation.</div>
|
||||
<div class="title">{{ $t('brandDetail.merchantInfo.About') }}</div>
|
||||
<div class="content">
|
||||
{{ designerDetail.description }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -107,9 +118,20 @@ const {} = toRefs(data);
|
||||
font-size: 1.4rem;
|
||||
line-height: 100%;
|
||||
color: #585858;
|
||||
align-items: center;
|
||||
|
||||
|
||||
&:last-child{
|
||||
margin-bottom: 0;
|
||||
}
|
||||
> .text{
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
// word-break: break-word;
|
||||
// white-space: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
> .about{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
|
||||
import img from "@/assets/images/collectionStory/Rectangle.png";
|
||||
import img from "@/assets/images/example.png";
|
||||
import coreConcept from "./coreConcept.vue";
|
||||
import inspiration from "./inspiration.vue";
|
||||
import feelingWithAiDA from "./feelingWithAiDA.vue";
|
||||
@@ -14,19 +14,11 @@ const emit = defineEmits([
|
||||
let data = reactive({
|
||||
})
|
||||
const list = ref([
|
||||
{
|
||||
url: img,
|
||||
title: "Windswept Burden",
|
||||
price: "$100.00",
|
||||
},{
|
||||
url: img,
|
||||
title: "Windswept Burden",
|
||||
price: "$100.00",
|
||||
},{
|
||||
url: img,
|
||||
title: "Windswept Burden",
|
||||
price: "$100.00",
|
||||
},
|
||||
// {
|
||||
// url: img,
|
||||
// title: "Item sold here",
|
||||
// price: "$?",
|
||||
// },
|
||||
])
|
||||
const addShopping = (item) => {
|
||||
emit('addShopping', item)
|
||||
@@ -47,9 +39,9 @@ const {} = toRefs(data);
|
||||
<div class="left">
|
||||
<div class="personal">
|
||||
<img src="@/assets/images/collectionStory/code-create.png" alt="">
|
||||
<div class="name">
|
||||
<div class="name" @click="openCodeCreate">
|
||||
<span>Code-Create</span>
|
||||
<div class="icon" @click="openCodeCreate">
|
||||
<div class="icon">
|
||||
<SvgIcon name="share" size="24" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -77,6 +69,7 @@ const {} = toRefs(data);
|
||||
display: flex;
|
||||
height: calc(100vh - var(--header-height) - var(--footer-height));
|
||||
align-items: flex-start;
|
||||
overflow: hidden;
|
||||
> div{
|
||||
// height: 100%;
|
||||
}
|
||||
@@ -99,8 +92,8 @@ const {} = toRefs(data);
|
||||
display: flex;
|
||||
gap: .4rem;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
> .icon{
|
||||
cursor: pointer;
|
||||
}
|
||||
> span{
|
||||
font-family: 'KaiseiOpti-Bold';
|
||||
@@ -119,8 +112,8 @@ const {} = toRefs(data);
|
||||
border-right: 0.5px solid #585858;
|
||||
// overflow-y: auto;
|
||||
overflow: hidden;
|
||||
// height: 100%;
|
||||
height: auto;
|
||||
height: 100%;
|
||||
// height: auto;
|
||||
position: relative;
|
||||
.line{
|
||||
border: 0.5px solid #58585899;
|
||||
@@ -141,6 +134,7 @@ const {} = toRefs(data);
|
||||
align-items: center;
|
||||
gap: 4rem;
|
||||
overflow-y: auto;
|
||||
height: 100%;
|
||||
&::-webkit-scrollbar{
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -15,17 +15,17 @@ const {} = toRefs(data);
|
||||
</script>
|
||||
<template>
|
||||
<div class="joinUs">
|
||||
<div class="title">Join Our Designer Community</div>
|
||||
<div class="title">{{ $t('collectionStory.joinUs.title') }}</div>
|
||||
<div class="info">
|
||||
<div>
|
||||
Join our community of visionaries and publish your collection story.
|
||||
{{ $t('collectionStory.joinUs.info') }}
|
||||
</div>
|
||||
<div>
|
||||
We are currently seeking collections that deeply integrate the AiDA creative workflow, specifically those that resonate through powerful core concepts and evocative inspiration.
|
||||
{{ $t('collectionStory.joinUs.info2') }}
|
||||
</div>
|
||||
<br />
|
||||
<div>
|
||||
This architecture is designed to elevate your exposure through profound "propositional expression," ensuring that soulful, story-driven designs achieve higher market premiums and superior sales conversion.
|
||||
{{ $t('collectionStory.joinUs.info3') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -22,21 +22,23 @@ const {} = toRefs(data);
|
||||
<div class="collectionStory">
|
||||
<div class="first-screen">
|
||||
<img class="banner" src="@/assets/images/collectionStory/collection_story_banner.png" alt="">
|
||||
<div class="back">
|
||||
<!-- <div class="back">
|
||||
<SvgIcon name="collectionStory-back" size="20" />
|
||||
<div class="text">Back to Home</div>
|
||||
</div>
|
||||
<div class="text">{{ $t('collectionStory.back') }}</div>
|
||||
</div> -->
|
||||
<div class="title-content">
|
||||
<div class="title-box">
|
||||
<div class="title">
|
||||
We’re Seeking
|
||||
{{ $t('collectionStory.title') }}
|
||||
</div>
|
||||
<div class="info">
|
||||
Fashion Voice Worth Featuring.
|
||||
{{ $t('collectionStory.description') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="button">
|
||||
<a href="mailto:info@code-create.com.hk">Contact Us if Interested</a>
|
||||
<a href="mailto:info@code-create.com.hk">
|
||||
{{ $t('collectionStory.button') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -57,6 +59,7 @@ const {} = toRefs(data);
|
||||
height: auto;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
align-items: center;
|
||||
> .back{
|
||||
position: absolute;
|
||||
top: 2.4rem;
|
||||
@@ -76,7 +79,7 @@ const {} = toRefs(data);
|
||||
width: 100%;
|
||||
position: absolute;
|
||||
padding: 0 6.7rem;
|
||||
margin-top: 11.5rem;
|
||||
// margin-top: 11.5rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
@@ -85,7 +88,7 @@ const {} = toRefs(data);
|
||||
flex-direction: column;
|
||||
> .title{
|
||||
font-size: 6.5rem;
|
||||
line-height: 100%;
|
||||
line-height: 1.3;
|
||||
font-weight: 500;
|
||||
color: #585858;
|
||||
}
|
||||
|
||||
@@ -3,94 +3,203 @@ import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import img from "@/assets/images/collectionStory/Rectangle.png";
|
||||
import myEvent from '@/utils/myEvent'
|
||||
import { getListingDetailApi } from '@/api/listing'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { AddShoppingCart } from '@/api/shoppingCart'
|
||||
import { CreateOrder } from '@/api/shoppingCart'
|
||||
|
||||
|
||||
//const props = defineProps({
|
||||
//})
|
||||
//const emit = defineEmits([
|
||||
//])
|
||||
const route = useRoute()
|
||||
const { t } = useI18n()
|
||||
|
||||
const router = useRouter()
|
||||
let data = reactive({
|
||||
let detail:any = ref({
|
||||
description: '',
|
||||
title: '',
|
||||
price: '',
|
||||
shopName: '',
|
||||
updateTime: '',
|
||||
gender: '',
|
||||
sellerId: '',
|
||||
productStatus: 2,
|
||||
})
|
||||
const addShopping = (item) => {
|
||||
myEvent.emit('addShopping', item)
|
||||
const sketchList = ref([])
|
||||
const illustrationList = ref([])
|
||||
const productList = ref([])
|
||||
const editorialVisualList = ref([])
|
||||
const addShopping = () => {
|
||||
if(!detail.value.price) return ElMessage.warning(t('brandDetail.addShoppingTip'))
|
||||
if(detail.value.productStatus == 0) return ElMessage.warning(t('addShoppingCart.status0'))
|
||||
if(detail.value.productStatus == 1) return ElMessage.warning(t('addShoppingCart.status1'))
|
||||
let data = {
|
||||
cover: detail.value.images.cover[0],
|
||||
price: detail.value.price,
|
||||
shopName: detail.value.shopName,
|
||||
title: detail.value.title,
|
||||
id: detail.value.id,
|
||||
productStatus: detail.value.productStatus,
|
||||
}
|
||||
AddShoppingCart({listingIds:[detail.value.id]}).then((res)=>{
|
||||
myEvent.emit('addShopping', data)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const goShopping = () => {
|
||||
if(!detail.value.price) return ElMessage.warning(t('brandDetail.addShoppingTip'))
|
||||
if(detail.value.productStatus == 0) return ElMessage.warning(t('addShoppingCart.status0'))
|
||||
if(detail.value.productStatus == 1) return ElMessage.warning(t('addShoppingCart.status1'))
|
||||
// console.log(detail.value)
|
||||
// return
|
||||
let data = {
|
||||
listingId: detail.value.id, //资产ID
|
||||
title: detail.value.title, //标题
|
||||
brand: detail.value.shopName, //店铺名称
|
||||
sellerId: detail.value.sellerId, //店铺ID
|
||||
cover: detail.value.images.cover[0], //封面
|
||||
amount: detail.value.price, //价格
|
||||
status: 1, //状态
|
||||
// date: v.addTime, //添加时间
|
||||
// tags: v.productCategory, //标签
|
||||
// salesVolume: v.salesVolume, //销售量
|
||||
}
|
||||
const list = btoa(encodeURIComponent(JSON.stringify([data])))
|
||||
router.push({
|
||||
name: 'pay',
|
||||
query: { list }
|
||||
})
|
||||
}
|
||||
const setImgList = (list)=>{
|
||||
sketchList.value = list?.apparel?.map((item:any) => {return {imgUrl:item}})
|
||||
illustrationList.value = list?.sketch?.map((item:any) => {return {imgUrl:item}})
|
||||
productList.value = list?.main_product?.map((item:any) => {return {imgUrl:item}})
|
||||
|
||||
editorialVisualList.value = list.product.map((item:any) => {return {imgUrl:item}})
|
||||
if(list.firstFrame){
|
||||
list.firstFrame.forEach((item:any,index) => {
|
||||
editorialVisualList.value.push({
|
||||
imgUrl:item,
|
||||
gif: list.gif[index],
|
||||
video: list.video[index],
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
const getListingDetail = ()=>{
|
||||
getListingDetailApi({
|
||||
id:route.params.id + '',
|
||||
}).then((res)=>{
|
||||
if(res)detail.value = res
|
||||
setImgList(res.images)
|
||||
})
|
||||
}
|
||||
const gobrand = () => {
|
||||
if(!detail.value.sellerId)return
|
||||
router.push({path: `/brand/${detail.value.sellerId}`})
|
||||
}
|
||||
// const vObserve = {
|
||||
// mounted (el,binding) {
|
||||
// },
|
||||
// unmounted (el,binding) {
|
||||
// }
|
||||
// }
|
||||
onMounted(()=>{
|
||||
getListingDetail()
|
||||
})
|
||||
onUnmounted(()=>{
|
||||
})
|
||||
defineExpose({})
|
||||
const {} = toRefs(data);
|
||||
</script>
|
||||
<template>
|
||||
<div class="digitalItemDetail">
|
||||
<div class="center">
|
||||
<div class="img-list">
|
||||
<div class="title">
|
||||
<div>Sketch</div>
|
||||
<div>Illustration</div>
|
||||
<div>Product</div>
|
||||
</div>
|
||||
<div class="img">
|
||||
<div class="sketch">
|
||||
<img :src="img" v-for="item in 4" :key="item" alt="">
|
||||
</div>
|
||||
<div class="illustration">
|
||||
<img :src="img" v-for="item in 4" :key="item" alt="">
|
||||
</div>
|
||||
<div class="product">
|
||||
<img :src="img" v-for="item in 4" :key="item" alt="">
|
||||
<div class="left">
|
||||
<div class="title">{{ $t('digitalDetail.Sketch') }}</div>
|
||||
<div class="box sketch" :class="{'active': sketchList.length == 1}">
|
||||
<div class="imgBox">
|
||||
<img :src="item?.imgUrl" v-for="item in sketchList" :key="item" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
<div class="top">
|
||||
<div class="box illustration">
|
||||
<div class="title">{{ $t('digitalDetail.Illustration') }}</div>
|
||||
<div class="imgBox">
|
||||
<img :src="item?.imgUrl" v-for="item in illustrationList" :key="item" alt="">
|
||||
</div>
|
||||
</div>
|
||||
<div class="box">
|
||||
<div class="title">{{ $t('digitalDetail.Product') }}</div>
|
||||
<div class="imgBox">
|
||||
<img :src="item?.imgUrl" v-for="item in productList" :key="item" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bottom" v-if="editorialVisualList.length > 0">
|
||||
<div class="box editorialVisual">
|
||||
<div class="title">{{ $t('digitalDetail.EditorialVisual') }}</div>
|
||||
<div class="imgBox">
|
||||
<img :src="item?.gif||item?.imgUrl" v-for="item in editorialVisualList" :key="item" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="img-detail">
|
||||
<div class="back" @click="router.back()">
|
||||
<div class="icon">
|
||||
<svg-icon name="digital-back" size="28"></svg-icon>
|
||||
</div>
|
||||
<span>Back</span>
|
||||
<span>{{ $t('digitalDetail.Back') }}</span>
|
||||
</div>
|
||||
<div class="img-info">
|
||||
<div class="img-type">FEMALE / skirt, blouse, Outwear</div>
|
||||
<div class="img-name">Heritage Layered Set</div>
|
||||
<div class="img-price">$100 <span class="mini-scrollbar">HKD</span></div>
|
||||
<div class="img-type">{{ detail.designFor?.toUpperCase() || ''}} / {{ detail.productCategory?.join(',')?.toUpperCase() || '' }}</div>
|
||||
<div class="img-name">{{ detail.title }}</div>
|
||||
<div class="img-price" v-if="detail.price">¥{{ detail.price }} <span class="mini-scrollbar">HKD</span></div>
|
||||
</div>
|
||||
<div class="commodity">
|
||||
<div class="info">
|
||||
<img class="profile" :src="img" alt="">
|
||||
<img class="profile" v-avatarLoad="detail.avatar" alt="">
|
||||
<div class="detail">
|
||||
<div class="name">Roaming Clouds</div>
|
||||
<div class="name" @click="gobrand">{{ detail.shopName }}</div>
|
||||
<div class="release-time">
|
||||
<span>Release in Feb 26, 2026</span>
|
||||
<span>{{ $t('digitalDetail.ReleaseIn') }} {{ new Date(detail.updateTime).toLocaleString() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="introduce">
|
||||
This ensemble artfully merges traditional folk heritage with contemporary tailoring, creating a timeless silhouette that honors ancestral craftsmanship while embracing modern sophistication.
|
||||
{{ detail.description }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="notice">
|
||||
<div class="title">Copyright & License Notice</div>
|
||||
<div class="title">{{ $t('digitalDetail.CopyrightLicenseNotice') }}</div>
|
||||
<div class="conter">
|
||||
<div class="contet-title">
|
||||
<div class="icon">
|
||||
<svg-icon name="digital-Info" size="24"></svg-icon>
|
||||
</div>
|
||||
<span>License Included in Asset</span>
|
||||
<span>{{ $t('digitalDetail.LicenseIncludedInAsset') }}</span>
|
||||
</div>
|
||||
<div class="info">
|
||||
All products on this platform are digital assets, not physical goods. Purchase grants a usage license only; copyright and intellectual property rights remain with the original creator, unless otherwise stated.
|
||||
{{ $t('digitalDetail.LicenseIncludedInAssetInfo') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="button">
|
||||
<div class="buy-now">Buy Now</div>
|
||||
<div class="add-cart" @click="addShopping(item)">
|
||||
<div class="buy-now" @click="goShopping">{{ $t('digitalDetail.BuyNow') }}</div>
|
||||
<div class="add-cart" @click="addShopping()">
|
||||
<div class="icon">
|
||||
<svg-icon name="cart_0" size="24"></svg-icon>
|
||||
</div>
|
||||
Add to Cart
|
||||
{{ $t('digitalDetail.AddToCart') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -105,61 +214,136 @@ const {} = toRefs(data);
|
||||
.digitalItemDetail{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
.center{
|
||||
flex: 1;
|
||||
// flex: 1;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
// overflow: hidden;
|
||||
.img-list{
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 2fr;
|
||||
--row-width: 33.333%;
|
||||
align-items: stretch;
|
||||
// --row-width: 29.3rem;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
border-right: 0.5px solid #585858;
|
||||
&::-webkit-scrollbar{
|
||||
display: none;
|
||||
}
|
||||
> .title{
|
||||
display: flex;
|
||||
.title{
|
||||
width: 100%;
|
||||
line-height: 8.6rem;
|
||||
padding-left: 2.4rem;
|
||||
font-weight: 500;
|
||||
font-size: 1.6rem;
|
||||
background-color: #f5f5f5;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
> div{
|
||||
width: var(--row-width);
|
||||
line-height: 8.6rem;
|
||||
padding-left: 2.4rem;
|
||||
border-right: 0.5px solid #C4C4C4;
|
||||
font-weight: 500;
|
||||
font-size: 1.6rem;
|
||||
background-color: #f5f5f5;
|
||||
&:last-child{
|
||||
border: none;
|
||||
}
|
||||
.box{
|
||||
flex: 1;
|
||||
.imgBox{
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
img{
|
||||
width: 100%;
|
||||
// height: 51rem;
|
||||
// margin: 0 auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
> .img{
|
||||
display: flex;
|
||||
|
||||
> div{
|
||||
&.sketch{
|
||||
.imgBox{
|
||||
flex-direction: column;
|
||||
gap: 4rem;
|
||||
padding: 5rem 0;
|
||||
img{
|
||||
height: 26.4rem;
|
||||
object-fit: contain;
|
||||
width: auto;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
&.active{
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
aspect-ratio: 9 / 16;
|
||||
img{
|
||||
height: auto;
|
||||
width: 20.1rem;
|
||||
aspect-ratio: 9 / 16;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.illustration{
|
||||
display: flex;
|
||||
width: var(--row-width);
|
||||
border-right: 0.5px solid #C4C4C4;
|
||||
flex-direction: column;
|
||||
&:last-child{
|
||||
border: none;
|
||||
.imgBox{
|
||||
flex: 1;
|
||||
> img{
|
||||
aspect-ratio: 9 / 16;
|
||||
object-fit: contain;
|
||||
padding: 3.6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.editorialVisual{
|
||||
.imgBox{
|
||||
row-gap: 60px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
> .left{
|
||||
border-right: 0.5px solid #C4C4C4;
|
||||
.imgBox{
|
||||
padding: 0 3rem;
|
||||
}
|
||||
}
|
||||
> .right{
|
||||
position: relative;
|
||||
&::after{
|
||||
content: '';
|
||||
display: block;
|
||||
border-right: 0.5px solid #C4C4C4;
|
||||
position: absolute;
|
||||
height: 100%;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
}
|
||||
> .top{
|
||||
display: flex;
|
||||
}
|
||||
> .bottom{
|
||||
margin-top: 10.5rem;
|
||||
.imgBox{
|
||||
display: flex;
|
||||
img{
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.img-detail{
|
||||
border-left: 0.5px solid #585858;
|
||||
padding-left: 3.2rem;
|
||||
width: 57rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
height: calc(var(--app-view-height) - var(--footer-height));
|
||||
position: sticky;
|
||||
top: 0;
|
||||
// height: 100%;
|
||||
&::-webkit-scrollbar{
|
||||
display: none;
|
||||
}
|
||||
@@ -169,6 +353,7 @@ const {} = toRefs(data);
|
||||
gap: 1.4rem;
|
||||
align-items: center;
|
||||
width: min-content;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
> span{
|
||||
font-weight: 500;
|
||||
@@ -191,6 +376,9 @@ const {} = toRefs(data);
|
||||
font-size: 3rem;
|
||||
line-height: 120%;
|
||||
margin-bottom: 1rem;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.img-price{
|
||||
font-family: KaiseiOpti-Bold;
|
||||
@@ -217,12 +405,16 @@ const {} = toRefs(data);
|
||||
object-fit: cover;
|
||||
}
|
||||
> .detail{
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
.name{
|
||||
text-decoration: underline;
|
||||
font-weight: 500;
|
||||
font-size: 1.8rem;
|
||||
line-height: 100%;
|
||||
margin-bottom: .8rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
.release-time{
|
||||
font-family: KaiseiOpti-Regular;
|
||||
|
||||
@@ -1,68 +1,108 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
|
||||
import img from "@/assets/images/collectionStory/Rectangle.png";
|
||||
//const props = defineProps({
|
||||
//})
|
||||
import { getListingMallListApi } from '@/api/listing'
|
||||
|
||||
const props = defineProps({
|
||||
getListData: {
|
||||
type: Object,
|
||||
default: () => ({
|
||||
designFor: '',
|
||||
categories: [],
|
||||
sortField: '',
|
||||
})
|
||||
}
|
||||
})
|
||||
const emit = defineEmits([
|
||||
'addShopping',
|
||||
'openDetail'
|
||||
'openDetail',
|
||||
'getListingMallList'
|
||||
])
|
||||
let data = reactive({
|
||||
})
|
||||
const list = ref([
|
||||
{
|
||||
url: img,
|
||||
title: "Windswept Burden",
|
||||
price: "$100.00",
|
||||
},{
|
||||
url: img,
|
||||
title: "Windswept Burden",
|
||||
price: "$100.00",
|
||||
},{
|
||||
url: img,
|
||||
title: "Windswept Burden",
|
||||
price: "$100.00",
|
||||
},{
|
||||
url: img,
|
||||
title: "Windswept Burden",
|
||||
price: "$100.00",
|
||||
},{
|
||||
url: img,
|
||||
title: "Windswept Burden",
|
||||
price: "$100.00",
|
||||
},{
|
||||
url: img,
|
||||
title: "Windswept Burden",
|
||||
price: "$100.00",
|
||||
},{
|
||||
url: img,
|
||||
title: "Windswept Burden",
|
||||
price: "$100.00",
|
||||
},{
|
||||
url: img,
|
||||
title: "Windswept Burden",
|
||||
price: "$100.00",
|
||||
},
|
||||
])
|
||||
const type = ref('All')
|
||||
const addShopping = (item) => {
|
||||
emit('addShopping', item)
|
||||
}
|
||||
const openDetail = (item) => {
|
||||
emit('openDetail', item)
|
||||
}
|
||||
|
||||
const commodityList = ref([])
|
||||
|
||||
const getListingListData = reactive({
|
||||
pageSize: 10,
|
||||
pageNum: 1,
|
||||
isShowMark:false,
|
||||
isNoData:false,
|
||||
})
|
||||
|
||||
const reset = () => {
|
||||
commodityList.value = []
|
||||
getListingListData.pageNum = 1
|
||||
getListingListData.isShowMark = false
|
||||
getListingListData.isNoData = false
|
||||
}
|
||||
const clearData = () => {
|
||||
commodityList.value = []
|
||||
getListingListData.pageNum = 1
|
||||
getListingListData.isNoData = true
|
||||
}
|
||||
|
||||
const getListingMallList = ()=>{
|
||||
getListingListData.isShowMark = true
|
||||
getListingListData.isNoData = false
|
||||
let data = {
|
||||
designFor: props.getListData.designFor,
|
||||
// categories: [all],
|
||||
categories: props.getListData.categories[0] == 'all' ? [] : props.getListData.categories,
|
||||
sortField: props.getListData.sortField,
|
||||
sortOrder: 'desc',
|
||||
pageSize: getListingListData.pageSize,
|
||||
pageNum: getListingListData.pageNum,
|
||||
|
||||
}
|
||||
getListingMallListApi(data).then(res => {
|
||||
if(res.content.length == 0)getListingListData.isNoData = true
|
||||
commodityList.value.push(...res.content)
|
||||
getListingListData.isShowMark = false
|
||||
}).catch(()=>{
|
||||
getListingListData.isNoData = true
|
||||
getListingListData.isShowMark = false
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const vObserve = {
|
||||
mounted (el,binding) {
|
||||
getListingListData.isShowMark = false
|
||||
getListingListData.isNoData = false
|
||||
new IntersectionObserver(
|
||||
(entries, observer) => {
|
||||
// 如果不是相交,则直接返回
|
||||
// console.log(entries[0]);
|
||||
if (!entries[0].intersectionRatio) return;
|
||||
binding.value()
|
||||
getListingListData.pageNum += 1
|
||||
},
|
||||
// { root:worksPage }
|
||||
).observe(el);
|
||||
}
|
||||
}
|
||||
onMounted(()=>{
|
||||
})
|
||||
onUnmounted(()=>{
|
||||
})
|
||||
defineExpose({})
|
||||
defineExpose({reset,clearData,commodityList,getListingListData})
|
||||
const {} = toRefs(data);
|
||||
</script>
|
||||
<template>
|
||||
<div class="commodityList">
|
||||
<div class="list">
|
||||
<div class="item" v-for="item in list" :key="item.url">
|
||||
<CommodityItem :url="item.url" :name="item.title" :price="item.price" @addShopping="addShopping(item)" @openDetail="openDetail(item)"></CommodityItem>
|
||||
<div class="item" v-for="item in commodityList" :key="item.url">
|
||||
<CommodityItem :url="item.cover" :name="item.title" :price="item.price" @addShopping="addShopping(item)" @openDetail="openDetail(item)"></CommodityItem>
|
||||
</div>
|
||||
<div v-show="!getListingListData.isNoData" class="material_content_list_loding">
|
||||
<span class="page_loading" v-show="!getListingListData.isShowMark" v-observe="getListingMallList"></span>
|
||||
<img v-if="getListingListData.isShowMark" src="@/assets/images/brand/brandLoading.gif" alt="">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -81,12 +121,11 @@ const {} = toRefs(data);
|
||||
// align-content: start;
|
||||
// grid-template-columns: repeat(3, 1fr);
|
||||
overflow: hidden;
|
||||
|
||||
display: grid;
|
||||
align-content: start;
|
||||
grid-template-columns: repeat(auto-fit, minmax(min(100%, 28rem), 1fr));
|
||||
border-top: 0.5px solid #585858;
|
||||
grid-template-columns: repeat(auto-fill, minmax(min(100%, 28rem), 1fr));
|
||||
padding: .5px 0 0 .5px;
|
||||
|
||||
/* 垂直线(右边框) */
|
||||
.item{
|
||||
position: relative;
|
||||
@@ -95,6 +134,26 @@ const {} = toRefs(data);
|
||||
border-right: 0.5px solid #585858;
|
||||
margin-right: -1px;
|
||||
margin-bottom: -1px;
|
||||
min-width: 0;
|
||||
}
|
||||
> .material_content_list_loding{
|
||||
width: 100%;
|
||||
height: 5rem;
|
||||
aspect-ratio: 1/1;
|
||||
grid-column: 1 / -1;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
justify-content: center;
|
||||
> .page_loading{
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
}
|
||||
> img{
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,13 @@ import { ref, onMounted, onUnmounted, reactive, toRefs, onActivated } from "vue"
|
||||
import CommodityList from "./commodity-list.vue";
|
||||
import MerchantInfo from "./merchant-info.vue";
|
||||
import { useRouter } from "vue-router";
|
||||
import scListNull from '@/views/shoppingCart/sc-list-null.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { AddShoppingCart } from '@/api/shoppingCart'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import myEvent from '@/utils/myEvent'
|
||||
|
||||
|
||||
// 定义组件名称
|
||||
defineOptions({
|
||||
name: 'digitalItem'
|
||||
@@ -11,32 +18,54 @@ defineOptions({
|
||||
//})
|
||||
//const emit = defineEmits([
|
||||
//])
|
||||
const { t } = useI18n()
|
||||
const categories = ref(['all'])
|
||||
const gender = ref(['all'])
|
||||
const digitalItemRef = ref(null)
|
||||
const scrollTop = ref(0)
|
||||
const router = useRouter()
|
||||
let data = reactive({
|
||||
})
|
||||
const commodityListRef = ref(null)
|
||||
|
||||
const categoriesList = ref([
|
||||
const searechTypeList = ref([
|
||||
{
|
||||
value:'Best Selling',
|
||||
label:'Best Selling'
|
||||
value:'salesVolume',
|
||||
label:t('digitalItem.BestSelling')
|
||||
},{
|
||||
value:'Price: Low to High',
|
||||
label:'Price: Low to High'
|
||||
value:'price',
|
||||
label:t('digitalItem.Price')
|
||||
},{
|
||||
value:'Newest First',
|
||||
label:'Newest First'
|
||||
value:'updateTime',
|
||||
label:t('digitalItem.NewestFirst')
|
||||
},
|
||||
])
|
||||
const categories = ref('Newest First')
|
||||
const addShopping = (item) => {}
|
||||
|
||||
const searechType = ref('updateTime')
|
||||
const addShopping = (item) => {
|
||||
if(!item.price) return ElMessage.warning(t('brandDetail.addShoppingTip'))
|
||||
if(item.productStatus == 0) return ElMessage.warning(t('addShoppingCart.status0'))
|
||||
if(item.productStatus == 1) return ElMessage.warning(t('addShoppingCart.status1'))
|
||||
AddShoppingCart({listingIds:[item.id]}).then((res)=>{
|
||||
myEvent.emit('addShopping', item)
|
||||
})
|
||||
}
|
||||
const openDetail = (item) => {
|
||||
scrollTop.value = digitalItemRef.value.scrollTop
|
||||
router.push({
|
||||
path: '/digitalItem/' + 123,
|
||||
path: '/digitalItem/' + item.id,
|
||||
})
|
||||
}
|
||||
const handleChange = (val) => {
|
||||
categories.value = val.categories
|
||||
gender.value = val.gender
|
||||
if(categories.value.length == 0 || gender.value.length == 0){
|
||||
commodityListRef.value.clearData()
|
||||
}else{
|
||||
commodityListRef.value.reset()
|
||||
}
|
||||
}
|
||||
const updateSort = () => {
|
||||
commodityListRef.value.reset()
|
||||
}
|
||||
onActivated(()=>{
|
||||
digitalItemRef.value.scrollTop = scrollTop.value
|
||||
})
|
||||
@@ -45,26 +74,25 @@ onMounted(()=>{
|
||||
onUnmounted(()=>{
|
||||
})
|
||||
defineExpose({})
|
||||
const {} = toRefs(data);
|
||||
</script>
|
||||
<template>
|
||||
<div class="digitalItem" ref="digitalItemRef">
|
||||
<div class="digitalItem" :class="{'active': commodityListRef?.commodityList?.length > 0}" ref="digitalItemRef">
|
||||
<div class="header-img">
|
||||
<img src="@/assets/images/digitalItem/digital_item_banner.png" alt="">
|
||||
<div class="text">
|
||||
<div class="title">Digital Item</div>
|
||||
<p class="info">Virtual fashion creations collected in your personal archive</p>
|
||||
<div class="title">{{ $t('digitalItem.title') }}</div>
|
||||
<p class="info">{{ $t('digitalItem.info') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filters">
|
||||
<div class="filter-item">
|
||||
<el-select v-model="categories" placeholder="Sort By" :teleported="false">
|
||||
<el-select v-model="searechType" @change="updateSort" :placeholder="$t('digitalItem.sortBy')" :teleported="false">
|
||||
<template #label="{ label }">
|
||||
<span class="header-label">Sort By</span>
|
||||
<span class="header-label">{{ $t('digitalItem.sortBy') }}</span>
|
||||
<span class="header-value">{{ label }}</span>
|
||||
</template>
|
||||
<el-option
|
||||
v-for="item in categoriesList"
|
||||
v-for="item in searechTypeList"
|
||||
:key="item.label"
|
||||
:value="item.value"
|
||||
:label="item.label"
|
||||
@@ -74,10 +102,28 @@ const {} = toRefs(data);
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="merchant-info">
|
||||
<MerchantInfo></MerchantInfo>
|
||||
<MerchantInfo @change="handleChange"></MerchantInfo>
|
||||
</div>
|
||||
<div class="commodity-list">
|
||||
<CommodityList @addShopping="addShopping" @openDetail="openDetail"></CommodityList>
|
||||
<CommodityList
|
||||
v-show="commodityListRef?.commodityList?.length > 0 || !commodityListRef?.getListingListData.isNoData"
|
||||
ref="commodityListRef"
|
||||
:getListData="{
|
||||
designFor: gender[0],
|
||||
categories: categories,
|
||||
sortField: searechType,
|
||||
}"
|
||||
@addShopping="addShopping"
|
||||
@openDetail="openDetail"></CommodityList>
|
||||
<div v-show="commodityListRef?.commodityList?.length == 0" class="null">
|
||||
<sc-list-null
|
||||
nullImage="shopping-cart"
|
||||
:showButton="false"
|
||||
:title="$t('digitalItem.noData')"
|
||||
:tip="$t('digitalItem.noDataTip')"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<Footer></Footer>
|
||||
@@ -86,9 +132,12 @@ const {} = toRefs(data);
|
||||
<style lang="less" scoped>
|
||||
.digitalItem{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
height: auto;
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
&.active{
|
||||
height: 100%;
|
||||
}
|
||||
.header-img{
|
||||
width: 100%;
|
||||
position: relative;
|
||||
@@ -114,7 +163,7 @@ const {} = toRefs(data);
|
||||
color: #585858;
|
||||
font-size: 1.6rem;
|
||||
line-height: 140%;
|
||||
margin-top: 1.2rem;
|
||||
margin-top: 2.4rem;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
@@ -128,16 +177,17 @@ const {} = toRefs(data);
|
||||
justify-content: flex-end;
|
||||
> .filter-item{
|
||||
:deep(.el-select) {
|
||||
width: 15rem;
|
||||
width: 18rem;
|
||||
--el-border-radius-base: 0;
|
||||
--el-select-input-color: rgba(0, 0, 0, 0.5);
|
||||
--el-select-input-font-size: 1rem;
|
||||
--el-border-color: #222222;
|
||||
.el-select__wrapper {
|
||||
font-size: 1.07rem;
|
||||
padding: 0 0.7rem;
|
||||
line-height: 1;
|
||||
min-height: 0;
|
||||
height: 2.2rem;
|
||||
height: 2.8rem;
|
||||
|
||||
.header-label {
|
||||
font-family: KaiseiOpti-Regular;
|
||||
@@ -178,7 +228,7 @@ const {} = toRefs(data);
|
||||
> .content{
|
||||
display: flex;
|
||||
height: auto;
|
||||
align-items: flex-start;
|
||||
// align-items: flex-start;
|
||||
border-top: 0.5px solid #585858;
|
||||
.merchant-info{
|
||||
width: 38.5rem;
|
||||
@@ -197,6 +247,15 @@ const {} = toRefs(data);
|
||||
border-left: 0.5px solid #585858;
|
||||
border-right: 0.5px solid #585858;
|
||||
margin-right: 9rem;
|
||||
display: flex;
|
||||
position: relative;
|
||||
.null{
|
||||
flex: 1;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,76 +1,54 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
|
||||
import { ref, onMounted, onUnmounted, computed, toRefs } from "vue";
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useClothesCategories } from '@/utils/ClothesCategory'
|
||||
//const props = defineProps({
|
||||
//})
|
||||
//const emit = defineEmits([
|
||||
//])
|
||||
let data = reactive({
|
||||
})
|
||||
const categoriesList = ref([
|
||||
{
|
||||
label: 'Outwear',
|
||||
value: 'Outwear'
|
||||
},
|
||||
{
|
||||
label: 'Dress',
|
||||
value: 'Dress'
|
||||
},
|
||||
{
|
||||
label: 'Trousers',
|
||||
value: 'Trousers'
|
||||
},
|
||||
{
|
||||
label: 'Blouse',
|
||||
value: 'Blouse'
|
||||
},
|
||||
{
|
||||
label: 'Skirt',
|
||||
value: 'Skirt'
|
||||
},
|
||||
{
|
||||
label: 'Accessories',
|
||||
value: 'Accessories'
|
||||
},
|
||||
]);
|
||||
const genderList = ref([
|
||||
{
|
||||
label: 'Male',
|
||||
value: 'Male'
|
||||
},
|
||||
{
|
||||
label: 'Female',
|
||||
value: 'Female'
|
||||
},
|
||||
const emit = defineEmits([
|
||||
'change'
|
||||
])
|
||||
const categories = ref([''])
|
||||
const gender = ref([''])
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const categoriesList = useClothesCategories();
|
||||
|
||||
const genderList = computed(() => [
|
||||
{ label: t('Wardrobe.assets.genders.male'), value: 'male' },
|
||||
{ label: t('Wardrobe.assets.genders.female'), value: 'female' }
|
||||
])
|
||||
const categories = ref(['all'])
|
||||
const gender = ref(['all'])
|
||||
|
||||
const clearFilters = () => {
|
||||
categories.value = ['']
|
||||
gender.value = ['']
|
||||
if(categories.value?.[0] == 'all' && gender.value?.[0] == 'all')return
|
||||
categories.value = ['all']
|
||||
gender.value = ['all']
|
||||
handleChange()
|
||||
}
|
||||
const handleChange = () => {
|
||||
emit('change', {categories:categories.value, gender:gender.value})
|
||||
}
|
||||
onMounted(()=>{
|
||||
})
|
||||
onUnmounted(()=>{
|
||||
})
|
||||
defineExpose({})
|
||||
const {} = toRefs(data);
|
||||
</script>
|
||||
<template>
|
||||
<div class="filters">
|
||||
<div class="title">
|
||||
<div class="left">Filters</div>
|
||||
<div class="right" @click="clearFilters">Clear</div>
|
||||
<div class="left">{{ $t('digitalItem.MerchantInfo.Filters') }}</div>
|
||||
<div class="right" @click="clearFilters">{{ $t('digitalItem.MerchantInfo.Clear') }}</div>
|
||||
</div>
|
||||
<div class="categories">Categories</div>
|
||||
<div class="categories">{{ $t('digitalItem.MerchantInfo.Categories') }}</div>
|
||||
<div class="line"></div>
|
||||
<div class="multiple">
|
||||
<checked :list="categoriesList" v-model:selected="categories" />
|
||||
<checked :list="categoriesList" @change="handleChange" v-model:selected="categories" />
|
||||
</div>
|
||||
<div class="categories">Gender</div>
|
||||
<div class="categories">{{ $t('digitalItem.MerchantInfo.Gender') }}</div>
|
||||
<div class="line"></div>
|
||||
<div class="multiple">
|
||||
<checked :list="genderList" v-model:selected="gender" />
|
||||
<checked-gender :list="genderList" @change="handleChange" v-model:selected="gender" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="home-index">
|
||||
<section-index />
|
||||
<section-designer />
|
||||
<section-design />
|
||||
<section-aida />
|
||||
<section-digital-items1 />
|
||||
<section-digital-items2 />
|
||||
<section-footer />
|
||||
@@ -13,7 +13,7 @@
|
||||
import { computed } from 'vue'
|
||||
import SectionIndex from './section-index.vue'
|
||||
import SectionDesigner from './section-designer.vue'
|
||||
import SectionDesign from './section-design.vue'
|
||||
import SectionAida from './section-aida.vue'
|
||||
import SectionDigitalItems1 from './section-digital-items1.vue'
|
||||
import SectionDigitalItems2 from './section-digital-items2.vue'
|
||||
import SectionFooter from './section-footer.vue'
|
||||
@@ -28,6 +28,7 @@
|
||||
> section {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
min-height: 50vw;
|
||||
}
|
||||
> section.bgw {
|
||||
position: relative;
|
||||
@@ -40,5 +41,8 @@
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
> .section-footer {
|
||||
min-height: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
<template>
|
||||
<section class="section-design bgw">
|
||||
<img src="@/assets/images/home/design-bg.jpg" class="bg" />
|
||||
<section class="section-aida bgw">
|
||||
<img src="@/assets/images/home/aida-bg.jpg" class="bg" />
|
||||
<div class="content">
|
||||
<div class="aida-logo"><img src="@/assets/images/logos/aida.png" /></div>
|
||||
<div class="title">Design with AiDA</div>
|
||||
<div class="tip">
|
||||
Each garment on this platform is where designer vision blooms through AiDA. A tool that
|
||||
nurtures your creativity, never overshadows it. Let your ideas flourish.
|
||||
</div>
|
||||
<button custom>Try Now</button>
|
||||
<div class="title">{{ $t('Home.AidaTitle') }}</div>
|
||||
<div class="tip">{{ $t('Home.AidaTip') }}</div>
|
||||
<button custom @click="openView(UrlList.aida)">{{ $t('Home.TryNow') }}</button>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { UrlList, openView } from '../../utils/UrlList'
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
.section-design {
|
||||
.section-aida {
|
||||
> .content {
|
||||
width: 55rem;
|
||||
top: 12rem;
|
||||
@@ -1,11 +1,8 @@
|
||||
<template>
|
||||
<section class="section-designer">
|
||||
<div class="title">Designer Community</div>
|
||||
<div class="tip">
|
||||
Discover the designers shaping AiDA’s creative landscape. <br />
|
||||
Each month, we will showcase a curated selection of their most distinguished works.
|
||||
</div>
|
||||
<button custom="black" @click="onSearchBrand">Search Brands</button>
|
||||
<div class="title" v-html="$t('Home.DesignerTitle')"></div>
|
||||
<div class="tip" v-html="$t('Home.DesignerTip')"></div>
|
||||
<button custom="black" @click="onSearchBrand">{{ $t('Home.SearchBrands') }}</button>
|
||||
<img src="@/assets/images/home/designer-bg.png" />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -2,13 +2,9 @@
|
||||
<section class="digital-items1 bgw">
|
||||
<img src="@/assets/images/home/digital-items-1.jpg" class="bg" />
|
||||
<div class="content">
|
||||
<div class="title">Digital Items</div>
|
||||
<div class="tip">
|
||||
AiDA captures your boldest thoughts and transforms them into vivid
|
||||
<br />
|
||||
digital visions—a virtual realm where creativity collides and evolves.
|
||||
</div>
|
||||
<button custom="black">Shop All</button>
|
||||
<div class="title">{{ $t('Home.DigitalItems') }}</div>
|
||||
<div class="tip" v-html="$t('Home.DigitalItemsTip1')"></div>
|
||||
<button custom="black" @click="onShopAll">Shop All</button>
|
||||
<div class="list">
|
||||
<div v-for="v in list" :key="v.url">
|
||||
<img :src="v.url" alt="" />
|
||||
@@ -22,28 +18,34 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
const router = useRouter()
|
||||
|
||||
const list = ref([
|
||||
{
|
||||
title: 'Women’s Itemk',
|
||||
title: 'Women’s Item',
|
||||
tip: 'Blue Pleat Aria',
|
||||
url: 'http://118.31.39.42:3000/falls/digital-items-1.png'
|
||||
url: '/images/home/digital-items-1.png'
|
||||
},
|
||||
{
|
||||
title: 'Girls’ Item',
|
||||
tip: 'Candy Riot',
|
||||
url: 'http://118.31.39.42:3000/falls/digital-items-2.jpg'
|
||||
url: '/images/home/digital-items-2.jpg'
|
||||
},
|
||||
{
|
||||
title: 'Men’s Item',
|
||||
tip: 'Void Armour',
|
||||
url: 'http://118.31.39.42:3000/falls/digital-items-3.png'
|
||||
url: '/images/home/digital-items-3.png'
|
||||
},
|
||||
{
|
||||
title: 'Boys’ Item',
|
||||
tip: 'Jester Edit',
|
||||
url: 'http://118.31.39.42:3000/falls/digital-items-4.png'
|
||||
url: '/images/home/digital-items-4.png'
|
||||
}
|
||||
])
|
||||
const onShopAll = () => {
|
||||
router.push({ name: 'digitalItem' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
@@ -76,6 +78,7 @@
|
||||
> div {
|
||||
padding: 1rem;
|
||||
border: 0.1rem solid #979797;
|
||||
background-color: #fff;
|
||||
> img {
|
||||
width: 27.4rem;
|
||||
height: 34.6rem;
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
<template>
|
||||
<section class="digital-items2 bgw">
|
||||
<img src="@/assets/images/home/digital-items-1.jpg" class="bg" />
|
||||
<img src="@/assets/images/home/digital-items-2.jpg" class="bg" />
|
||||
<div class="content">
|
||||
<div class="tip">
|
||||
AiDA accelerates style innovation, shaping daily pieces that keep
|
||||
<br />
|
||||
your wardrobe in sync with modern fashion’s rhythm.
|
||||
</div>
|
||||
<div class="tip" v-html="$t('Home.DigitalItemsTip2')"></div>
|
||||
<div class="list">
|
||||
<div v-for="v in list" :key="v.url">
|
||||
<img :src="v.url" alt="" />
|
||||
@@ -21,25 +17,25 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
const list = ref([
|
||||
{
|
||||
title: 'Women’s Itemk',
|
||||
tip: 'Blue Pleat Aria',
|
||||
url: 'http://118.31.39.42:3000/falls/digital-items-1.png'
|
||||
},
|
||||
{
|
||||
title: 'Girls’ Item',
|
||||
tip: 'Candy Riot',
|
||||
url: 'http://118.31.39.42:3000/falls/digital-items-2.jpg'
|
||||
tip: 'Meadow Wrap',
|
||||
url: '/images/home/digital-items-5.png'
|
||||
},
|
||||
{
|
||||
title: 'Men’s Item',
|
||||
tip: 'Void Armour',
|
||||
url: 'http://118.31.39.42:3000/falls/digital-items-3.png'
|
||||
title: 'Women’s Item',
|
||||
tip: 'Onyx Silhouette',
|
||||
url: '/images/home/digital-items-6.png'
|
||||
},
|
||||
{
|
||||
title: 'Boys’ Item',
|
||||
tip: 'Jester Edit',
|
||||
url: 'http://118.31.39.42:3000/falls/digital-items-4.png'
|
||||
tip: 'Autumn Roam',
|
||||
url: '/images/home/digital-items-7.png'
|
||||
},
|
||||
{
|
||||
title: 'Men’s Item',
|
||||
tip: 'Archive Casual',
|
||||
url: '/images/home/digital-items-8.png'
|
||||
}
|
||||
])
|
||||
</script>
|
||||
@@ -69,6 +65,7 @@
|
||||
> div {
|
||||
padding: 1rem;
|
||||
border: 0.1rem solid #979797;
|
||||
background-color: #fff;
|
||||
> img {
|
||||
width: 27.4rem;
|
||||
height: 34.6rem;
|
||||
|
||||
@@ -3,57 +3,116 @@
|
||||
<div class="content">
|
||||
<div class="mate">
|
||||
<div class="logos">
|
||||
<img src="@/assets/images/logos/code-create-black.png" />
|
||||
<img
|
||||
src="@/assets/images/logos/code-create-black.png"
|
||||
@click="openView(UrlList.codeCreate)"
|
||||
/>
|
||||
<img src="@/assets/images/logos/stylish-parade-black.png" />
|
||||
<img src="@/assets/images/logos/aida-black.png" />
|
||||
</div>
|
||||
<div class="tip">
|
||||
Stylish Parade is a commerce platform for designers, serving as AiDA's commercial
|
||||
extension.
|
||||
<img
|
||||
src="@/assets/images/logos/aida-black.png"
|
||||
@click="openView(UrlList.aida)"
|
||||
/>
|
||||
</div>
|
||||
<div class="tip">{{ $t('Home.FooterTip') }}</div>
|
||||
<div class="link">
|
||||
<span class="text">Bloom your Creativity with AiDA!</span>
|
||||
<span class="text" @click="openView(UrlList.aida)">{{
|
||||
$t('Home.FooterAidaTip')
|
||||
}}</span>
|
||||
<span class="icon"><svg-icon name="arrow_right" size="12" /></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="help">
|
||||
<div class="title">HELP</div>
|
||||
<div class="item">FAQ</div>
|
||||
<div class="item">My Account</div>
|
||||
<div class="item">My Orders</div>
|
||||
<div class="item">Payment and Invoices</div>
|
||||
<div class="item">Copyright Licence</div>
|
||||
</div>
|
||||
<div class="polices">
|
||||
<div class="title">POLICES</div>
|
||||
<div class="item">Legal</div>
|
||||
<div class="item">Privacy Policy</div>
|
||||
<div class="item">Cookies Settings</div>
|
||||
<div class="item">Purchase Conditions</div>
|
||||
</div>
|
||||
<div class="company">
|
||||
<div class="title">COMPANY</div>
|
||||
<div class="item">About us</div>
|
||||
<div class="item">Offices</div>
|
||||
<div class="item">Join with us</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<div class="left">© Code-Create 2026</div>
|
||||
<div class="right">
|
||||
<img src="@/assets/images/icons/xiaohongshu.png" />
|
||||
<img src="@/assets/images/icons/linkedin.png" />
|
||||
<img src="@/assets/images/icons/instagram.png" />
|
||||
<img src="@/assets/images/icons/facebook.png" />
|
||||
<img src="@/assets/images/icons/douyin.png" />
|
||||
<img src="@/assets/images/icons/wechat.png" />
|
||||
<div v-for="v in list" :key="v.title" :class="[v.class]">
|
||||
<div class="title">{{ $t(v.title) }}</div>
|
||||
<div v-for="item in v.child" :key="item.title" class="item" @click="onItem(item)">
|
||||
{{ $t(item.title) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<HomeFooter class="footer" isHome />
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
const router = useRouter()
|
||||
import HomeFooter from '@/components/Footer.vue'
|
||||
import { UrlList, openView } from '../../utils/UrlList'
|
||||
const list = ref([
|
||||
{
|
||||
title: 'Home.Help',
|
||||
class: 'help',
|
||||
child: [
|
||||
{
|
||||
title: 'Home.FAQ',
|
||||
url: UrlList.faq
|
||||
},
|
||||
{
|
||||
title: 'Home.MyAccount',
|
||||
name: 'settings'
|
||||
},
|
||||
{
|
||||
title: 'Home.MyOrders',
|
||||
name: 'wardrobe'
|
||||
},
|
||||
{
|
||||
title: 'Home.PaymentInvoices',
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
title: 'Home.CopyrightLicense',
|
||||
url: ''
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Home.Polices',
|
||||
class: 'polices',
|
||||
child: [
|
||||
{
|
||||
title: 'Home.Legal',
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
title: 'Home.PrivacyPolicy',
|
||||
url: UrlList.privacy
|
||||
},
|
||||
{
|
||||
title: 'Home.CookiesSettings',
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
title: 'Home.PurchaseConditions',
|
||||
url: ''
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
title: 'Home.Company',
|
||||
class: 'company',
|
||||
child: [
|
||||
{
|
||||
title: 'Home.AboutUs',
|
||||
url: UrlList.aboutUs
|
||||
},
|
||||
{
|
||||
title: 'Home.Offices',
|
||||
url: ''
|
||||
},
|
||||
{
|
||||
title: 'Home.JoinWithUs',
|
||||
url: UrlList.joinWithUs
|
||||
}
|
||||
]
|
||||
}
|
||||
])
|
||||
const onItem = (item) => {
|
||||
if (item.url) {
|
||||
openView(item.url)
|
||||
} else if (item.name) {
|
||||
router.push({ name: item.name })
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
@@ -121,6 +180,7 @@
|
||||
font-size: 1.8rem;
|
||||
color: #232323;
|
||||
margin-bottom: 2.3rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
> .item {
|
||||
font-family: KaiseiOpti-Regular;
|
||||
@@ -134,24 +194,8 @@
|
||||
}
|
||||
}
|
||||
> .footer {
|
||||
padding: 0 8rem;
|
||||
height: 7rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
> .left {
|
||||
font-family: KaiseiOpti-Regular;
|
||||
font-size: 1.2rem;
|
||||
color: #585858;
|
||||
}
|
||||
> .right {
|
||||
display: flex;
|
||||
> img {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
margin-left: 1.6rem;
|
||||
}
|
||||
}
|
||||
border: none;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -2,11 +2,8 @@
|
||||
<section class="section-index bgw">
|
||||
<img src="@/assets/images/home/bg.png" class="bg" />
|
||||
<div class="content">
|
||||
<div class="title" v-html="title"></div>
|
||||
<div class="tip">
|
||||
Discover collections through the stories behind their creation. A curated space connecting
|
||||
designers, narratives, and fashion commerce.
|
||||
</div>
|
||||
<div class="title" v-html="$t('Home.IndexTitle')"></div>
|
||||
<div class="tip">{{ $t('Home.IndexTip') }}</div>
|
||||
<button custom="black-box" @click="handleClickArrow">
|
||||
<svg-icon name="arrow_right" size="34" />
|
||||
</button>
|
||||
@@ -18,8 +15,6 @@
|
||||
import { computed } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
const router = useRouter()
|
||||
const title =
|
||||
'We’re Seeking<br /><span>Fashion Voice</span><br /><span class="small">Worth Featuring.</span>'
|
||||
const handleClickArrow = () => {
|
||||
router.push({ name: 'collectionStory' })
|
||||
}
|
||||
|
||||
@@ -22,18 +22,14 @@
|
||||
import { useI18n } from 'vue-i18n'
|
||||
const { t } = useI18n()
|
||||
const router = useRouter()
|
||||
const emit = defineEmits(['submit-email-code'])
|
||||
const emit = defineEmits(['submit-email-code', 'resend-email-code'])
|
||||
const props = defineProps({
|
||||
email: { type: String, required: true },
|
||||
type: {
|
||||
type: String as () => 'LOGIN' | 'REGISTER' | 'FORGOT_PWD',
|
||||
required: true
|
||||
},
|
||||
password: { type: String, default: '' },
|
||||
isShowOtherLogin: { type: Boolean, default: true }
|
||||
})
|
||||
const code = ref('')
|
||||
const time = ref(60)
|
||||
const time = ref(0)
|
||||
const timeStr = computed(() => CountDown(time.value))
|
||||
const timeout = ref(null)
|
||||
const setTime = (s = 120) => {
|
||||
@@ -55,7 +51,8 @@
|
||||
clearTime()
|
||||
})
|
||||
onMounted(() => {
|
||||
onSendCode()
|
||||
// onSendCode()
|
||||
setTime()
|
||||
})
|
||||
const inputCodeRef = ref(null)
|
||||
const resetCode = () => {
|
||||
@@ -68,19 +65,9 @@
|
||||
console.warn('请输入邮箱')
|
||||
return Promise.reject('请输入邮箱')
|
||||
}
|
||||
// const data = {
|
||||
// email,
|
||||
// type: props.type
|
||||
// }
|
||||
// if (props.type === 'LOGIN') {
|
||||
// data['password'] = md5(props.password)
|
||||
// }
|
||||
// const res = await SendVerificationCode(data)
|
||||
// if (!res) {
|
||||
// ElMessage.error(t('Login.sendCodeError'))
|
||||
// return Promise.reject('发送验证码失败')
|
||||
// }
|
||||
setTime()
|
||||
emit('resend-email-code', () => {
|
||||
setTime()
|
||||
})
|
||||
return Promise.resolve()
|
||||
}
|
||||
const onResend = () => {
|
||||
|
||||
@@ -14,26 +14,28 @@
|
||||
<div class="close" @click="show = false"><svg-icon name="close" /></div>
|
||||
<div class="content" v-if="curentTabInfo">
|
||||
<div class="header">
|
||||
<div class="title" v-show="curentTabInfo.title">
|
||||
<div class="title" v-if="curentTabInfo.title">
|
||||
<div class="icon" @click="onBack"><svg-icon name="back" size="17" /></div>
|
||||
<div class="label">{{ curentTabInfo.title }}</div>
|
||||
<div class="label">{{ $t(curentTabInfo.title) }}</div>
|
||||
</div>
|
||||
<div class="nav" v-show="!curentTabInfo.title">
|
||||
<div
|
||||
class="item"
|
||||
:class="{
|
||||
active: [TabNames.register, TabNames.register_success].includes(currentTab)
|
||||
active: [TabNames.register, TabNames.register_success].includes(
|
||||
currentTab
|
||||
)
|
||||
}"
|
||||
@click="currentTab = TabNames.register"
|
||||
>
|
||||
SIGN UP
|
||||
{{ $t('Login.signup') }}
|
||||
</div>
|
||||
<div
|
||||
class="item"
|
||||
:class="{ active: currentTab === TabNames.login }"
|
||||
@click="currentTab = TabNames.login"
|
||||
>
|
||||
LOG IN
|
||||
{{ $t('Login.login') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -43,11 +45,11 @@
|
||||
@login="onLogin"
|
||||
@register="onRegister"
|
||||
@submit-email-code="onSubmitEmailCode"
|
||||
@resend-email-code="onResendEmailCode"
|
||||
@back="onBack"
|
||||
:name="data.name"
|
||||
:email="data.email"
|
||||
:password="data.password"
|
||||
type="FORGOT_PWD"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -55,6 +57,12 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
AccountSendLoginCode,
|
||||
AccountLogin,
|
||||
AccountRegister,
|
||||
AccountSendVerifyCode
|
||||
} from '@/api/account'
|
||||
import { computed, ref, markRaw, watch, onBeforeUnmount } from 'vue'
|
||||
import md5 from 'md5'
|
||||
import login from './login.vue'
|
||||
@@ -63,6 +71,10 @@
|
||||
import registerSuccess from './register-success.vue'
|
||||
import retrievePassword from './retrieve-password.vue'
|
||||
import myEvent from '@/utils/myEvent'
|
||||
import { useUserInfoStore } from '@/stores/userInfo'
|
||||
import { useRouter } from 'vue-router'
|
||||
const router = useRouter()
|
||||
const userStore = useUserInfoStore()
|
||||
const data = ref({
|
||||
name: '',
|
||||
email: '',
|
||||
@@ -87,12 +99,12 @@
|
||||
},
|
||||
{
|
||||
name: TabNames.email_verify,
|
||||
title: 'EMAIL VERIFICATION',
|
||||
title: 'Login.emailVerification',
|
||||
component: emailVerify
|
||||
},
|
||||
{
|
||||
name: TabNames.retrieve_password,
|
||||
title: 'RETRIEVE PASSWORD',
|
||||
title: 'Login.retrievePassword',
|
||||
component: retrievePassword
|
||||
},
|
||||
{
|
||||
@@ -120,24 +132,53 @@
|
||||
myEvent.remove('openLoginDialog', open)
|
||||
})
|
||||
|
||||
const onLogin = (res: any) => {
|
||||
const onLogin = async (res: any) => {
|
||||
await AccountSendLoginCode({
|
||||
email: res.email,
|
||||
password: md5(res.password)
|
||||
})
|
||||
data.value = res
|
||||
data.value.type = TabNames.login
|
||||
currentTab.value = TabNames.email_verify
|
||||
}
|
||||
const onRegister = (res: any) => {
|
||||
const onRegister = async (res: any) => {
|
||||
const value = {
|
||||
email: res.email,
|
||||
operationType: 'REGISTER'
|
||||
}
|
||||
await AccountSendVerifyCode(value)
|
||||
data.value = res
|
||||
data.value.type = TabNames.register
|
||||
currentTab.value = TabNames.email_verify
|
||||
}
|
||||
const onSubmitEmailCode = (code: string) => {
|
||||
if (data.value.type === TabNames.login) {
|
||||
console.log('登录', code)
|
||||
show.value = false
|
||||
} else {
|
||||
console.log('注册', code)
|
||||
currentTab.value = TabNames.register_success
|
||||
const value = {
|
||||
email: data.value.email,
|
||||
password: md5(data.value.password),
|
||||
emailVerifyCode: code
|
||||
}
|
||||
if (data.value.type === TabNames.login) {
|
||||
AccountLogin(value).then((v) => {
|
||||
userStore.setUserInfo(v)
|
||||
show.value = false
|
||||
setTimeout(() => {
|
||||
router.go(0)
|
||||
})
|
||||
})
|
||||
} else {
|
||||
value['username'] = data.value.name
|
||||
AccountRegister(value).then((v) => {
|
||||
currentTab.value = TabNames.register_success
|
||||
})
|
||||
}
|
||||
}
|
||||
const onResendEmailCode = async (callback: () => void) => {
|
||||
if (data.value.type === TabNames.login) {
|
||||
await onLogin(data.value)
|
||||
} else if (data.value.type === TabNames.register) {
|
||||
await onRegister(data.value)
|
||||
}
|
||||
callback()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -210,6 +251,7 @@
|
||||
font-size: 2rem;
|
||||
font-family: KaiseiOpti-Regular;
|
||||
color: #9f9f9f;
|
||||
text-transform: uppercase;
|
||||
&.active {
|
||||
font-family: KaiseiOpti-Bold;
|
||||
color: #232323;
|
||||
@@ -239,6 +281,7 @@
|
||||
font-family: KaiseiOpti-Bold;
|
||||
font-size: 2rem;
|
||||
color: #232323;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { validateEmail, validatePassLength, validatePrivacy } from './tools'
|
||||
import { validateEmail, validatePass, validatePrivacy } from './tools'
|
||||
import OtherLogin from './other-login.vue'
|
||||
const emit = defineEmits(['retrieve-password', 'login'])
|
||||
const props = defineProps({
|
||||
@@ -43,7 +43,7 @@
|
||||
})
|
||||
const ruleForm = reactive({
|
||||
email: [{ validator: validateEmail, trigger: 'change' }],
|
||||
password: [{ validator: validatePassLength, trigger: 'change' }],
|
||||
password: [{ validator: validatePass, trigger: 'change' }],
|
||||
privacy: [{ validator: validatePrivacy, trigger: 'change' }]
|
||||
})
|
||||
const formData = reactive({
|
||||
|
||||
@@ -7,13 +7,13 @@
|
||||
</el-icon>
|
||||
<span>{{ $t('Login.passwordLengthError', { min: 6, max: 20 }) }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<!-- <div>
|
||||
<el-icon>
|
||||
<CloseBold v-if="validateSpecial(value)" />
|
||||
<Select v-else />
|
||||
</el-icon>
|
||||
<span>{{ $t('Login.passwordSpecial') }}</span>
|
||||
</div>
|
||||
</div> -->
|
||||
<div>
|
||||
<el-icon>
|
||||
<CloseBold v-if="validateCase(value)" />
|
||||
@@ -42,7 +42,7 @@
|
||||
color: #fff;
|
||||
font-size: 1.1rem;
|
||||
padding: 1.5rem;
|
||||
border-radius: 1.5rem;
|
||||
// border-radius: 1.5rem;
|
||||
line-height: normal;
|
||||
|
||||
> div {
|
||||
|
||||
@@ -1,31 +1,25 @@
|
||||
<template>
|
||||
<div class="register-success">
|
||||
<div class="icon"><svg-icon name="dui" size="20" /></div>
|
||||
<div class="title">Welcome to Stylish Parade!</div>
|
||||
<div class="title">Please switch to the Login tab to log in.</div>
|
||||
<div class="title">{{ t('RegisterSuccess.title1') }}</div>
|
||||
<div class="title">{{ t('RegisterSuccess.title2') }}</div>
|
||||
<div class="footer">
|
||||
<div class="title">
|
||||
<span class="text">What awaits you in Stylish Parade</span>
|
||||
<span class="text">{{ t('RegisterSuccess.title3') }}</span>
|
||||
<span class="icon"><svg-icon name="arrow_right" size="11" /></span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div>
|
||||
<div class="title">Behind the design</div>
|
||||
<div class="tip">
|
||||
Discover how designers bring ideas to life with AiDA — from first sketch to final look.
|
||||
</div>
|
||||
<div class="title">{{ t('RegisterSuccess.item1title') }}</div>
|
||||
<div class="tip">{{ t('RegisterSuccess.item1tip') }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="title">Creative digital works</div>
|
||||
<div class="tip">
|
||||
Unlock a growing library of inspiring digital works to refresh your creative mind.
|
||||
</div>
|
||||
<div class="title">{{ t('RegisterSuccess.item2title') }}</div>
|
||||
<div class="tip">{{ t('RegisterSuccess.item2tip') }}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="title">A fashion community</div>
|
||||
<div class="tip">
|
||||
Join a space where fashion speaks — exchange ideas and connect with creators worldwide.
|
||||
</div>
|
||||
<div class="title">{{ t('RegisterSuccess.item3title') }}</div>
|
||||
<div class="tip">{{ t('RegisterSuccess.item3tip') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
<div class="password-warning">
|
||||
<span class="icon"><svg-icon name="warning" size="12" /></span>
|
||||
<span class="label">You must satisfy ALL password conditions to register.</span>
|
||||
<span class="label">{{ $t('Login.passwordTip') }}</span>
|
||||
</div>
|
||||
<el-form-item :label="$t('Login.email')" prop="email">
|
||||
<el-input name="email" v-model="formData.email" :placeholder="$t('Login.enterEmail')" />
|
||||
|
||||
@@ -7,21 +7,25 @@
|
||||
ref="form1Ref"
|
||||
v-show="index === 0"
|
||||
>
|
||||
<div class="title">Please enter your email address below to verify your identity.</div>
|
||||
<div class="title">{{ $t('Login.retrievePasswordTitle') }}</div>
|
||||
<el-form-item :label="$t('Login.email')" prop="email">
|
||||
<el-input v-model="formData.email" :placeholder="$t('Login.enterEmail')" name="email" />
|
||||
<el-input
|
||||
v-model="formData.email"
|
||||
:placeholder="$t('Login.enterEmail')"
|
||||
name="email"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item class="submit-item">
|
||||
<button class="submit" type="submit" custom="black" @click.prevent="onSubmit1">
|
||||
SUBMIT
|
||||
{{ $t('Login.submit') }}
|
||||
</button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<div class="verify-box" v-if="index === 1">
|
||||
<email-verify
|
||||
type="FORGOT_PWD"
|
||||
:email="formData.email"
|
||||
@submit-email-code="onVerifyCode"
|
||||
@resend-email-code="SendVerifyCode"
|
||||
:is-show-other-login="false"
|
||||
/>
|
||||
</div>
|
||||
@@ -32,10 +36,10 @@
|
||||
ref="form2Ref"
|
||||
v-show="index === 2"
|
||||
>
|
||||
<div class="title">
|
||||
Enter a new password for <br />
|
||||
<span>{{ formData.email }}</span>
|
||||
</div>
|
||||
<div
|
||||
class="title"
|
||||
v-html="$t('Login.enterNewPassword', { email: formData.email })"
|
||||
></div>
|
||||
<el-form-item :label="$t('Login.password')" prop="password">
|
||||
<password-tip :value="formData.password" v-show="showPasswordTip" />
|
||||
<el-input
|
||||
@@ -50,7 +54,7 @@
|
||||
</el-form-item>
|
||||
<div class="password-warning">
|
||||
<span class="icon"><svg-icon name="warning" size="12" /></span>
|
||||
<span class="label">You must satisfy ALL password conditions to register.</span>
|
||||
<span class="label">{{ $t('Login.passwordTip') }}</span>
|
||||
</div>
|
||||
<el-form-item :label="$t('Login.passwordConfirmation')" prop="confirmPassword">
|
||||
<el-input
|
||||
@@ -63,7 +67,7 @@
|
||||
</el-form-item>
|
||||
<el-form-item class="submit-item">
|
||||
<button class="submit" type="submit" custom="black" @click.prevent="onSubmit2">
|
||||
SUBMIT
|
||||
{{ $t('Login.submit') }}
|
||||
</button>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
@@ -71,15 +75,18 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { AccountSendVerifyCode, AccountVerifyCode, AccountResetPassword } from '@/api/account'
|
||||
import md5 from 'md5'
|
||||
import { computed, reactive, ref } from 'vue'
|
||||
import { validateEmail, validatePass } from './tools'
|
||||
import PasswordTip from './password-tip.vue'
|
||||
import EmailVerify from './email-verify.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
const { t } = useI18n()
|
||||
const emit = defineEmits(['back'])
|
||||
const validateConfirmPassword = (rule: any, value: string, callback: any) => {
|
||||
if (value !== formData.password) {
|
||||
callback(new Error('Passwords do not match'))
|
||||
callback(new Error(t('Login.passwordsDoNotMatch')))
|
||||
} else {
|
||||
callback()
|
||||
}
|
||||
@@ -103,32 +110,47 @@
|
||||
const onSubmit1 = () => {
|
||||
form1Ref.value?.validate?.((valid) => {
|
||||
if (valid) {
|
||||
index.value = 1
|
||||
SendVerifyCode().then(() => {
|
||||
index.value = 1
|
||||
})
|
||||
} else {
|
||||
console.warn('error submit!')
|
||||
}
|
||||
})
|
||||
}
|
||||
const SendVerifyCode = async () => {
|
||||
await AccountSendVerifyCode({
|
||||
email: formData.email,
|
||||
operationType: 'FORGET_PWD'
|
||||
})
|
||||
}
|
||||
const onVerifyCode = (code: string) => {
|
||||
if (!code) return
|
||||
AccountVerifyCode({
|
||||
email: formData.email,
|
||||
emailVerifyCode: code,
|
||||
operationType: 'FORGET_PWD'
|
||||
}).then(() => {
|
||||
formData.code = code
|
||||
index.value = 2
|
||||
})
|
||||
}
|
||||
const onSubmit2 = () => {
|
||||
form2Ref.value?.validate?.((valid) => {
|
||||
if (valid) {
|
||||
const data = {
|
||||
email: formData.email,
|
||||
code: formData.code,
|
||||
password: md5(formData.password)
|
||||
password: md5(formData.password),
|
||||
emailVerifyCode: formData.code
|
||||
}
|
||||
console.log(data)
|
||||
emit('back')
|
||||
AccountResetPassword(data).then(() => {
|
||||
emit('back')
|
||||
})
|
||||
} else {
|
||||
console.warn('error submit!')
|
||||
}
|
||||
})
|
||||
}
|
||||
const onVerifyCode = (code: string) => {
|
||||
if (!code) return
|
||||
formData.code = code
|
||||
index.value = 2
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
@@ -142,6 +164,9 @@
|
||||
flex-direction: column;
|
||||
.el-form-item.submit-item {
|
||||
margin-top: auto;
|
||||
.submit {
|
||||
text-transform: uppercase;
|
||||
}
|
||||
}
|
||||
.el-input {
|
||||
--el-input-height: 4.8rem;
|
||||
|
||||
@@ -23,15 +23,15 @@ export const validateEmail = (rule, value, callback) => {
|
||||
export const validateLength = (v, min = 6, max = 20) => (v.length < 6 || v.length > 20);
|
||||
//检查特殊字符
|
||||
export const validateSpecial = (v) => (!/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(v));
|
||||
//检查大小写字母和数字
|
||||
export const validateCase = (v) => (!/[a-z]/.test(v) || !/[A-Z]/.test(v) || !/\d/.test(v));
|
||||
//检查字母和数字
|
||||
export const validateCase = (v) => (!/[A-z]/.test(v) || !/\d/.test(v));
|
||||
// 检查密码
|
||||
export const validatePass = (rule, value, callback) => {
|
||||
var str = ''
|
||||
if (validateLength(value)) {
|
||||
str = t('Login.passwordLengthError', { min: 6, max: 20 })
|
||||
} else if (validateSpecial(value)) {
|
||||
str = t('Login.passwordSpecial')
|
||||
// } else if (validateSpecial(value)) {
|
||||
// str = t('Login.passwordSpecial')
|
||||
} else if (validateCase(value)) {
|
||||
str = t('Login.passwordCase')
|
||||
}
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
<template>
|
||||
<div class="main-header" id="main-header">
|
||||
<div class="left">
|
||||
<img class="logo" src="@/assets/images/logo.png" @click="onNavItemClick('/')" />
|
||||
<img
|
||||
class="logo"
|
||||
src="@/assets/images/logo.png"
|
||||
@click="onNavItemClick({ path: '/' })"
|
||||
/>
|
||||
</div>
|
||||
<div class="center">
|
||||
<div
|
||||
v-for="v in navList1"
|
||||
:key="v.path"
|
||||
class="nav-item"
|
||||
:class="{ active: activePath === v.path }"
|
||||
@click="onNavItemClick(v.path)"
|
||||
:class="{
|
||||
active:
|
||||
v.path === '/'
|
||||
? activePath === v.path
|
||||
: new RegExp(`^${v.path}`).test(activePath)
|
||||
}"
|
||||
@click="onNavItemClick(v)"
|
||||
>
|
||||
<span>{{ v.name }}</span>
|
||||
<span>{{ $t(v.name) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="right">
|
||||
@@ -19,49 +28,65 @@
|
||||
class="icon"
|
||||
v-for="v in navList2"
|
||||
:key="v.path"
|
||||
:class="{ active: activePath === v.path }"
|
||||
@click="onNavItemClick(v.path)"
|
||||
:class="{ active: new RegExp(`^${v.path}`).test(activePath) }"
|
||||
@click="onNavItemClick(v)"
|
||||
>
|
||||
<svg-icon :name="activePath === v.path ? v.active_icon : v.icon" size="22" />
|
||||
</div>
|
||||
<div class="login" @click="onLogin">Login</div>
|
||||
<el-popover
|
||||
ref="profilePopover"
|
||||
placement="bottom-end"
|
||||
trigger="click"
|
||||
:show-arrow="false"
|
||||
popper-style="width: 24rem; padding: 0; border-radius: 0; right: 2rem; top: 10rem;"
|
||||
v-if="userInfo.userId"
|
||||
>
|
||||
<template #reference><div class="profile"></div></template>
|
||||
<template #reference>
|
||||
<el-badge :value="unReadCount" :hidden="unReadCount < 1">
|
||||
<div class="icon"><svg-icon name="user_0" size="22" /></div>
|
||||
</el-badge>
|
||||
</template>
|
||||
<template #default>
|
||||
<div class="profile-content">
|
||||
<div class="info">
|
||||
<img src="@/assets/images/profile-content-bg.jpg" alt="" />
|
||||
<div class="content">
|
||||
<div class="profile"></div>
|
||||
<div class="name">Hi, Alexandra_chen</div>
|
||||
<div class="profile">
|
||||
<img class="profile-avatar" :src="userInfo.avatarUrl" alt="" />
|
||||
</div>
|
||||
<div class="name">
|
||||
{{ $t('MainHeader.HiName', { name: userInfo.username }) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav-item" @click="onMyWardrobe">
|
||||
<div class="icon"><svg-icon name="my_wardrobe" size="18" /></div>
|
||||
<div class="label">My Wardrobe</div>
|
||||
</div>
|
||||
<div class="nav-item" @click="onNotifications">
|
||||
<div class="icon"><svg-icon name="notifications" size="14" /></div>
|
||||
<div class="label">Notifications</div>
|
||||
<div class="label">{{ $t('MainHeader.MyWardrobe') }}</div>
|
||||
</div>
|
||||
<el-badge
|
||||
:value="unReadCount"
|
||||
class="nav-item badge"
|
||||
:offset="[10, 0]"
|
||||
:hidden="unReadCount < 1"
|
||||
>
|
||||
<div class="nav-item flex" @click="onNotifications">
|
||||
<div class="icon"><svg-icon name="notifications" size="14" /></div>
|
||||
<div class="label">{{ $t('MainHeader.Notifications') }}</div>
|
||||
</div>
|
||||
</el-badge>
|
||||
<div class="nav-item" @click="onSettings">
|
||||
<div class="icon"><svg-icon name="settings" size="16" /></div>
|
||||
<div class="label">Settings</div>
|
||||
<div class="label">{{ $t('MainHeader.Settings') }}</div>
|
||||
</div>
|
||||
<div class="hr"></div>
|
||||
<div class="nav-item logout" @click="onLogout">
|
||||
<div class="icon"><svg-icon name="logout" size="20" /></div>
|
||||
<div class="label">Log off</div>
|
||||
<div class="label">{{ $t('Login.logoff') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</el-popover>
|
||||
<div class="login" @click="onLogin" v-else>{{ $t('Login.login') }}</div>
|
||||
<div class="language" @click="onLanguageClick">
|
||||
<span :class="{ active: locale === 'CHINESE_SIMPLIFIED' }">中</span> /
|
||||
<span :class="{ active: locale === 'ENGLISH' }">ENG</span>
|
||||
@@ -71,29 +96,38 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ElMessageBox } from 'element-plus'
|
||||
import { computed, ref } from 'vue'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import myEvent from '@/utils/myEvent'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useUserInfoStore } from '@/stores/userInfo'
|
||||
import { useGlobalStore } from '@/stores/global'
|
||||
import { setUserLanguage } from '@/api/user'
|
||||
|
||||
const { t, locale } = useI18n()
|
||||
const userInfoStore = useUserInfoStore()
|
||||
const GLOABL = useGlobalStore()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const userInfo = computed(() => userInfoStore.state.userInfo)
|
||||
const activePath = computed(() => route.path)
|
||||
const unReadCount = computed(() => GLOABL.state.unReadMessageCount)
|
||||
const navList1 = ref([
|
||||
{
|
||||
name: 'Home',
|
||||
name: 'MainHeader.Home',
|
||||
path: '/'
|
||||
},
|
||||
{
|
||||
name: 'Collection Story',
|
||||
name: 'MainHeader.CollectionStory',
|
||||
path: '/collectionStory'
|
||||
},
|
||||
{
|
||||
name: 'Brand',
|
||||
name: 'MainHeader.Brand',
|
||||
path: '/brand'
|
||||
},
|
||||
{
|
||||
name: 'Digital Item',
|
||||
name: 'MainHeader.DigitalItem',
|
||||
path: '/digitalItem'
|
||||
}
|
||||
])
|
||||
@@ -101,15 +135,15 @@
|
||||
{
|
||||
icon: 'cart_0',
|
||||
active_icon: 'cart_1',
|
||||
path: '/shoppingCart'
|
||||
},
|
||||
{
|
||||
icon: 'user_0',
|
||||
active_icon: 'user_1',
|
||||
path: '/account'
|
||||
path: '/shoppingCart',
|
||||
login: true
|
||||
}
|
||||
])
|
||||
const onNavItemClick = (path: string) => {
|
||||
const onNavItemClick = (v: any) => {
|
||||
const path = v.path
|
||||
if (v.login && !userInfoStore.state.token) {
|
||||
return onLogin()
|
||||
}
|
||||
if (path === activePath.value) return
|
||||
router.push(path)
|
||||
}
|
||||
@@ -122,7 +156,6 @@
|
||||
}
|
||||
const onMyWardrobe = () => {
|
||||
hideProfilePopover()
|
||||
console.log('my wardrobe')
|
||||
router.push('/wardrobe')
|
||||
}
|
||||
const onNotifications = () => {
|
||||
@@ -135,11 +168,20 @@
|
||||
}
|
||||
const onLogout = () => {
|
||||
hideProfilePopover()
|
||||
console.log('logout')
|
||||
ElMessageBox.confirm(t('Login.logOffTip'))
|
||||
.then(() => {
|
||||
userInfoStore.logout(true)
|
||||
})
|
||||
.catch(() => {})
|
||||
}
|
||||
const onLanguageClick = () => {
|
||||
locale.value = locale.value === 'ENGLISH' ? 'CHINESE_SIMPLIFIED' : 'ENGLISH'
|
||||
localStorage.setItem('language', locale.value)
|
||||
|
||||
setUserLanguage(locale.value).then((res) => {
|
||||
userInfoStore.setToken(res)
|
||||
location.reload()
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -201,7 +243,7 @@
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
> .nav-item {
|
||||
.nav-item {
|
||||
height: 100%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
@@ -246,7 +288,12 @@
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
border-radius: 50%;
|
||||
background: #cfcfcf;
|
||||
.profile-avatar {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
> .name {
|
||||
font-size: 1.4rem;
|
||||
@@ -258,7 +305,7 @@
|
||||
margin: 1.2rem 1rem;
|
||||
border-top: 0.1rem solid #c4c4c4;
|
||||
}
|
||||
> .nav-item {
|
||||
.nav-item {
|
||||
margin-left: 2rem;
|
||||
margin-bottom: 1.2rem;
|
||||
display: flex;
|
||||
@@ -266,10 +313,14 @@
|
||||
height: 2rem;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
column-gap: 1rem;
|
||||
&.badge {
|
||||
margin: 0;
|
||||
}
|
||||
> .icon {
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
margin-right: 1rem;
|
||||
// margin-right: 1rem;
|
||||
}
|
||||
> .label {
|
||||
font-family: KaiseiOpti-Regular;
|
||||
|
||||
@@ -1,166 +1,248 @@
|
||||
<template>
|
||||
<div class="notifications-view mini-scrollbar">
|
||||
<section class="notifications-hero">
|
||||
<div class="notifications-hero__content">
|
||||
<h1 class="notifications-hero__title">Notifications</h1>
|
||||
<p class="notifications-hero__subtitle">System announcements and updates</p>
|
||||
</div>
|
||||
</section>
|
||||
<div class="notifications-view mini-scrollbar" @scroll="handleScroll">
|
||||
<section class="notifications-hero">
|
||||
<div class="notifications-hero__content">
|
||||
<h1 class="notifications-hero__title">Notifications</h1>
|
||||
<p class="notifications-hero__subtitle">System announcements and updates</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="notifications-content">
|
||||
<NotificationsList
|
||||
:items="notifications"
|
||||
:unread-count="unreadCount"
|
||||
@toggle-item="handleToggleItem"
|
||||
@mark-all-as-read="handleMarkAllAsRead"
|
||||
/>
|
||||
</section>
|
||||
</div>
|
||||
<section class="notifications-content">
|
||||
<NotificationsList
|
||||
:items="notifications"
|
||||
:unread-count="unreadCount"
|
||||
@toggle-item="handleToggleItem"
|
||||
@mark-all-as-read="handleMarkAllAsRead"
|
||||
/>
|
||||
<div v-if="loading" class="loading-indicator">Loading...</div>
|
||||
<div v-if="!hasMore && notifications.length > 0" class="no-more-data">No more notifications</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import NotificationsList from './components/NotificationsList.vue'
|
||||
import type { NotificationRecord } from './types'
|
||||
import { computed, ref, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import NotificationsList from './components/NotificationsList.vue'
|
||||
import type { NotificationRecord } from './types'
|
||||
import { fetchAllMessageList, markMessageAsRead, markAllMessagesAsRead } from '@/api/notification'
|
||||
|
||||
const notifications = ref<NotificationRecord[]>([
|
||||
{
|
||||
id: 'maintenance-mar-10-primary',
|
||||
title: 'Platform Maintenance Notice - Mar 10, 2026',
|
||||
date: 'Mar 6, 2026',
|
||||
content:
|
||||
'We will perform scheduled platform maintenance on Mar 10, 2026 to improve checkout stability and notification delivery. During this window, a few account features may respond more slowly than usual.',
|
||||
isUnread: true,
|
||||
isExpanded: false
|
||||
},
|
||||
{
|
||||
id: 'maintenance-mar-10-reminder',
|
||||
title: 'Platform Maintenance Notice - Mar 10, 2026',
|
||||
date: 'Feb 28, 2026',
|
||||
content:
|
||||
'This is an early reminder for the Mar 10 maintenance window. Please avoid making urgent profile or order updates right before the scheduled service period.',
|
||||
isUnread: true,
|
||||
isExpanded: false
|
||||
},
|
||||
{
|
||||
id: 'terms-mar-1',
|
||||
title: 'Updated Terms of Service - Effective Mar 1, 2026',
|
||||
date: 'Feb 20, 2026',
|
||||
content:
|
||||
'We updated our Terms of Service to clarify digital item ownership, payment processing responsibilities, and account conduct expectations. Please review the new terms before your next purchase.',
|
||||
isUnread: true,
|
||||
isExpanded: false
|
||||
},
|
||||
{
|
||||
id: 'welcome',
|
||||
title: 'Welcome to Stylish Parade',
|
||||
date: 'Jan 4, 2026',
|
||||
content:
|
||||
'Thanks for joining Stylish Parade. Explore brand stories, save your favorite pieces, and keep your profile updated so we can recommend the right collections for you.',
|
||||
isUnread: false,
|
||||
isExpanded: false
|
||||
},
|
||||
{
|
||||
id: 'holiday-support',
|
||||
title: 'Platform Maintenance Notice - Mar 10, 2026',
|
||||
date: 'Dec 20, 2025',
|
||||
content:
|
||||
'Our customer support team will have limited availability during the holiday season from Dec 24 to Jan 2. Response times may be longer than usual. The platform will remain fully operational throughout this period. We wish you a wonderful holiday season.',
|
||||
isUnread: false,
|
||||
isExpanded: true
|
||||
}
|
||||
])
|
||||
const { locale } = useI18n()
|
||||
|
||||
const unreadCount = computed(() => notifications.value.filter((item) => item.isUnread).length)
|
||||
// Store notifications with original date strings
|
||||
interface NotificationWithOriginalDate extends NotificationRecord {
|
||||
originalDate: string
|
||||
}
|
||||
|
||||
const handleToggleItem = (id: NotificationRecord['id']) => {
|
||||
const targetItem = notifications.value.find((item) => item.id === id)
|
||||
const notificationsRaw = ref<NotificationWithOriginalDate[]>([])
|
||||
const loading = ref(false)
|
||||
const hasMore = ref(true)
|
||||
|
||||
if (!targetItem) return
|
||||
/**
|
||||
* Format date based on current language
|
||||
* Chinese: yyyy年M月D日 (e.g., 2024年5月15日)
|
||||
* English: Month Day, Year (e.g., January 15, 2024)
|
||||
*/
|
||||
const formatDate = (dateString: string): string => {
|
||||
if (!dateString) return ''
|
||||
|
||||
const date = new Date(dateString)
|
||||
if (isNaN(date.getTime())) return dateString
|
||||
|
||||
const nextExpanded = !targetItem.isExpanded
|
||||
const isChinese = locale.value === 'CHINESE_SIMPLIFIED'
|
||||
|
||||
notifications.value = notifications.value.map((item) => ({
|
||||
...item,
|
||||
isUnread: item.id === id ? false : item.isUnread,
|
||||
isExpanded: item.id === id ? nextExpanded : false
|
||||
}))
|
||||
}
|
||||
if (isChinese) {
|
||||
// Chinese format: yyyy年M月D日 (without leading zeros)
|
||||
const year = date.getFullYear()
|
||||
const month = date.getMonth() + 1
|
||||
const day = date.getDate()
|
||||
return `${year}年${month}月${day}日`
|
||||
} else {
|
||||
// English format: Month Day, Year
|
||||
const options: Intl.DateTimeFormatOptions = {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
}
|
||||
return date.toLocaleDateString('en-US', options)
|
||||
}
|
||||
}
|
||||
|
||||
const handleMarkAllAsRead = () => {
|
||||
notifications.value = notifications.value.map((item) => ({
|
||||
...item,
|
||||
isUnread: false
|
||||
}))
|
||||
}
|
||||
// Computed property that formats dates based on current locale
|
||||
const notifications = computed<NotificationRecord[]>(() => {
|
||||
return notificationsRaw.value.map((item) => ({
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
date: formatDate(item.originalDate),
|
||||
content: item.content,
|
||||
isUnread: item.isUnread,
|
||||
isExpanded: item.isExpanded
|
||||
}))
|
||||
})
|
||||
|
||||
const unreadCount = computed(() => notifications.value.filter((item) => item.isUnread).length)
|
||||
|
||||
const handleToggleItem = async (id: NotificationRecord['id']) => {
|
||||
const targetItem = notificationsRaw.value.find((item) => item.id === id)
|
||||
|
||||
if (!targetItem) return
|
||||
|
||||
const nextExpanded = !targetItem.isExpanded
|
||||
|
||||
// 如果消息是未读状态,调用API标记为已读
|
||||
if (targetItem.isUnread) {
|
||||
try {
|
||||
await markMessageAsRead(id)
|
||||
} catch (error) {
|
||||
console.error('Failed to mark message as read:', error)
|
||||
}
|
||||
}
|
||||
|
||||
notificationsRaw.value = notificationsRaw.value.map((item) => ({
|
||||
...item,
|
||||
isUnread: item.id === id ? false : item.isUnread,
|
||||
isExpanded: item.id === id ? nextExpanded : false
|
||||
}))
|
||||
}
|
||||
|
||||
const handleMarkAllAsRead = async () => {
|
||||
try {
|
||||
await markAllMessagesAsRead()
|
||||
notificationsRaw.value = notificationsRaw.value.map((item) => ({
|
||||
...item,
|
||||
isUnread: false
|
||||
}))
|
||||
} catch (error) {
|
||||
console.error('Failed to mark all messages as read:', error)
|
||||
}
|
||||
}
|
||||
|
||||
const params = ref({
|
||||
page: 1,
|
||||
size: 15
|
||||
})
|
||||
|
||||
const handleFetchMessageList = async () => {
|
||||
if (loading.value || !hasMore.value) return
|
||||
|
||||
loading.value = true
|
||||
try {
|
||||
const res = await fetchAllMessageList(params.value)
|
||||
|
||||
// Transform API data to match NotificationRecord interface with original date
|
||||
const newNotifications: NotificationWithOriginalDate[] = res.content.map((item: any) => ({
|
||||
id: String(item.id),
|
||||
title: item.title,
|
||||
date: item.createTime, // This will be formatted by computed property
|
||||
originalDate: item.createTime, // Store original date for re-formatting
|
||||
content: item.content,
|
||||
isUnread: item.isRead === 0,
|
||||
isExpanded: false
|
||||
}))
|
||||
|
||||
if (params.value.page === 1) {
|
||||
notificationsRaw.value = newNotifications
|
||||
} else {
|
||||
notificationsRaw.value = [...notificationsRaw.value, ...newNotifications]
|
||||
}
|
||||
|
||||
// Check if there are more pages
|
||||
hasMore.value = params.value.page < res.pages
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch notifications:', error)
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleScroll = (event: Event) => {
|
||||
const target = event.target as HTMLElement
|
||||
const scrollTop = target.scrollTop
|
||||
const scrollHeight = target.scrollHeight
|
||||
const clientHeight = target.clientHeight
|
||||
|
||||
// Load more when scrolled to 80% of the content
|
||||
if (scrollTop + clientHeight >= scrollHeight * 0.8 && !loading.value && hasMore.value) {
|
||||
params.value.page++
|
||||
handleFetchMessageList()
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
handleFetchMessageList()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.notifications-view {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
background: #ffffff;
|
||||
}
|
||||
.notifications-view {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.notifications-hero {
|
||||
min-height: 14.8rem;
|
||||
padding: 3.2rem 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(180deg, rgba(245, 243, 240, 0.92) 0%, rgba(250, 249, 246, 0.95) 100%),
|
||||
linear-gradient(
|
||||
90deg,
|
||||
rgba(227, 221, 212, 0.28) 0%,
|
||||
rgba(255, 255, 255, 0.24) 50%,
|
||||
rgba(227, 221, 212, 0.28) 100%
|
||||
);
|
||||
border-bottom: 0.05rem solid #dfd8d1;
|
||||
}
|
||||
.notifications-hero {
|
||||
min-height: 14.8rem;
|
||||
padding: 3.2rem 2rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: url('@/assets/images/wardrobe/settings_bg.jpg') no-repeat;
|
||||
background-size: cover;
|
||||
border-bottom: 0.05rem solid #dfd8d1;
|
||||
}
|
||||
|
||||
.notifications-hero__content {
|
||||
text-align: center;
|
||||
}
|
||||
.notifications-hero__content {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.notifications-hero__title {
|
||||
margin: 0;
|
||||
color: #232323;
|
||||
font-size: 4rem;
|
||||
line-height: 1;
|
||||
font-family: 'KaiseiOpti-Bold';
|
||||
}
|
||||
.notifications-hero__title {
|
||||
margin: 0;
|
||||
color: #232323;
|
||||
font-size: 4rem;
|
||||
line-height: 1;
|
||||
font-family: 'KaiseiOpti-Bold';
|
||||
}
|
||||
|
||||
.notifications-hero__subtitle {
|
||||
margin: 1.2rem 0 0;
|
||||
color: #585858;
|
||||
font-size: 1.6rem;
|
||||
line-height: 1.4;
|
||||
font-family: 'KaiseiOpti-Regular';
|
||||
}
|
||||
.notifications-hero__subtitle {
|
||||
margin: 1.2rem 0 0;
|
||||
color: #585858;
|
||||
font-size: 1.6rem;
|
||||
line-height: 1.4;
|
||||
font-family: 'KaiseiOpti-Regular';
|
||||
}
|
||||
|
||||
.notifications-content {
|
||||
width: min(108rem, calc(100% - 4rem));
|
||||
margin: 0 auto;
|
||||
padding: 4rem 0 8rem;
|
||||
}
|
||||
.notifications-content {
|
||||
width: min(108rem, calc(100% - 4rem));
|
||||
margin: 0 auto;
|
||||
padding: 4rem 0 8rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.notifications-hero {
|
||||
min-height: 12rem;
|
||||
padding: 2.8rem 1.6rem;
|
||||
}
|
||||
.loading-indicator,
|
||||
.no-more-data {
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
color: #979797;
|
||||
font-size: 1.4rem;
|
||||
font-family: 'KaiseiOpti-Regular';
|
||||
}
|
||||
|
||||
.notifications-hero__title {
|
||||
font-size: 3rem;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.notifications-hero {
|
||||
min-height: 12rem;
|
||||
padding: 2.8rem 1.6rem;
|
||||
}
|
||||
|
||||
.notifications-hero__subtitle {
|
||||
margin-top: 0.8rem;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
.notifications-hero__title {
|
||||
font-size: 3rem;
|
||||
}
|
||||
|
||||
.notifications-content {
|
||||
width: calc(100% - 3.2rem);
|
||||
padding: 3.2rem 0 4.8rem;
|
||||
}
|
||||
}
|
||||
.notifications-hero__subtitle {
|
||||
margin-top: 0.8rem;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.notifications-content {
|
||||
width: calc(100% - 3.2rem);
|
||||
padding: 3.2rem 0 4.8rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
55
src/views/pay/index.vue
Normal file
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<div class="pay">
|
||||
<div class="content">
|
||||
<payment :ids="ids" />
|
||||
<sc-list :title="$t('Pay.OrderSummary')" is-view is-mini :list="list" />
|
||||
</div>
|
||||
<my-footer />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import scList from '../shoppingCart/sc-list.vue'
|
||||
import payment from './payment.vue'
|
||||
import myFooter from '@/components/Footer.vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { SCART_STATUS } from '../shoppingCart/index.d'
|
||||
|
||||
const route = useRoute()
|
||||
const list = ref([])
|
||||
const ids = computed(() => {
|
||||
return list.value.filter((v) => v.status === SCART_STATUS.NORMAL).map((item) => item.listingId)
|
||||
})
|
||||
try {
|
||||
let str = route.query.list as string
|
||||
list.value = JSON.parse(decodeURIComponent(atob(str)))
|
||||
console.log(list.value)
|
||||
} catch (error) {
|
||||
console.log(error)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.pay {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
overflow-y: auto;
|
||||
--content-top: 4.8rem;
|
||||
> .content {
|
||||
min-height: calc(var(--app-view-height) - var(--footer-height));
|
||||
max-width: 160rem;
|
||||
margin: 0 auto;
|
||||
padding: 4rem 9rem 10rem;
|
||||
gap: 10rem;
|
||||
display: flex;
|
||||
> .payment {
|
||||
width: 60%;
|
||||
}
|
||||
> .sc-list {
|
||||
width: 40%;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
391
src/views/pay/payment.vue
Normal file
@@ -0,0 +1,391 @@
|
||||
<template>
|
||||
<div class="payment">
|
||||
<div class="header" @click="onBack">
|
||||
<span class="icon"><svg-icon name="back" size="30" /></span>
|
||||
<span class="text">{{ $t('Pay.PaymentDetails') }}</span>
|
||||
</div>
|
||||
<!-- 未支付 -->
|
||||
<template v-if="paymentStatus !== ORDER_STATUS.SUCCESS">
|
||||
<div class="paylist">
|
||||
<div class="item">
|
||||
<img src="@/assets/images/pay/stripe.png" alt="" />
|
||||
<span>{{ $t('Pay.CreditDebitCard') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="agreement">
|
||||
<el-checkbox v-model="agreement">
|
||||
<div
|
||||
class="text"
|
||||
v-html="
|
||||
$t('Pay.AgreementText', {
|
||||
onTermsClick: 'openTermsView()',
|
||||
onPrivacyClick: 'openPrivacyView()'
|
||||
})
|
||||
"
|
||||
></div
|
||||
></el-checkbox>
|
||||
</div>
|
||||
<div class="title">
|
||||
<span class="icon"><svg-icon name="card" size="20" /></span>
|
||||
<span class="text">{{ $t('Pay.PayWithStripe') }}</span>
|
||||
</div>
|
||||
<template v-if="!query.paymentId">
|
||||
<div class="tip">{{ $t('Pay.PayTip1') }}</div>
|
||||
<div class="buttons">
|
||||
<button custom="black" @click="onPayWith">
|
||||
<span class="text">{{ $t('Pay.PayWith') }}</span>
|
||||
<span class="icon pay"><svg-icon name="pay-stripe" /></span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<span class="text" @click="onBack">{{ $t('Pay.Cancel') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 已支付,等待确认 -->
|
||||
<template v-if="query.paymentId">
|
||||
<div class="tip">{{ $t('Pay.PayTip2') }}</div>
|
||||
<div class="buttons">
|
||||
<button custom="black" @click="getOrderStatus">
|
||||
{{ $t('Pay.IHaveCompletedPayment') }}
|
||||
</button>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<span class="text" @click="onBack">{{ $t('Pay.Back') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
<!-- 已支付 -->
|
||||
<template v-if="paymentStatus === ORDER_STATUS.SUCCESS">
|
||||
<div class="success">
|
||||
<img src="@/assets/images/pay/success.png" alt="" />
|
||||
<div class="title">{{ $t('Pay.PurchaseSuccessful') }}</div>
|
||||
<div class="tip">{{ $t('Pay.PurchaseSuccessfulTip') }}</div>
|
||||
</div>
|
||||
|
||||
<div class="progres" v-if="downloadInfo.status !== DOWNLOAD_STATUS.null">
|
||||
<el-progress
|
||||
:text-inside="true"
|
||||
:percentage="downloadInfo.progress * 100"
|
||||
:status="progressStatus"
|
||||
:duration="0.3"
|
||||
>
|
||||
<span class="text"
|
||||
>{{ FormatBytes(downloadInfo.loaded, { unitBig: true }) }} /
|
||||
{{ FormatBytes(downloadInfo.total, { unitBig: true }) }}</span
|
||||
>
|
||||
</el-progress>
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<button custom="black" @click="handleDownloadAllAssets">
|
||||
{{ $t('Pay.DownloadAllAssets') }}
|
||||
</button>
|
||||
<!-- <button custom="black-box">
|
||||
<span class="icon"><svg-icon name="order-file" size="18" /></span>
|
||||
<span class="text">{{ $t('Pay.ExportInvoice') }}</span>
|
||||
</button> -->
|
||||
</div>
|
||||
<div class="buttons">
|
||||
<span class="text" @click="onBack">{{ $t('Pay.ContinueShopping') }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { FormatBytes } from '@/utils/tools'
|
||||
import { fetchDownloadItemsByGet } from '@/api/user'
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { CreateOrder, ORDER_STATUS, GetOrderStatus } from '@/api/shoppingCart'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import { UrlList, openView } from '@/utils/UrlList'
|
||||
window['openTermsView'] = () => {
|
||||
const e = window.event as Event
|
||||
e.preventDefault()
|
||||
openView(UrlList.terms)
|
||||
}
|
||||
window['openPrivacyView'] = () => {
|
||||
const e = window.event as Event
|
||||
e.preventDefault()
|
||||
openView(UrlList.privacy)
|
||||
}
|
||||
const onBack = () => router.back()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const query = computed(() => route.query)
|
||||
const props = defineProps({
|
||||
ids: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
}
|
||||
})
|
||||
onMounted(() => {
|
||||
if (query.value.paymentLink && query.value.paymentId) {
|
||||
createOrderCall({
|
||||
paymentLink: query.value.paymentLink,
|
||||
paymentId: query.value.paymentId
|
||||
})
|
||||
}
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
clearInterval(viewTime.value)
|
||||
})
|
||||
const agreement = ref(false)
|
||||
const paymentStatus = ref(ORDER_STATUS.PENDING)
|
||||
const onPayWith = () => {
|
||||
if (!agreement.value) {
|
||||
return ElMessage.warning('Please agree to the Terms & Conditions and Privacy Policy.')
|
||||
}
|
||||
const ids = [...props.ids]
|
||||
if (ids.length === 0) return
|
||||
CreateOrder(ids, true).then(createOrderCall)
|
||||
}
|
||||
const createOrderCall = (res: any) => {
|
||||
const url = res.paymentLink
|
||||
const paymentId = res.paymentId
|
||||
openPayView(url)
|
||||
router.replace({
|
||||
query: {
|
||||
list: query.value.list,
|
||||
paymentId
|
||||
}
|
||||
})
|
||||
}
|
||||
const getOrderStatus = () => {
|
||||
const paymentId = query.value.paymentId as string
|
||||
GetOrderStatus(paymentId).then((res: any) => {
|
||||
const status = res === null ? ORDER_STATUS.PENDING : (res as number)
|
||||
paymentStatus.value = status
|
||||
if (status !== ORDER_STATUS.SUCCESS) {
|
||||
ElMessage.warning('订单未支付')
|
||||
}
|
||||
})
|
||||
}
|
||||
const viewTime = ref(null)
|
||||
const openPayView = (url, width = 600, height = 800) => {
|
||||
const left = (screen.width - width) / 2
|
||||
const top = (screen.height - height) / 2
|
||||
const view = window.open(
|
||||
url,
|
||||
'_blank',
|
||||
'width=' + width + ', height=' + height + ', left=' + left + ', top=' + top
|
||||
)
|
||||
|
||||
clearInterval(viewTime.value)
|
||||
viewTime.value = setInterval(() => {
|
||||
if (view.closed) {
|
||||
clearInterval(viewTime.value)
|
||||
getOrderStatus()
|
||||
}
|
||||
}, 500)
|
||||
}
|
||||
const DOWNLOAD_STATUS = {
|
||||
null: 0, // 未开始
|
||||
dowloading: 1, // 下载中
|
||||
success: 2, // 下载成功
|
||||
failed: 3 // 下载失败
|
||||
}
|
||||
const downloadInfo = ref({
|
||||
progress: 0,
|
||||
status: DOWNLOAD_STATUS.null,
|
||||
total: 0,
|
||||
loaded: 0
|
||||
})
|
||||
const progressStatus = computed(() => {
|
||||
if (downloadInfo.value.status === DOWNLOAD_STATUS.dowloading) {
|
||||
return 'warning'
|
||||
} else if (downloadInfo.value.status === DOWNLOAD_STATUS.success) {
|
||||
return 'success'
|
||||
} else if (downloadInfo.value.status === DOWNLOAD_STATUS.failed) {
|
||||
return 'exception'
|
||||
} else {
|
||||
return 'warning'
|
||||
}
|
||||
})
|
||||
const handleDownloadAllAssets = () => {
|
||||
if (downloadInfo.value.status === DOWNLOAD_STATUS.dowloading) {
|
||||
return
|
||||
}
|
||||
const ids = props.ids as string[]
|
||||
downloadInfo.value.status = DOWNLOAD_STATUS.dowloading
|
||||
downloadInfo.value.progress = 0
|
||||
fetchDownloadItemsByGet({ ids }, (event) => {
|
||||
downloadInfo.value.progress = event.progress
|
||||
downloadInfo.value.loaded = event.loaded
|
||||
downloadInfo.value.total = event.total
|
||||
})
|
||||
.then((res) => {
|
||||
console.log(res)
|
||||
const disposition = res.headers['content-disposition']
|
||||
const fileName = disposition?.split('filename=')[1]?.replace(/"/g, '') || 'download.zip'
|
||||
const blob = res.data
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
|
||||
const timestamp = new Date().getTime()
|
||||
link.download = fileName || `wardrobe_download_${timestamp}.zip`
|
||||
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
|
||||
document.body.removeChild(link)
|
||||
window.URL.revokeObjectURL(url)
|
||||
})
|
||||
.catch((error) => {
|
||||
downloadInfo.value.status = DOWNLOAD_STATUS.failed
|
||||
})
|
||||
.finally(() => {
|
||||
downloadInfo.value.progress = 1
|
||||
downloadInfo.value.status = DOWNLOAD_STATUS.success
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.payment {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
> .header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
> .text {
|
||||
font-family: KaiseiOpti-Bold;
|
||||
font-size: 4rem;
|
||||
}
|
||||
}
|
||||
> .paylist {
|
||||
margin: 4rem 0 0 4rem;
|
||||
padding: 2.7rem 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-bottom: 0.1rem solid #c4c4c4;
|
||||
> .item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
> img {
|
||||
width: auto;
|
||||
height: 3rem;
|
||||
margin-bottom: 1.6rem;
|
||||
}
|
||||
> span {
|
||||
font-family: KaiseiOpti-Regular;
|
||||
font-size: 1.2rem;
|
||||
color: #585858;
|
||||
}
|
||||
}
|
||||
}
|
||||
> .agreement {
|
||||
margin-top: 2.4rem;
|
||||
margin-left: 2rem;
|
||||
margin-bottom: 10rem;
|
||||
display: flex;
|
||||
> .el-checkbox {
|
||||
--el-checkbox-font-size: 1.4rem;
|
||||
--el-checkbox-input-width: 1.4rem;
|
||||
--el-checkbox-input-height: 1.4rem;
|
||||
--el-checkbox-checked-bg-color: #000;
|
||||
--el-checkbox-checked-input-border-color: #000;
|
||||
--el-checkbox-input-border: 0.1rem solid #c4c4c4;
|
||||
--el-checkbox-bg-color: #fff;
|
||||
--el-checkbox-border-radius: 0;
|
||||
}
|
||||
|
||||
.text {
|
||||
color: #666;
|
||||
word-break: break-word;
|
||||
white-space: pre-wrap;
|
||||
> * {
|
||||
font-family: KaiseiOpti-Bold;
|
||||
text-decoration: underline;
|
||||
color: #232323;
|
||||
}
|
||||
}
|
||||
}
|
||||
> .title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
> .tip {
|
||||
margin: 60px auto;
|
||||
max-width: 59rem;
|
||||
background: #f6f6f6;
|
||||
border-left: 0.2rem solid #979797;
|
||||
padding: 1.5rem 2rem;
|
||||
font-size: 1.4rem;
|
||||
line-height: 2.5rem;
|
||||
color: #585858;
|
||||
}
|
||||
> .buttons {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 1.8rem;
|
||||
gap: 1.2rem;
|
||||
> button {
|
||||
min-width: 28rem;
|
||||
height: 5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.8rem;
|
||||
text-transform: uppercase;
|
||||
> .icon.pay {
|
||||
width: 5rem;
|
||||
--svg-icon-width: auto;
|
||||
--svg-icon-height: 2rem;
|
||||
}
|
||||
}
|
||||
> span {
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
font-size: 1.4rem;
|
||||
color: #979797;
|
||||
}
|
||||
}
|
||||
> .success {
|
||||
margin: 12rem auto;
|
||||
max-width: 39rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
> img {
|
||||
width: 19.8rem;
|
||||
height: auto;
|
||||
margin-bottom: 2.4rem;
|
||||
}
|
||||
> .title {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
> .tip {
|
||||
font-family: KaiseiOpti-Regular;
|
||||
font-size: 1.4rem;
|
||||
color: #666;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
> .progres {
|
||||
margin: 2rem auto;
|
||||
max-width: 50rem;
|
||||
width: 100%;
|
||||
.text {
|
||||
font-size: 1.4rem;
|
||||
line-height: 2.2rem;
|
||||
color: #000;
|
||||
}
|
||||
&:deep(.el-progress) {
|
||||
.el-progress-bar__outer {
|
||||
height: 2.2rem !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
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
@@ -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: '42rem', height: '42rem' }"
|
||||
: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,361 @@
|
||||
<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'
|
||||
import { useUserInfoStore } from '@/stores'
|
||||
|
||||
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
|
||||
(event: 'avatar-updated', url: string): 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
|
||||
}>()
|
||||
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 })
|
||||
useUserInfoStore().setAvatarUrl(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 = (file: File) => {
|
||||
closeAvatarCropDialog()
|
||||
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>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
<div class="region-row">
|
||||
<div class="security-label">{{ t('Settings.region.region') }}</div>
|
||||
<div v-show="!isEditing" class="security-static field-box">
|
||||
{{ displayRegionLabel }}
|
||||
{{ t(`area.${displayRegionLabel}`) }}
|
||||
</div>
|
||||
<div v-show="isEditing" class="outlined-field select-field">
|
||||
<el-select
|
||||
@@ -38,7 +38,7 @@
|
||||
<el-option
|
||||
v-for="item in regionOptions"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:label="t(`area.${item.key}`)"
|
||||
:value="item.value"
|
||||
/>
|
||||
</el-select>
|
||||
@@ -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<{
|
||||
|
||||
@@ -1,242 +1,296 @@
|
||||
<template>
|
||||
<SettingsSection
|
||||
:title="t('Settings.security.title')"
|
||||
:description="t('Settings.security.description')"
|
||||
content-class="security-container"
|
||||
>
|
||||
<div class="inner-divider" />
|
||||
<div class="security-row">
|
||||
<div class="security-inline-row">
|
||||
<div class="security-label inline">{{ t('Settings.security.email') }}</div>
|
||||
<div class="security-static">{{ email }}</div>
|
||||
<button v-show="isEditing" type="button" class="small-btn" @click="emit('reset-email')">
|
||||
{{ t('Settings.buttons.cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<SettingsSection
|
||||
:title="t('Settings.security.title')"
|
||||
:description="t('Settings.security.description')"
|
||||
content-class="security-container"
|
||||
>
|
||||
<div class="inner-divider" />
|
||||
<div class="security-row">
|
||||
<div class="security-inline-row">
|
||||
<div class="security-label inline">{{ t('Settings.security.email') }}</div>
|
||||
<div class="security-static">{{ email }}</div>
|
||||
<button
|
||||
v-show="isEditing && !isEditingEmail"
|
||||
type="button"
|
||||
class="small-btn"
|
||||
@click="emit('edit-email')"
|
||||
>
|
||||
{{ t('Settings.buttons.edit') }}
|
||||
</button>
|
||||
<button
|
||||
v-show="isEditing && isEditingEmail"
|
||||
type="button"
|
||||
class="small-btn"
|
||||
@click="emit('reset-email')"
|
||||
>
|
||||
{{ t('Settings.buttons.cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="isEditing" class="security-row">
|
||||
<div class="security-label">{{ t('Settings.security.newEmail') }}</div>
|
||||
<div class="outlined-field verify-field">
|
||||
<el-input
|
||||
:model-value="newEmail"
|
||||
:placeholder="t('Settings.security.newEmailPlaceholder')"
|
||||
@update:model-value="emit('update:newEmail', String($event))"
|
||||
/>
|
||||
<button
|
||||
<div v-if="isEditing && isEditingEmail" class="security-row">
|
||||
<div class="security-label">{{ t('Settings.security.newEmail') }}</div>
|
||||
<div class="outlined-field verify-field">
|
||||
<el-input
|
||||
:model-value="newEmail"
|
||||
:placeholder="t('Settings.security.newEmailPlaceholder')"
|
||||
@update:model-value="emit('update:newEmail', String($event))"
|
||||
/>
|
||||
<!-- <button
|
||||
type="button"
|
||||
class="verify-btn"
|
||||
:class="{ verified: isEmailVerified }"
|
||||
@click="emit('verify-email')"
|
||||
>
|
||||
{{ isEmailVerified ? t('Settings.security.verified') : t('Settings.security.verify') }}
|
||||
</button>
|
||||
</div>
|
||||
<div v-if="isEmailVerified" class="security-tip verified-tip">
|
||||
</button> -->
|
||||
</div>
|
||||
<!-- <div v-if="isEmailVerified" class="security-tip verified-tip">
|
||||
{{ t('Settings.security.verifiedTip') }}
|
||||
</div>
|
||||
</div>
|
||||
</div> -->
|
||||
</div>
|
||||
|
||||
<div class="inner-divider" />
|
||||
<div class="inner-divider" />
|
||||
|
||||
<div class="security-row">
|
||||
<div class="security-inline-row">
|
||||
<div class="security-label inline">{{ t('Settings.security.password') }}</div>
|
||||
<div class="security-static password-mask">.........</div>
|
||||
<button v-show="isEditing" type="button" class="small-btn" @click="emit('reset-password')">
|
||||
{{ t('Settings.buttons.cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="security-row">
|
||||
<div class="security-inline-row">
|
||||
<div class="security-label inline">{{ t('Settings.security.password') }}</div>
|
||||
<div class="security-static password-mask">.........</div>
|
||||
<button
|
||||
v-show="isEditing && !isEditingPassword"
|
||||
type="button"
|
||||
class="small-btn"
|
||||
@click="emit('edit-password')"
|
||||
>
|
||||
{{ t('Settings.buttons.edit') }}
|
||||
</button>
|
||||
<button
|
||||
v-show="isEditing && isEditingPassword"
|
||||
type="button"
|
||||
class="small-btn"
|
||||
@click="emit('reset-password')"
|
||||
>
|
||||
{{ t('Settings.buttons.cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-show="isEditing" class="security-row">
|
||||
<div class="security-label">{{ t('Settings.security.newPassword') }}</div>
|
||||
<div class="outlined-field">
|
||||
<el-input
|
||||
:model-value="newPassword"
|
||||
type="password"
|
||||
show-password
|
||||
:placeholder="t('Settings.security.newPasswordPlaceholder')"
|
||||
@update:model-value="emit('update:newPassword', String($event))"
|
||||
/>
|
||||
</div>
|
||||
<div class="security-tip">{{ t('Settings.security.passwordTip') }}</div>
|
||||
</div>
|
||||
<div v-if="isEditing && isEditingPassword" class="security-row">
|
||||
<div class="security-label">{{ t('Settings.security.newPassword') }}</div>
|
||||
<div class="outlined-field">
|
||||
<PasswordTip :value="newPassword" v-show="showNewPWDTip" />
|
||||
<el-input
|
||||
:model-value="newPassword"
|
||||
type="password"
|
||||
show-password
|
||||
:placeholder="t('Settings.security.newPasswordPlaceholder')"
|
||||
@blur="showNewPWDTip = false"
|
||||
@focus="showNewPWDTip = true"
|
||||
@update:model-value="emit('update:newPassword', String($event))"
|
||||
/>
|
||||
</div>
|
||||
<div class="security-tip">{{ t('Settings.security.passwordTip') }}</div>
|
||||
</div>
|
||||
|
||||
<div v-show="isEditing" class="security-row">
|
||||
<div class="security-label">{{ t('Settings.security.currentPassword') }}</div>
|
||||
<div class="outlined-field">
|
||||
<el-input
|
||||
:model-value="currentPassword"
|
||||
type="password"
|
||||
show-password
|
||||
:placeholder="t('Settings.security.currentPasswordPlaceholder')"
|
||||
@update:model-value="emit('update:currentPassword', String($event))"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inner-divider" />
|
||||
</SettingsSection>
|
||||
<div v-if="isEditing && isEditingPassword" class="security-row">
|
||||
<div class="security-label">{{ t('Settings.security.currentPassword') }}</div>
|
||||
<div class="outlined-field">
|
||||
<PasswordTip :value="newPassword" v-show="showOldPWDTip" />
|
||||
<el-input
|
||||
:model-value="currentPassword"
|
||||
type="password"
|
||||
show-password
|
||||
:placeholder="t('Settings.security.currentPasswordPlaceholder')"
|
||||
@update:model-value="emit('update:currentPassword', String($event))"
|
||||
@blur="showOldPWDTip = false"
|
||||
@focus="showOldPWDTip = true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inner-divider" />
|
||||
</SettingsSection>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import SettingsSection from './SettingsSection.vue'
|
||||
import { ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import SettingsSection from './SettingsSection.vue'
|
||||
import PasswordTip from '@/views/login/password-tip.vue'
|
||||
|
||||
defineProps<{
|
||||
email: string
|
||||
newEmail: string
|
||||
newPassword: string
|
||||
currentPassword: string
|
||||
isEditing: boolean
|
||||
isEmailVerified: boolean
|
||||
}>()
|
||||
defineProps<{
|
||||
email: string
|
||||
newEmail: string
|
||||
newPassword: string
|
||||
currentPassword: string
|
||||
isEditing: boolean
|
||||
isEditingEmail: boolean
|
||||
isEditingPassword: boolean
|
||||
isEmailVerified: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:newEmail', value: string): void
|
||||
(event: 'update:newPassword', value: string): void
|
||||
(event: 'update:currentPassword', value: string): void
|
||||
(event: 'reset-email'): void
|
||||
(event: 'reset-password'): void
|
||||
(event: 'verify-email'): void
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:newEmail', value: string): void
|
||||
(event: 'update:newPassword', value: string): void
|
||||
(event: 'update:currentPassword', value: string): void
|
||||
(event: 'edit-email'): void
|
||||
(event: 'edit-password'): void
|
||||
(event: 'reset-email'): void
|
||||
(event: 'reset-password'): void
|
||||
(event: 'verify-email'): void
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
const { t } = useI18n()
|
||||
|
||||
const showNewPWDTip = ref(false)
|
||||
const showOldPWDTip = ref(false)
|
||||
</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;
|
||||
}
|
||||
|
||||
.security-row + .security-row {
|
||||
margin-top: 2.8rem;
|
||||
}
|
||||
.security-row + .security-row {
|
||||
margin-top: 2.8rem;
|
||||
}
|
||||
|
||||
.security-label {
|
||||
margin: 0 0 0.8rem;
|
||||
font-family: 'KaiseiOpti-Medium';
|
||||
font-size: 1.4rem;
|
||||
line-height: 2.4rem;
|
||||
letter-spacing: 0.04em;
|
||||
color: #585858;
|
||||
.security-label {
|
||||
margin: 0 0 0.8rem;
|
||||
font-family: 'KaiseiOpti-Medium';
|
||||
font-size: 1.4rem;
|
||||
line-height: 2.4rem;
|
||||
letter-spacing: 0.04em;
|
||||
color: #585858;
|
||||
|
||||
&.inline {
|
||||
width: 10.8rem;
|
||||
margin-bottom: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
&.inline {
|
||||
width: 10.8rem;
|
||||
margin-bottom: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.security-static {
|
||||
.field-text();
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-height: 2.4rem;
|
||||
padding: 0.1rem 0 0;
|
||||
}
|
||||
.security-static {
|
||||
.field-text();
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
min-height: 2.4rem;
|
||||
padding: 0.1rem 0 0;
|
||||
}
|
||||
|
||||
.security-inline-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2.8rem;
|
||||
min-height: 3.2rem;
|
||||
}
|
||||
.security-inline-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2.8rem;
|
||||
min-height: 3.2rem;
|
||||
}
|
||||
|
||||
.security-tip {
|
||||
margin-top: 0.6rem;
|
||||
font-family: 'KaiseiOpti-Regular';
|
||||
font-size: 1.2rem;
|
||||
line-height: 1.6rem;
|
||||
color: #9f9f9f;
|
||||
}
|
||||
.security-tip {
|
||||
margin-top: 0.6rem;
|
||||
font-family: 'KaiseiOpti-Regular';
|
||||
font-size: 1.2rem;
|
||||
line-height: 1.6rem;
|
||||
color: #9f9f9f;
|
||||
}
|
||||
|
||||
.outlined-field {
|
||||
.field-frame();
|
||||
.outlined-field {
|
||||
.field-frame();
|
||||
position: relative;
|
||||
:deep(.password-tip) {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
:deep(.el-input) {
|
||||
width: 100%;
|
||||
min-height: 4rem;
|
||||
}
|
||||
|
||||
:deep(.el-input) {
|
||||
width: 100%;
|
||||
min-height: 4rem;
|
||||
}
|
||||
:deep(.el-input__wrapper) {
|
||||
.control-wrapper();
|
||||
min-height: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-input__wrapper) {
|
||||
.control-wrapper();
|
||||
min-height: 4rem;
|
||||
}
|
||||
}
|
||||
.verify-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 0.8rem;
|
||||
|
||||
.verify-field {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-top: 0.8rem;
|
||||
:deep(.el-input) {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-input) {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
.verify-btn {
|
||||
border: none;
|
||||
min-width: 11rem;
|
||||
height: 2.8rem;
|
||||
line-height: 2.8rem;
|
||||
border-left: 0.1rem solid #979797;
|
||||
background: #ffffff;
|
||||
font-family: 'KaiseiOpti-Medium';
|
||||
font-size: 1.4rem;
|
||||
color: #232323;
|
||||
cursor: pointer;
|
||||
padding: 0 2rem;
|
||||
|
||||
.verify-btn {
|
||||
border: none;
|
||||
min-width: 11rem;
|
||||
height: 2.8rem;
|
||||
line-height: 2.8rem;
|
||||
border-left: 0.1rem solid #979797;
|
||||
background: #ffffff;
|
||||
font-family: 'KaiseiOpti-Medium';
|
||||
font-size: 1.4rem;
|
||||
color: #232323;
|
||||
cursor: pointer;
|
||||
padding: 0 2rem;
|
||||
&.verified {
|
||||
color: #ffffff;
|
||||
background: #232323;
|
||||
border-left-color: #232323;
|
||||
}
|
||||
}
|
||||
|
||||
&.verified {
|
||||
color: #ffffff;
|
||||
.password-mask {
|
||||
font-family: 'KaiseiOpti-Bold';
|
||||
letter-spacing: 0.08rem;
|
||||
}
|
||||
|
||||
.inner-divider {
|
||||
height: 1px;
|
||||
margin: 2rem 0;
|
||||
background-color: #c4c4c4;
|
||||
}
|
||||
|
||||
.small-btn {
|
||||
width: 10rem;
|
||||
height: 3.2rem;
|
||||
align-self: flex-start;
|
||||
border: 0.1rem solid #c4c4c4;
|
||||
background: #f6f6f6;
|
||||
font-family: 'KaiseiOpti-Bold';
|
||||
font-size: 1.2rem;
|
||||
line-height: 2.6rem;
|
||||
letter-spacing: -0.03em;
|
||||
color: #232323;
|
||||
cursor: pointer;
|
||||
/*
|
||||
&.edit-btn {
|
||||
border-color: #232323;
|
||||
background: #232323;
|
||||
border-left-color: #232323;
|
||||
}
|
||||
}
|
||||
color: #ffffff;
|
||||
} */
|
||||
}
|
||||
|
||||
.password-mask {
|
||||
font-family: 'KaiseiOpti-Bold';
|
||||
letter-spacing: 0.08rem;
|
||||
}
|
||||
|
||||
.inner-divider {
|
||||
height: 1px;
|
||||
margin: 2rem 0;
|
||||
background-color: #c4c4c4;
|
||||
}
|
||||
|
||||
.small-btn {
|
||||
width: 10rem;
|
||||
height: 3.2rem;
|
||||
align-self: flex-start;
|
||||
border: 0.1rem solid #c4c4c4;
|
||||
background: #f6f6f6;
|
||||
font-family: 'KaiseiOpti-Bold';
|
||||
font-size: 1.2rem;
|
||||
line-height: 2.6rem;
|
||||
letter-spacing: -0.03em;
|
||||
color: #232323;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.verified-tip {
|
||||
color: #6f7f68;
|
||||
}
|
||||
.verified-tip {
|
||||
color: #6f7f68;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
<template>
|
||||
<div class="action-container">
|
||||
<template v-if="isEditing">
|
||||
<button type="button" class="primary-btn" :disabled="saving" @click="emit('save')">
|
||||
<button v-if="needsEmailVerification" type="button" class="primary-btn" :disabled="saving" @click="emit('verify')">
|
||||
{{ t('Settings.buttons.verifyEmail') }}
|
||||
</button>
|
||||
<button v-else type="button" class="primary-btn" :disabled="saving" @click="emit('save')">
|
||||
{{ saving ? t('Settings.buttons.saving') : t('Settings.buttons.saveChange') }}
|
||||
</button>
|
||||
<button type="button" class="secondary-btn" :disabled="saving" @click="emit('discard')">
|
||||
@@ -22,11 +25,13 @@ import { useI18n } from 'vue-i18n'
|
||||
defineProps<{
|
||||
isEditing: boolean
|
||||
saving: boolean
|
||||
needsEmailVerification: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'edit'): void
|
||||
(event: 'save'): void
|
||||
(event: 'verify'): void
|
||||
(event: 'discard'): void
|
||||
}>()
|
||||
|
||||
|
||||
@@ -1,159 +1,177 @@
|
||||
<template>
|
||||
<div class="setting-wrapper mini-scrollbar">
|
||||
<div class="banner">
|
||||
<div class="title">{{ t('Settings.title') }}</div>
|
||||
<div class="slogan">{{ t('Settings.slogan') }}</div>
|
||||
</div>
|
||||
<div class="setting-wrapper mini-scrollbar">
|
||||
<div class="banner">
|
||||
<div class="title">{{ t('Settings.title') }}</div>
|
||||
<div class="slogan">{{ t('Settings.slogan') }}</div>
|
||||
</div>
|
||||
|
||||
<div class="setting-content">
|
||||
<ProfileSection
|
||||
v-model:first-name="draftData.firstName"
|
||||
v-model:last-name="draftData.lastName"
|
||||
v-model:username="draftData.username"
|
||||
v-model:role-model="roleModel"
|
||||
:display-data="displayData"
|
||||
:full-name="fullName"
|
||||
:is-editing="isEditing"
|
||||
:role-options="roleList"
|
||||
/>
|
||||
<div class="setting-content">
|
||||
<ProfileSection
|
||||
v-model:first-name="draftData.firstName"
|
||||
v-model:last-name="draftData.lastName"
|
||||
v-model:username="draftData.username"
|
||||
v-model:role-model="roleModel"
|
||||
:display-data="displayData"
|
||||
:full-name="fullName"
|
||||
:is-editing="isEditing"
|
||||
:role-options="roleList"
|
||||
@avatar-updated="loadUserProfile"
|
||||
/>
|
||||
|
||||
<div class="gap" />
|
||||
<div class="gap" />
|
||||
|
||||
<SecuritySection
|
||||
v-model:new-email="securityDraft.newEmail"
|
||||
v-model:new-password="securityDraft.newPassword"
|
||||
v-model:current-password="securityDraft.currentPassword"
|
||||
:email="displayData.email"
|
||||
:is-editing="isEditing"
|
||||
:is-email-verified="isEmailVerified"
|
||||
@reset-email="resetSecurityEmail"
|
||||
@reset-password="resetSecurityPassword"
|
||||
@verify-email="handleVerifyEmail"
|
||||
/>
|
||||
<SecuritySection
|
||||
v-model:new-email="securityDraft.newEmail"
|
||||
v-model:new-password="securityDraft.newPassword"
|
||||
v-model:current-password="securityDraft.currentPassword"
|
||||
:email="displayData.email"
|
||||
:is-editing="isEditing"
|
||||
:is-editing-email="isEditingEmail"
|
||||
:is-editing-password="isEditingPassword"
|
||||
:is-email-verified="isEmailVerified"
|
||||
@edit-email="handleEditEmail"
|
||||
@edit-password="handleEditPassword"
|
||||
@reset-email="resetSecurityEmail"
|
||||
@reset-password="resetSecurityPassword"
|
||||
@verify-email="handleVerifyEmail"
|
||||
/>
|
||||
|
||||
<div class="gap" />
|
||||
<div class="gap" />
|
||||
|
||||
<RegionSection
|
||||
v-model:language="draftData.language"
|
||||
v-model:region="draftData.region"
|
||||
:display-language-label="displayLanguageLabel"
|
||||
:display-region-label="displayRegionLabel"
|
||||
:is-editing="isEditing"
|
||||
:language-options="languageList"
|
||||
:region-options="regionList"
|
||||
/>
|
||||
<RegionSection
|
||||
v-model:language="draftData.language"
|
||||
v-model:region="draftData.region"
|
||||
:display-language-label="displayLanguageLabel"
|
||||
:display-region-label="displayRegionLabel"
|
||||
:is-editing="isEditing"
|
||||
:language-options="languageList"
|
||||
:region-options="regionList"
|
||||
/>
|
||||
|
||||
<div class="gap bottom-gap" />
|
||||
<div class="gap bottom-gap" />
|
||||
|
||||
<SettingsActions
|
||||
:is-editing="isEditing"
|
||||
:saving="saving"
|
||||
@edit="handleEdit"
|
||||
@save="handleSave"
|
||||
@discard="handleDiscard"
|
||||
/>
|
||||
</div>
|
||||
<SettingsActions
|
||||
:is-editing="isEditing"
|
||||
:saving="saving"
|
||||
:needs-email-verification="needsEmailVerification"
|
||||
@edit="handleEdit"
|
||||
@save="handleSave"
|
||||
@verify="handleVerifyEmail"
|
||||
@discard="handleDiscard"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Footer />
|
||||
<Footer />
|
||||
|
||||
<EmailVerificationDialog
|
||||
:visible="isVerificationDialogVisible"
|
||||
:email="verificationTargetEmail"
|
||||
:saving="saving"
|
||||
@close="closeVerificationDialog"
|
||||
@resend="handleSendVerifyCode"
|
||||
@submit="handleVerificationSubmit"
|
||||
/>
|
||||
</div>
|
||||
<EmailVerificationDialog
|
||||
:visible="isVerificationDialogVisible"
|
||||
:email="displayData.email"
|
||||
:saving="saving"
|
||||
@close="closeVerificationDialog"
|
||||
@resend="handleSendVerifyCode"
|
||||
@submit="handleVerificationSubmit"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import EmailVerificationDialog from './components/EmailVerificationDialog.vue'
|
||||
import ProfileSection from './components/ProfileSection.vue'
|
||||
import RegionSection from './components/RegionSection.vue'
|
||||
import SecuritySection from './components/SecuritySection.vue'
|
||||
import SettingsActions from './components/SettingsActions.vue'
|
||||
import { useSettingsForm } from './useSettingsForm'
|
||||
import { ref, onMounted, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import EmailVerificationDialog from './components/EmailVerificationDialog.vue'
|
||||
import ProfileSection from './components/ProfileSection.vue'
|
||||
import RegionSection from './components/RegionSection.vue'
|
||||
import SecuritySection from './components/SecuritySection.vue'
|
||||
import SettingsActions from './components/SettingsActions.vue'
|
||||
import { useSettingsForm } from './useSettingsForm'
|
||||
|
||||
const { t, locale } = useI18n({ useScope: 'global' })
|
||||
const { t, locale } = useI18n({ useScope: 'global' })
|
||||
|
||||
const {
|
||||
draftData,
|
||||
securityDraft,
|
||||
isEditing,
|
||||
saving,
|
||||
isVerificationDialogVisible,
|
||||
verificationTargetEmail,
|
||||
roleList,
|
||||
languageList,
|
||||
regionList,
|
||||
displayData,
|
||||
isEmailVerified,
|
||||
displayLanguageLabel,
|
||||
displayRegionLabel,
|
||||
fullName,
|
||||
roleModel,
|
||||
handleEdit,
|
||||
handleDiscard,
|
||||
handleSave,
|
||||
resetSecurityEmail,
|
||||
resetSecurityPassword,
|
||||
handleVerifyEmail,
|
||||
handleSendVerifyCode,
|
||||
handleVerificationSubmit,
|
||||
closeVerificationDialog
|
||||
} = useSettingsForm({ t, locale })
|
||||
const {
|
||||
draftData,
|
||||
securityDraft,
|
||||
isEditing,
|
||||
saving,
|
||||
isVerificationDialogVisible,
|
||||
verificationTargetEmail,
|
||||
roleList,
|
||||
languageList,
|
||||
regionList,
|
||||
displayData,
|
||||
isEmailVerified,
|
||||
displayLanguageLabel,
|
||||
displayRegionLabel,
|
||||
fullName,
|
||||
roleModel,
|
||||
needsEmailVerification,
|
||||
isEditingEmail,
|
||||
isEditingPassword,
|
||||
handleEdit,
|
||||
handleDiscard,
|
||||
handleSave,
|
||||
handleEditEmail,
|
||||
handleEditPassword,
|
||||
resetSecurityEmail,
|
||||
resetSecurityPassword,
|
||||
handleVerifyEmail,
|
||||
handleSendVerifyCode,
|
||||
handleVerificationSubmit,
|
||||
closeVerificationDialog,
|
||||
loadUserProfile
|
||||
} = useSettingsForm({ t, locale })
|
||||
|
||||
onMounted(() => {
|
||||
loadUserProfile()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.setting-wrapper {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
background: #ffffff;
|
||||
}
|
||||
.setting-wrapper {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.banner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 14.8rem;
|
||||
row-gap: 1.2rem;
|
||||
background: linear-gradient(rgba(255, 255, 255, 0.91), rgba(255, 255, 255, 0.91)),
|
||||
linear-gradient(90deg, #f2eee8 0%, #fbfaf8 40%, #f1ede7 100%);
|
||||
}
|
||||
.banner {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 14.8rem;
|
||||
row-gap: 1.2rem;
|
||||
background: url('@/assets/images/wardrobe/settings_bg.jpg') no-repeat;
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-family: 'KaiseiOpti-Bold';
|
||||
font-size: 4rem;
|
||||
line-height: 3.6rem;
|
||||
color: #232323;
|
||||
}
|
||||
.title {
|
||||
font-family: 'KaiseiOpti-Bold';
|
||||
font-size: 4rem;
|
||||
line-height: 3.6rem;
|
||||
color: #232323;
|
||||
}
|
||||
|
||||
.slogan {
|
||||
font-family: 'KaiseiOpti-Regular';
|
||||
font-size: 1.6rem;
|
||||
line-height: 2.4rem;
|
||||
color: #585858;
|
||||
}
|
||||
.slogan {
|
||||
font-family: 'KaiseiOpti-Regular';
|
||||
font-size: 1.6rem;
|
||||
line-height: 2.4rem;
|
||||
color: #585858;
|
||||
}
|
||||
|
||||
.setting-content {
|
||||
padding: 4rem 18rem 7rem;
|
||||
}
|
||||
.setting-content {
|
||||
padding: 4rem 18rem 7rem;
|
||||
}
|
||||
|
||||
.gap {
|
||||
height: 0.05rem;
|
||||
margin-top: 6rem;
|
||||
margin-bottom: 4rem;
|
||||
background-color: #c4c4c4;
|
||||
.gap {
|
||||
height: 0.05rem;
|
||||
margin-top: 6rem;
|
||||
margin-bottom: 4rem;
|
||||
background-color: #c4c4c4;
|
||||
|
||||
&.bottom-gap {
|
||||
margin-top: 4rem;
|
||||
}
|
||||
}
|
||||
&.bottom-gap {
|
||||
margin-top: 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.el-select-dropdown__item) {
|
||||
padding: 0 2rem !important;
|
||||
}
|
||||
:deep(.el-select-dropdown__item) {
|
||||
padding: 0 2rem !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -12,21 +12,22 @@ export const roleValues = [
|
||||
'other'
|
||||
] as const
|
||||
|
||||
export const languageValues = ['english', 'chinese'] as const
|
||||
export const regionValues = ['hongKongSar', 'mainlandChina', 'singapore', 'unitedKingdom'] as const
|
||||
export const languageValues = ['ENGLISH', 'CHINESE_SIMPLIFIED'] as const
|
||||
|
||||
|
||||
export type RoleValue = (typeof roleValues)[number]
|
||||
export type LanguageValue = (typeof languageValues)[number]
|
||||
export type RegionValue = (typeof regionValues)[number]
|
||||
export type RegionValue = string
|
||||
|
||||
export interface SettingsData {
|
||||
firstName: string
|
||||
lastName: string
|
||||
email: string
|
||||
username: string
|
||||
role: RoleValue[]
|
||||
language: LanguageValue
|
||||
region: RegionValue
|
||||
roles: RoleValue[]
|
||||
language: LanguageValue | ''
|
||||
region: string | ''
|
||||
avatarUrl?: string
|
||||
}
|
||||
|
||||
export interface SecurityDraft {
|
||||
@@ -44,3 +45,7 @@ export interface SettingOption<T extends string> {
|
||||
label: string
|
||||
value: T
|
||||
}
|
||||
|
||||
export interface RegionOption extends SettingOption<RegionValue> {
|
||||
key: string
|
||||
}
|
||||
|
||||
@@ -1,288 +1,443 @@
|
||||
import { computed, ref, shallowRef, watch, type Ref } from 'vue'
|
||||
import { ElMessage } from 'element-plus'
|
||||
import md5 from 'md5'
|
||||
import {
|
||||
languageValues,
|
||||
regionValues,
|
||||
roleValues,
|
||||
type LanguageValue,
|
||||
type RegionValue,
|
||||
type RoleValue,
|
||||
type SecurityDraft,
|
||||
type SettingsData
|
||||
fetchUserProfile,
|
||||
type UserProfile,
|
||||
updateUserProfile,
|
||||
verifyEmailCode,
|
||||
fetchVerifyCode,
|
||||
setUserLanguage
|
||||
} from '@/api/user'
|
||||
import regionList from '@/utils/area'
|
||||
import {
|
||||
languageValues,
|
||||
roleValues,
|
||||
type LanguageValue,
|
||||
type RoleValue,
|
||||
type SecurityDraft,
|
||||
type SettingsData
|
||||
} from './types'
|
||||
import { validateCase, validateLength, validateSpecial } from '@/views/login/tools'
|
||||
import {useUserInfoStore} from '@/stores'
|
||||
|
||||
const userInfoStore = useUserInfoStore()
|
||||
|
||||
type Translate = (key: string, ...args: unknown[]) => string
|
||||
|
||||
interface UseSettingsFormOptions {
|
||||
t: Translate
|
||||
locale: Ref<string>
|
||||
}
|
||||
|
||||
const languageLocaleMap: Record<LanguageValue, 'ENGLISH' | 'CHINESE_SIMPLIFIED'> = {
|
||||
english: 'ENGLISH',
|
||||
chinese: 'CHINESE_SIMPLIFIED'
|
||||
t: Translate
|
||||
locale: Ref<string>
|
||||
}
|
||||
|
||||
const emailPattern = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
|
||||
const isRoleValue = (value: string): value is RoleValue =>
|
||||
(roleValues as readonly string[]).includes(value)
|
||||
|
||||
const createDefaultData = (): SettingsData => ({
|
||||
firstName: 'Alexandra',
|
||||
lastName: 'Chen',
|
||||
email: 'alex.chen@gmail.com',
|
||||
username: '@alexandra_chen',
|
||||
role: ['student', 'graphicDesigner'],
|
||||
language: 'english',
|
||||
region: 'hongKongSar'
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
email: '',
|
||||
username: '',
|
||||
roles: [] as RoleValue[],
|
||||
language: '',
|
||||
region: '',
|
||||
avatarUrl: ''
|
||||
})
|
||||
|
||||
const cloneSettingsData = (data: SettingsData): SettingsData => ({
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
email: data.email,
|
||||
username: data.username,
|
||||
role: [...data.role],
|
||||
language: data.language,
|
||||
region: data.region
|
||||
firstName: data.firstName,
|
||||
lastName: data.lastName,
|
||||
email: data.email,
|
||||
username: data.username,
|
||||
roles: [...data.roles],
|
||||
language: data.language,
|
||||
region: data.region,
|
||||
avatarUrl: data.avatarUrl
|
||||
})
|
||||
|
||||
const normalizeLanguage = (language: string | null | undefined): LanguageValue => {
|
||||
if (!language) {
|
||||
return '' as LanguageValue
|
||||
}
|
||||
const trimmed = language.trim()
|
||||
if (trimmed === 'ENGLISH' || trimmed === 'CHINESE_SIMPLIFIED') {
|
||||
return trimmed as LanguageValue
|
||||
}
|
||||
return '' as LanguageValue
|
||||
}
|
||||
|
||||
const buildSettingsDataFromProfile = (profile: Partial<UserProfile>): SettingsData => ({
|
||||
firstName: profile.firstName || '',
|
||||
lastName: profile.lastName || '',
|
||||
username: profile.username || '',
|
||||
email: profile.email || '',
|
||||
roles: (profile.roles || []).filter(isRoleValue),
|
||||
language: normalizeLanguage(profile.language),
|
||||
region: profile.region || '',
|
||||
avatarUrl: profile.avatarUrl || ''
|
||||
})
|
||||
|
||||
const createEmptySecurityDraft = (): SecurityDraft => ({
|
||||
newEmail: '',
|
||||
newPassword: '',
|
||||
currentPassword: ''
|
||||
newEmail: '',
|
||||
newPassword: '',
|
||||
currentPassword: ''
|
||||
})
|
||||
|
||||
export function useSettingsForm({ t, locale }: UseSettingsFormOptions) {
|
||||
const sourceData = ref<SettingsData>(createDefaultData())
|
||||
const draftData = ref<SettingsData>(cloneSettingsData(sourceData.value))
|
||||
const securityDraft = ref<SecurityDraft>(createEmptySecurityDraft())
|
||||
const isEditing = shallowRef(false)
|
||||
const saving = shallowRef(false)
|
||||
const isVerificationDialogVisible = shallowRef(false)
|
||||
const verificationTargetEmail = shallowRef('')
|
||||
const verifiedEmail = shallowRef('')
|
||||
const sourceData = ref<SettingsData>(createDefaultData())
|
||||
const draftData = ref<SettingsData>(cloneSettingsData(sourceData.value))
|
||||
const securityDraft = ref<SecurityDraft>(createEmptySecurityDraft())
|
||||
const isEditing = shallowRef(false)
|
||||
const saving = shallowRef(false)
|
||||
const isVerificationDialogVisible = shallowRef(false)
|
||||
const verificationTargetEmail = shallowRef('')
|
||||
const verifiedEmail = shallowRef('')
|
||||
const verificationCode = shallowRef('')
|
||||
const isEditingEmail = shallowRef(false)
|
||||
const isEditingPassword = shallowRef(false)
|
||||
|
||||
const roleList = computed(() =>
|
||||
roleValues.map((value) => ({
|
||||
name: t(`Settings.roles.${value}`),
|
||||
value
|
||||
}))
|
||||
)
|
||||
const roleList = computed(() =>
|
||||
roleValues.map((value) => ({
|
||||
name: t(`Settings.roles.${value}`),
|
||||
value
|
||||
}))
|
||||
)
|
||||
|
||||
const languageList = computed(() =>
|
||||
languageValues.map((value) => ({
|
||||
label: t(`Settings.languages.${value}`),
|
||||
value
|
||||
}))
|
||||
)
|
||||
const languageList = computed(() =>
|
||||
languageValues.map((value) => ({
|
||||
label: t(`Settings.languages.${value}`),
|
||||
value
|
||||
}))
|
||||
)
|
||||
|
||||
const regionList = computed(() =>
|
||||
regionValues.map((value) => ({
|
||||
label: t(`Settings.regions.${value}`),
|
||||
value
|
||||
}))
|
||||
)
|
||||
const displayData = computed(() => (isEditing.value ? draftData.value : sourceData.value))
|
||||
const normalizedNewEmail = computed(() => securityDraft.value.newEmail.trim())
|
||||
const hasNewEmailChange = computed(
|
||||
() =>
|
||||
normalizedNewEmail.value.length > 0 &&
|
||||
normalizedNewEmail.value !== sourceData.value.email
|
||||
)
|
||||
const isEmailVerified = computed(
|
||||
() => hasNewEmailChange.value && verifiedEmail.value === normalizedNewEmail.value
|
||||
)
|
||||
const hasNewPasswordChange = computed(() => securityDraft.value.newPassword.length > 0)
|
||||
const needsEmailVerification = computed(() => {
|
||||
if (!hasNewEmailChange.value && !hasNewPasswordChange.value) return false
|
||||
if (hasNewEmailChange.value && !isEmailVerified.value) return true
|
||||
if (hasNewPasswordChange.value && !verificationCode.value) return true
|
||||
return false
|
||||
})
|
||||
const displayLanguageLabel = computed(() =>
|
||||
displayData.value.language ? t(`Settings.languages.${displayData.value.language}`) : ''
|
||||
)
|
||||
const displayRegionLabel = computed(() => {
|
||||
if (displayData.value.region) {
|
||||
const regionItem = regionList.find((item) => item.value === displayData.value.region)
|
||||
return regionItem.key
|
||||
} else {
|
||||
return ''
|
||||
}
|
||||
})
|
||||
|
||||
const displayData = computed(() => (isEditing.value ? draftData.value : sourceData.value))
|
||||
const normalizedNewEmail = computed(() => securityDraft.value.newEmail.trim())
|
||||
const hasNewEmailChange = computed(
|
||||
() => normalizedNewEmail.value.length > 0 && normalizedNewEmail.value !== sourceData.value.email
|
||||
)
|
||||
const isEmailVerified = computed(
|
||||
() => hasNewEmailChange.value && verifiedEmail.value === normalizedNewEmail.value
|
||||
)
|
||||
const displayLanguageLabel = computed(() => t(`Settings.languages.${displayData.value.language}`))
|
||||
const displayRegionLabel = computed(() => t(`Settings.regions.${displayData.value.region}`))
|
||||
const fullName = computed(() => {
|
||||
const data = displayData.value
|
||||
return `${data.firstName} ${data.lastName}`.trim()
|
||||
})
|
||||
|
||||
const fullName = computed(() => {
|
||||
const data = displayData.value
|
||||
return `${data.firstName} ${data.lastName}`.trim()
|
||||
})
|
||||
const roleModel = computed<RoleValue[]>({
|
||||
get: () => displayData.value.roles,
|
||||
set: (value) => {
|
||||
if (isEditing.value) {
|
||||
draftData.value.roles = value
|
||||
return
|
||||
}
|
||||
|
||||
const roleModel = computed<RoleValue[]>({
|
||||
get: () => displayData.value.role,
|
||||
set: (value) => {
|
||||
if (isEditing.value) {
|
||||
draftData.value.role = value
|
||||
return
|
||||
}
|
||||
sourceData.value.roles = value
|
||||
}
|
||||
})
|
||||
|
||||
sourceData.value.role = value
|
||||
}
|
||||
})
|
||||
const resetEmailVerificationState = () => {
|
||||
isVerificationDialogVisible.value = false
|
||||
verificationTargetEmail.value = ''
|
||||
verifiedEmail.value = ''
|
||||
verificationCode.value = ''
|
||||
}
|
||||
|
||||
const resetEmailVerificationState = () => {
|
||||
isVerificationDialogVisible.value = false
|
||||
verificationTargetEmail.value = ''
|
||||
verifiedEmail.value = ''
|
||||
}
|
||||
const syncAppLanguage = (language: LanguageValue) => {
|
||||
locale.value = language
|
||||
localStorage.setItem('language', language)
|
||||
}
|
||||
|
||||
const syncAppLanguage = (language: LanguageValue) => {
|
||||
const nextLocale = languageLocaleMap[language]
|
||||
locale.value = nextLocale
|
||||
localStorage.setItem('language', nextLocale)
|
||||
}
|
||||
const loadUserProfile = async () => {
|
||||
try {
|
||||
const profile = (await fetchUserProfile()) as Partial<UserProfile>
|
||||
const nextData = buildSettingsDataFromProfile(profile)
|
||||
sourceData.value = cloneSettingsData(nextData)
|
||||
draftData.value = cloneSettingsData(sourceData.value)
|
||||
|
||||
const resetDraftState = () => {
|
||||
draftData.value = cloneSettingsData(sourceData.value)
|
||||
securityDraft.value = createEmptySecurityDraft()
|
||||
resetEmailVerificationState()
|
||||
}
|
||||
if (sourceData.value.language) {
|
||||
syncAppLanguage(sourceData.value.language as LanguageValue)
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
}
|
||||
}
|
||||
|
||||
const handleEdit = () => {
|
||||
resetDraftState()
|
||||
isEditing.value = true
|
||||
}
|
||||
const resetDraftState = () => {
|
||||
draftData.value = cloneSettingsData(sourceData.value)
|
||||
securityDraft.value = createEmptySecurityDraft()
|
||||
resetEmailVerificationState()
|
||||
isEditingEmail.value = false
|
||||
isEditingPassword.value = false
|
||||
}
|
||||
|
||||
const resetSecurityEmail = () => {
|
||||
securityDraft.value.newEmail = ''
|
||||
resetEmailVerificationState()
|
||||
}
|
||||
const handleEdit = () => {
|
||||
resetDraftState()
|
||||
isEditing.value = true
|
||||
}
|
||||
|
||||
const resetSecurityPassword = () => {
|
||||
securityDraft.value.newPassword = ''
|
||||
securityDraft.value.currentPassword = ''
|
||||
}
|
||||
const resetSecurityEmail = () => {
|
||||
securityDraft.value.newEmail = ''
|
||||
resetEmailVerificationState()
|
||||
isEditingEmail.value = false
|
||||
}
|
||||
|
||||
const handleDiscard = () => {
|
||||
resetDraftState()
|
||||
isEditing.value = false
|
||||
}
|
||||
const resetSecurityPassword = () => {
|
||||
securityDraft.value.newPassword = ''
|
||||
securityDraft.value.currentPassword = ''
|
||||
isEditingPassword.value = false
|
||||
}
|
||||
|
||||
const closeVerificationDialog = () => {
|
||||
isVerificationDialogVisible.value = false
|
||||
verificationTargetEmail.value = ''
|
||||
}
|
||||
const handleEditEmail = () => {
|
||||
isEditingEmail.value = true
|
||||
}
|
||||
|
||||
const handleVerifyEmail = () => {
|
||||
const nextEmail = normalizedNewEmail.value
|
||||
const handleEditPassword = () => {
|
||||
isEditingPassword.value = true
|
||||
}
|
||||
|
||||
if (!nextEmail) {
|
||||
ElMessage.warning(t('Settings.messages.enterNewEmailFirst'))
|
||||
return
|
||||
}
|
||||
const handleDiscard = () => {
|
||||
resetDraftState()
|
||||
isEditing.value = false
|
||||
}
|
||||
|
||||
if (!emailPattern.test(nextEmail)) {
|
||||
ElMessage.warning(t('Settings.messages.invalidEmail'))
|
||||
return
|
||||
}
|
||||
const closeVerificationDialog = () => {
|
||||
isVerificationDialogVisible.value = false
|
||||
verificationTargetEmail.value = ''
|
||||
}
|
||||
|
||||
if (nextEmail === sourceData.value.email) {
|
||||
ElMessage.warning(t('Settings.messages.sameEmail'))
|
||||
return
|
||||
}
|
||||
const handleVerifyEmail = () => {
|
||||
if (!hasNewEmailChange.value && !hasNewPasswordChange.value) return
|
||||
|
||||
if (verifiedEmail.value === nextEmail) {
|
||||
ElMessage.success(t('Settings.messages.alreadyVerified'))
|
||||
return
|
||||
}
|
||||
const nextEmail = normalizedNewEmail.value
|
||||
const newPassword = securityDraft.value.newPassword
|
||||
const currentPassword = securityDraft.value.currentPassword
|
||||
let targetEmail = ''
|
||||
|
||||
verificationTargetEmail.value = nextEmail
|
||||
handleSendVerifyCode()
|
||||
isVerificationDialogVisible.value = true
|
||||
}
|
||||
if (hasNewPasswordChange.value) {
|
||||
if (validateLength(newPassword)) {
|
||||
ElMessage.warning(t('Settings.messages.passwordLengthError', { min: 6, max: 20 }))
|
||||
return
|
||||
}
|
||||
|
||||
const handleSendVerifyCode = () => {
|
||||
ElMessage.success(t('Settings.messages.verificationCodeSent'))
|
||||
}
|
||||
if (validateSpecial(newPassword)) {
|
||||
ElMessage.warning(t('Settings.messages.passwordSpecial'))
|
||||
return
|
||||
}
|
||||
|
||||
const handleVerificationSubmit = (code: string) => {
|
||||
if (code.length !== 6) {
|
||||
ElMessage.warning(t('Settings.messages.enterVerificationCode'))
|
||||
return
|
||||
}
|
||||
if (validateCase(newPassword)) {
|
||||
ElMessage.warning(t('Settings.messages.passwordCase'))
|
||||
return
|
||||
}
|
||||
|
||||
verifiedEmail.value = verificationTargetEmail.value
|
||||
closeVerificationDialog()
|
||||
ElMessage.success(t('Settings.messages.verificationCompleted'))
|
||||
}
|
||||
if (newPassword === currentPassword) {
|
||||
ElMessage.warning(t('Settings.messages.passwordNotSameAsOld'))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const buildNextData = (): SettingsData => {
|
||||
const nextEmail = securityDraft.value.newEmail.trim() || draftData.value.email
|
||||
if (hasNewEmailChange.value) {
|
||||
if (!emailPattern.test(nextEmail)) {
|
||||
ElMessage.warning(t('Settings.messages.invalidEmail'))
|
||||
return
|
||||
}
|
||||
|
||||
return {
|
||||
firstName: draftData.value.firstName.trim(),
|
||||
lastName: draftData.value.lastName.trim(),
|
||||
username: draftData.value.username.trim(),
|
||||
email: nextEmail,
|
||||
role: [...draftData.value.role],
|
||||
language: draftData.value.language,
|
||||
region: draftData.value.region
|
||||
}
|
||||
}
|
||||
if (nextEmail === sourceData.value.email) {
|
||||
ElMessage.warning(t('Settings.messages.sameEmail'))
|
||||
return
|
||||
}
|
||||
|
||||
const handleSave = async () => {
|
||||
if (hasNewEmailChange.value && !isEmailVerified.value) {
|
||||
ElMessage.warning(t('Settings.messages.verifyEmailBeforeSave'))
|
||||
return
|
||||
}
|
||||
if (verifiedEmail.value === nextEmail) {
|
||||
ElMessage.success(t('Settings.messages.alreadyVerified'))
|
||||
return
|
||||
}
|
||||
|
||||
const nextData = buildNextData()
|
||||
const previousLanguage = sourceData.value.language
|
||||
saving.value = true
|
||||
targetEmail = nextEmail
|
||||
} else if (hasNewPasswordChange.value) {
|
||||
targetEmail = sourceData.value.email
|
||||
}
|
||||
|
||||
try {
|
||||
sourceData.value = cloneSettingsData(nextData)
|
||||
if (!targetEmail) return
|
||||
|
||||
if (nextData.language !== previousLanguage) {
|
||||
syncAppLanguage(nextData.language)
|
||||
}
|
||||
verificationTargetEmail.value = targetEmail
|
||||
handleSendVerifyCode()
|
||||
}
|
||||
|
||||
draftData.value = cloneSettingsData(sourceData.value)
|
||||
securityDraft.value = createEmptySecurityDraft()
|
||||
resetEmailVerificationState()
|
||||
isEditing.value = false
|
||||
ElMessage.success(t('Settings.messages.settingsUpdated'))
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
const handleSendVerifyCode = () => {
|
||||
fetchVerifyCode().then(() => {
|
||||
ElMessage.success(t('Settings.messages.verificationCodeSent'))
|
||||
isVerificationDialogVisible.value = true
|
||||
})
|
||||
}
|
||||
|
||||
watch(
|
||||
() => securityDraft.value.newEmail,
|
||||
(value) => {
|
||||
const trimmedValue = value.trim()
|
||||
const handleVerificationSubmit = (code: string) => {
|
||||
if (code.length !== 6) {
|
||||
ElMessage.warning(t('Settings.messages.enterVerificationCode'))
|
||||
return
|
||||
}
|
||||
// send code to backend and store the code locally so save() can include it
|
||||
verifyEmailCode(code).then((res) => {
|
||||
verificationCode.value = code
|
||||
verifiedEmail.value = verificationTargetEmail.value
|
||||
closeVerificationDialog()
|
||||
ElMessage.success(t('Settings.messages.verificationCompleted'))
|
||||
})
|
||||
}
|
||||
|
||||
if (verifiedEmail.value && trimmedValue !== verifiedEmail.value) {
|
||||
verifiedEmail.value = ''
|
||||
}
|
||||
const handleSave = async () => {
|
||||
if (hasNewEmailChange.value && !isEmailVerified.value) {
|
||||
ElMessage.warning(t('Settings.messages.verifyEmailBeforeSave'))
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
isVerificationDialogVisible.value &&
|
||||
verificationTargetEmail.value &&
|
||||
trimmedValue !== verificationTargetEmail.value
|
||||
) {
|
||||
closeVerificationDialog()
|
||||
}
|
||||
}
|
||||
)
|
||||
if (hasNewPasswordChange.value && !verificationCode.value) {
|
||||
ElMessage.warning(t('Settings.messages.verifyEmailBeforeSave'))
|
||||
return
|
||||
}
|
||||
|
||||
return {
|
||||
sourceData,
|
||||
draftData,
|
||||
securityDraft,
|
||||
isEditing,
|
||||
saving,
|
||||
isVerificationDialogVisible,
|
||||
verificationTargetEmail,
|
||||
roleList,
|
||||
languageList,
|
||||
regionList,
|
||||
displayData,
|
||||
isEmailVerified,
|
||||
displayLanguageLabel,
|
||||
displayRegionLabel,
|
||||
fullName,
|
||||
roleModel,
|
||||
handleEdit,
|
||||
handleDiscard,
|
||||
handleSave,
|
||||
resetSecurityEmail,
|
||||
resetSecurityPassword,
|
||||
handleVerifyEmail,
|
||||
handleSendVerifyCode,
|
||||
handleVerificationSubmit,
|
||||
closeVerificationDialog
|
||||
}
|
||||
// 前端语言值直接作为后端格式
|
||||
const backendLanguage = draftData.value.language || ''
|
||||
|
||||
const nextData: UserProfile = {
|
||||
firstName: draftData.value.firstName.trim(),
|
||||
lastName: draftData.value.lastName.trim(),
|
||||
username: draftData.value.username.trim(),
|
||||
email: securityDraft.value.newEmail.trim(),
|
||||
roles: draftData.value.roles as string[],
|
||||
language: backendLanguage,
|
||||
region: draftData.value.region,
|
||||
newPassword: '',
|
||||
oldPassword: '',
|
||||
verifyCode: ''
|
||||
}
|
||||
|
||||
// 如果改邮箱或改密码,需要添加验证码和密码信息
|
||||
if (hasNewEmailChange.value || hasNewPasswordChange.value) {
|
||||
nextData.verifyCode = verificationCode.value
|
||||
}
|
||||
|
||||
if (hasNewPasswordChange.value) {
|
||||
nextData.oldPassword = md5(securityDraft.value.currentPassword)
|
||||
nextData.newPassword = md5(securityDraft.value.newPassword)
|
||||
}
|
||||
|
||||
const previousLanguage = sourceData.value.language
|
||||
saving.value = true
|
||||
|
||||
try {
|
||||
await updateUserProfile(nextData)
|
||||
|
||||
// 后端返回的语言值直接作为前端格式
|
||||
const frontendLanguage = backendLanguage as LanguageValue
|
||||
|
||||
const settingsData: SettingsData = {
|
||||
firstName: nextData.firstName,
|
||||
lastName: nextData.lastName,
|
||||
username: nextData.username,
|
||||
email: nextData.email || draftData.value.email,
|
||||
roles: nextData.roles as RoleValue[],
|
||||
language: frontendLanguage,
|
||||
region: nextData.region as any,
|
||||
avatarUrl: sourceData.value.avatarUrl
|
||||
}
|
||||
sourceData.value = cloneSettingsData(settingsData)
|
||||
console.log(nextData)
|
||||
|
||||
if (frontendLanguage && frontendLanguage !== previousLanguage) {
|
||||
// syncAppLanguage(frontendLanguage as LanguageValue)
|
||||
setUserLanguage(frontendLanguage).then((res) => {
|
||||
// console.log(res)
|
||||
userInfoStore.setToken(res)
|
||||
location.reload()
|
||||
})
|
||||
}
|
||||
|
||||
draftData.value = cloneSettingsData(sourceData.value)
|
||||
securityDraft.value = createEmptySecurityDraft()
|
||||
resetEmailVerificationState()
|
||||
isEditing.value = false
|
||||
isEditingEmail.value = false
|
||||
isEditingPassword.value = false
|
||||
ElMessage.success(t('Settings.messages.settingsUpdated'))
|
||||
} catch (error) {
|
||||
console.warn(error)
|
||||
} finally {
|
||||
saving.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => securityDraft.value.newEmail,
|
||||
(value) => {
|
||||
const trimmedValue = value.trim()
|
||||
|
||||
if (verifiedEmail.value && trimmedValue !== verifiedEmail.value) {
|
||||
verifiedEmail.value = ''
|
||||
}
|
||||
|
||||
if (
|
||||
isVerificationDialogVisible.value &&
|
||||
verificationTargetEmail.value &&
|
||||
trimmedValue !== verificationTargetEmail.value
|
||||
) {
|
||||
closeVerificationDialog()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
return {
|
||||
sourceData,
|
||||
draftData,
|
||||
securityDraft,
|
||||
isEditing,
|
||||
saving,
|
||||
isVerificationDialogVisible,
|
||||
verificationTargetEmail,
|
||||
roleList,
|
||||
languageList,
|
||||
regionList,
|
||||
displayData,
|
||||
isEmailVerified,
|
||||
displayLanguageLabel,
|
||||
displayRegionLabel,
|
||||
fullName,
|
||||
roleModel,
|
||||
needsEmailVerification,
|
||||
isEditingEmail,
|
||||
isEditingPassword,
|
||||
handleEdit,
|
||||
handleDiscard,
|
||||
handleSave,
|
||||
resetSecurityEmail,
|
||||
resetSecurityPassword,
|
||||
handleEditEmail,
|
||||
handleEditPassword,
|
||||
handleVerifyEmail,
|
||||
handleSendVerifyCode,
|
||||
handleVerificationSubmit,
|
||||
closeVerificationDialog,
|
||||
loadUserProfile
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, reactive, toRefs } from "vue";
|
||||
import myEvent from '@/utils/myEvent'
|
||||
import scList from '@/views/shoppingCart/sc-list.vue'
|
||||
// import scList from '@/views/shoppingCart/sc-list.vue'
|
||||
import { useRouter } from "vue-router";
|
||||
import img from '@/assets/images/brand-null.png'
|
||||
|
||||
//const props = defineProps({
|
||||
//})
|
||||
@@ -9,15 +11,28 @@ import scList from '@/views/shoppingCart/sc-list.vue'
|
||||
//])
|
||||
let data = reactive({
|
||||
})
|
||||
const cover = ref('')
|
||||
const price = ref('')
|
||||
const shopName = ref('')
|
||||
const commodityName = ref('')
|
||||
const router = useRouter()
|
||||
const isShoppingShow = ref(false)
|
||||
const shoppingClose = () => {
|
||||
isShoppingShow.value = false
|
||||
}
|
||||
|
||||
const goShopping = () => {
|
||||
router.push({path: '/shoppingCart'})
|
||||
isShoppingShow.value = false
|
||||
}
|
||||
|
||||
onMounted(()=>{
|
||||
myEvent.add('addShopping', (item) => {
|
||||
isShoppingShow.value = true
|
||||
console.log(item)
|
||||
cover.value = item.cover || ''
|
||||
price.value = item.price || ''
|
||||
shopName.value = item.shopName || ''
|
||||
commodityName.value = item.title || ''
|
||||
})
|
||||
})
|
||||
onUnmounted(()=>{
|
||||
@@ -27,14 +42,182 @@ defineExpose({})
|
||||
const {} = toRefs(data);
|
||||
</script>
|
||||
<template>
|
||||
<el-drawer v-model="isShoppingShow" width="50rem" :close-on-click-modal="false" title="I am the title" :with-header="false">
|
||||
<sc-list is-mini style="flex: 0.6;" @close="shoppingClose"/>
|
||||
<el-drawer v-model="isShoppingShow" width="50rem" class="addShoppingDrawer" :close-on-click-modal="true" title="I am the title" :with-header="false">
|
||||
<div class="addShoppingInfo">
|
||||
<div class="header">
|
||||
<div class="title">{{ $t('addShoppingCart.title') }}</div>
|
||||
<span class="close" @click="shoppingClose"
|
||||
><svg-icon name="close" size="13"
|
||||
/></span>
|
||||
</div>
|
||||
<div class="content">
|
||||
<div class="img-list">
|
||||
<div class="img-box">
|
||||
<!-- <img :src="img" alt=""> -->
|
||||
</div>
|
||||
<div class="img-box">
|
||||
<!-- <img :src="img" alt=""> -->
|
||||
</div>
|
||||
<div class="img-box">
|
||||
<img :src="cover" alt="">
|
||||
</div>
|
||||
</div>
|
||||
<div class="inf-box">
|
||||
<div class="name">{{ commodityName }}</div>
|
||||
<div class="shopping-name">
|
||||
<div class="icon">
|
||||
<SvgIcon name="shop" size="24" />
|
||||
</div>
|
||||
{{ shopName }}
|
||||
</div>
|
||||
<div class="price">${{ price }} <span class="currency">HKD</span></div>
|
||||
</div>
|
||||
<div class="statement">
|
||||
<div class="icon">
|
||||
<SvgIcon name="statement" size="20" />
|
||||
</div>
|
||||
{{ $t('addShoppingCart.statement') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="button" @click="goShopping">
|
||||
{{ $t('addShoppingCart.button') }}
|
||||
</div>
|
||||
</div>
|
||||
<!-- <sc-list is-mini style="flex: 0.6;" @close="shoppingClose"/> -->
|
||||
</el-drawer>
|
||||
</template>
|
||||
<style lang="less">
|
||||
.el-drawer.addShoppingDrawer{
|
||||
--el-drawer-padding-primary: 2.4rem 3.4rem;
|
||||
}
|
||||
</style>
|
||||
<style lang="less" scoped>
|
||||
.homeNavBox{
|
||||
|
||||
.addShoppingInfo{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
> .header{
|
||||
border-bottom: 0.1rem solid #c4c4c4;
|
||||
display: flex;
|
||||
padding-bottom: 2.4rem;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
> .title{
|
||||
font-family: KaiseiOpti-Bold;
|
||||
font-size: 2rem;
|
||||
line-height: 120%;
|
||||
color: #121212;
|
||||
font-weight: 400;
|
||||
}
|
||||
> .close{
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
> .content{
|
||||
flex: 1;
|
||||
padding-top: 9.6rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
> .img-list{
|
||||
height: 37.5rem;
|
||||
width: 33.7rem;
|
||||
position: relative;
|
||||
> .img-box{
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
width: 26.9rem;
|
||||
height: 34.1rem;
|
||||
border: 1px solid #EFEFEF;
|
||||
top: 1.7rem;
|
||||
right: 0;
|
||||
left: auto;
|
||||
transform-origin: bottom;
|
||||
box-shadow: 1rem .8rem 2.4rem 0px #4D4D4D0A;
|
||||
> img{
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
&:nth-child(1){
|
||||
transform: rotate(-8deg);
|
||||
background-color: #eaeaea;
|
||||
right: 2rem;
|
||||
}
|
||||
&:nth-child(2){
|
||||
transform: rotate(-4deg);
|
||||
background-color: #eeeeee;
|
||||
right: 1rem;
|
||||
}
|
||||
&:nth-child(3){
|
||||
transform: rotate(0);
|
||||
background-color: #fafafa;
|
||||
}
|
||||
}
|
||||
}
|
||||
> .inf-box{
|
||||
margin-top: 5.18rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
> .name{
|
||||
font-family: KaiseiOpti-Bold;
|
||||
font-weight: 700;
|
||||
font-size: 2.4rem;
|
||||
line-height: 140%;
|
||||
}
|
||||
> .shopping-name{
|
||||
margin-top: 1.3rem;
|
||||
font-weight: 500;
|
||||
font-size: 1.6rem;
|
||||
line-height: 140%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
> .icon{
|
||||
margin-right: 0.8rem;
|
||||
}
|
||||
}
|
||||
> .price{
|
||||
margin-top: 1.2rem;
|
||||
font-family: KaiseiOpti-Bold;
|
||||
font-weight: 700;
|
||||
font-style: Bold;
|
||||
font-size: 1.8rem;
|
||||
line-height: 140%;
|
||||
> .currency{
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
line-height: 140%;
|
||||
}
|
||||
}
|
||||
}
|
||||
> .statement{
|
||||
margin-top: 5rem;
|
||||
font-family: KaiseiOpti-Regular;
|
||||
color: #979797;
|
||||
display: flex;
|
||||
font-weight: 400;
|
||||
font-size: 1.2rem;
|
||||
line-height: 140%;
|
||||
> .icon{
|
||||
margin-right: 0.8rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
> .button{
|
||||
font-family: KaiseiOpti-Regular;
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 4.6rem;
|
||||
letter-spacing: 3%;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
border: 1px solid #232323;
|
||||
margin-bottom: calc(6rem - 2.4rem);
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
5
src/views/shoppingCart/index.d.js
Normal file
@@ -0,0 +1,5 @@
|
||||
/** 商品状态 */
|
||||
export const SCART_STATUS = {
|
||||
/** 正常 */
|
||||
NORMAL: 1,
|
||||
}
|
||||
@@ -3,8 +3,6 @@
|
||||
<div class="content">
|
||||
<sc-list @selected-change="(v) => (selectedList = v)" />
|
||||
<order-summary :list="selectedList" />
|
||||
<!-- <sc-list is-mini style="height: 70rem" /> -->
|
||||
<!-- <sc-list is-mini is-view title="Order Summary" style="height: 70rem" /> -->
|
||||
</div>
|
||||
<my-footer />
|
||||
</div>
|
||||
|
||||
@@ -1,39 +1,43 @@
|
||||
<template>
|
||||
<div class="order-summary">
|
||||
<div class="title">Order Summary</div>
|
||||
<div class="title">{{ $t('ShoppingCart.orderSummary') }}</div>
|
||||
<div class="count">
|
||||
<span class="label">Selected</span>
|
||||
<span class="label">{{ $t('ShoppingCart.selected') }}</span>
|
||||
<span class="value">{{ brandsList.length }}</span>
|
||||
</div>
|
||||
<div class="hr"></div>
|
||||
<div class="brands-header">
|
||||
<span class="icon"><svg-icon name="order-shop" size="24" /></span>
|
||||
<span class="text">Brands</span>
|
||||
<span class="text">{{ $t('ShoppingCart.brands') }}</span>
|
||||
</div>
|
||||
<div class="brands-item" v-for="v in brandsList" :key="v.brand">
|
||||
<span class="label">{{ v.brand }}</span>
|
||||
<span class="label" @click="handleBrandClick(v.id)">{{ v.brand }}</span>
|
||||
<span class="value"
|
||||
><span>{{ v.children.length }}</span
|
||||
>item</span
|
||||
>{{ $t('ShoppingCart.item') }}</span
|
||||
>
|
||||
</div>
|
||||
<br />
|
||||
<br />
|
||||
<div class="total">
|
||||
<span class="label">Total</span>
|
||||
<span class="label">{{ $t('ShoppingCart.total') }}</span>
|
||||
<span class="value"
|
||||
><span>${{ totalAmount }}</span> HKD</span
|
||||
>
|
||||
</div>
|
||||
<div class="hr"></div>
|
||||
<button class="checkout-btn" custom="black" @click="handleCheckout">CHECKOUT SELECTED</button>
|
||||
<div class="tip">Digital assets. Creator retains copyright.</div>
|
||||
<button class="checkout-btn" custom="black" @click="handleCheckout">
|
||||
{{ $t('ShoppingCart.checkoutSelected') }}
|
||||
</button>
|
||||
<div class="tip">{{ $t('ShoppingCart.digitalAssets') }}</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { FormatBytes, FormatDate } from '@/utils/tools'
|
||||
import { useRouter } from 'vue-router'
|
||||
const router = useRouter()
|
||||
const props = defineProps({
|
||||
list: {
|
||||
type: Array,
|
||||
@@ -47,6 +51,7 @@
|
||||
if (index === -1) {
|
||||
arr.push({
|
||||
brand: v.brand,
|
||||
id: v.sellerId,
|
||||
children: [v]
|
||||
})
|
||||
} else {
|
||||
@@ -57,7 +62,18 @@
|
||||
})
|
||||
const totalAmount = computed(() => props.list.reduce((pre, cur) => pre + cur.amount, 0).toFixed(2))
|
||||
const handleCheckout = () => {
|
||||
console.log('购买:', props.list)
|
||||
if (props.list.length === 0) return
|
||||
const list = btoa(encodeURIComponent(JSON.stringify(props.list)))
|
||||
router.push({
|
||||
name: 'pay',
|
||||
query: { list }
|
||||
})
|
||||
}
|
||||
const handleBrandClick = (id) => {
|
||||
router.push({
|
||||
name: 'brandDetail',
|
||||
params: { id }
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -115,6 +131,8 @@
|
||||
> .label {
|
||||
text-decoration: underline;
|
||||
color: #585858;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
> .value {
|
||||
color: #808080;
|
||||
@@ -154,6 +172,7 @@
|
||||
> .checkout-btn {
|
||||
width: 100%;
|
||||
margin-top: 3rem;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
> .tip {
|
||||
margin-top: 1rem;
|
||||
|
||||