Compare commits
51 Commits
af3161fc86
...
global_awa
| Author | SHA1 | Date | |
|---|---|---|---|
| f74d4d3099 | |||
| cf55389181 | |||
| c710394293 | |||
| 512cf36baf | |||
| dc72a132bd | |||
| a781b433f6 | |||
| 81adcb4bc4 | |||
| ca8764ad7a | |||
| f6d027474a | |||
| 18dcf18fb7 | |||
| b0438eec13 | |||
| 5f05f4773b | |||
| d3df9c138b | |||
| ea540f31c7 | |||
| 8233576c00 | |||
| abc73d759f | |||
| be7ce97a3c | |||
| 674c5dbbeb | |||
| 8c0fe1daf6 | |||
| 309cf49a54 | |||
| 18c5ad9521 | |||
| 9e20d709b0 | |||
| 2d335399f5 | |||
| aac25b05fe | |||
| 5a910eef15 | |||
| e7d967554a | |||
| 14b951d3fb | |||
| 60ebdb3d77 | |||
| 821c95148e | |||
| 77951a29ca | |||
| 341cbf7eb1 | |||
| 982b7308e8 | |||
| 6a08f2cede | |||
| 4989bb7156 | |||
| 5dd8840a0a | |||
| 6294f18b4b | |||
| 8efe7efa71 | |||
| 1b5d2bf762 | |||
| ec4ba64030 | |||
| f2c265e4e7 | |||
| fd352cac18 | |||
| 7e4929e260 | |||
| 32001dffc3 | |||
| 4daad8cb2f | |||
| 629a966d6b | |||
| d4bcbc0a64 | |||
| ad0a90e545 | |||
| c91981f3d5 | |||
| 1bd0e6334c | |||
| 73b8503ac7 | |||
| 7665bf59da |
@@ -1,4 +1,2 @@
|
||||
# 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_GOOGLE_CLIENT_ID = 216037134725-7q8vqp0ohtmohlosltkfg7bd2v29rm5a.apps.googleusercontent.com
|
||||
VITE_USER_NODE_ENV = 'development'
|
||||
VITE_APP_BASE_URL = 'https://develop.api.aida.com.hk'
|
||||
@@ -1,3 +1,4 @@
|
||||
VITE_APP_URL = https://www.lc-api.aida.com.hk
|
||||
# VITE_APP_URL = http://18.167.251.121:10095
|
||||
VITE_GOOGLE_CLIENT_ID = 29310152396-nnsd3h533fld665oguu8ovrt1nukmt46.apps.googleusercontent.com
|
||||
VITE_USER_NODE_ENV = 'production'
|
||||
# VITE_APP_BASE_URL = 'http://18.167.251.121:10086'
|
||||
# VITE_APP_BASE_URL = 'https://polyu.api.aida.com.hk'
|
||||
VITE_APP_BASE_URL = 'https://www.api.aida.com.hk'
|
||||
102
package-lock.json
generated
@@ -39,6 +39,7 @@
|
||||
"lint-staged": "^13.2.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.8.4",
|
||||
"terser": "^5.46.1",
|
||||
"typescript": "~4.8.4",
|
||||
"unplugin-auto-import": "^0.15.3",
|
||||
"unplugin-vue-components": "^0.24.1",
|
||||
@@ -668,6 +669,17 @@
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/source-map": {
|
||||
"version": "0.3.11",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
|
||||
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.25"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
@@ -1852,6 +1864,13 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cache-base": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/cache-base/-/cache-base-1.0.1.tgz",
|
||||
@@ -6999,6 +7018,17 @@
|
||||
"urix": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-support": {
|
||||
"version": "0.5.21",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-url": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/source-map-url/-/source-map-url-0.4.1.tgz",
|
||||
@@ -7606,6 +7636,32 @@
|
||||
"node": ">= 10"
|
||||
}
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "5.46.1",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz",
|
||||
"integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.15.0",
|
||||
"commander": "^2.20.0",
|
||||
"source-map-support": "~0.5.20"
|
||||
},
|
||||
"bin": {
|
||||
"terser": "bin/terser"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/terser/node_modules/commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/text-table": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz",
|
||||
@@ -9075,6 +9131,16 @@
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true
|
||||
},
|
||||
"@jridgewell/source-map": {
|
||||
"version": "0.3.11",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
|
||||
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.25"
|
||||
}
|
||||
},
|
||||
"@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
@@ -9937,6 +10003,12 @@
|
||||
"fill-range": "^7.0.1"
|
||||
}
|
||||
},
|
||||
"buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"dev": true
|
||||
},
|
||||
"cache-base": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/cache-base/-/cache-base-1.0.1.tgz",
|
||||
@@ -13802,6 +13874,16 @@
|
||||
"urix": "^0.1.0"
|
||||
}
|
||||
},
|
||||
"source-map-support": {
|
||||
"version": "0.5.21",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"source-map-url": {
|
||||
"version": "0.4.1",
|
||||
"resolved": "https://registry.npmmirror.com/source-map-url/-/source-map-url-0.4.1.tgz",
|
||||
@@ -14285,6 +14367,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"terser": {
|
||||
"version": "5.46.1",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.46.1.tgz",
|
||||
"integrity": "sha512-vzCjQO/rgUuK9sf8VJZvjqiqiHFaZLnOiimmUuOKODxWL8mm/xua7viT7aqX7dgPY60otQjUotzFMmCB4VdmqQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.15.0",
|
||||
"commander": "^2.20.0",
|
||||
"source-map-support": "~0.5.20"
|
||||
},
|
||||
"dependencies": {
|
||||
"commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"text-table": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz",
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
"lint-staged": "^13.2.1",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^2.8.4",
|
||||
"terser": "^5.46.1",
|
||||
"typescript": "~4.8.4",
|
||||
"unplugin-auto-import": "^0.15.3",
|
||||
"unplugin-vue-components": "^0.24.1",
|
||||
|
||||
17
src/App.vue
@@ -10,4 +10,21 @@ import RouteCache from '@/components/RouteCache.vue'
|
||||
#app {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
overscroll-behavior: none;
|
||||
overflow-x: hidden;
|
||||
&::-webkit-scrollbar {
|
||||
width: 0;
|
||||
display: none;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
display: none;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
display: none;
|
||||
}
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -18,7 +18,6 @@ body,
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
@keyframes loading {
|
||||
0% {
|
||||
@@ -28,11 +27,6 @@ body,
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
.background-pink {
|
||||
background-color: #f8f7f5;
|
||||
background-image: url('@/assets/images/home-bg.png');
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
@@ -103,4 +97,49 @@ body,
|
||||
@font-face {
|
||||
font-family: 'InstrumentBold';
|
||||
src: url('./fonts/InstrumentSans-Bold.ttf') format('truetype');
|
||||
}
|
||||
}
|
||||
|
||||
/* 遮罩层:利用 inherit 完美适配父元素形状 */
|
||||
.custom-loading-mask {
|
||||
position: absolute;
|
||||
z-index: 2000;
|
||||
background-color: rgba(255, 255, 255, 0.7);
|
||||
margin: 0;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: opacity 0.3s;
|
||||
|
||||
/* 核心修正:继承父元素的圆角,防止遮罩层直角溢出 */
|
||||
border-radius: inherit;
|
||||
/* 确保 top/left 0 包含在 border 之内 */
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* 加载图标 */
|
||||
.custom-loading-spinner {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 2px solid #f3f3f3;
|
||||
border-top: 2px solid #409eff;
|
||||
border-radius: 50%;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
/* 旋转动画 */
|
||||
@keyframes spin {
|
||||
0% { transform: rotate(0deg); }
|
||||
100% { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* 状态类:强制父元素定位 */
|
||||
.v-loading-parent--relative {
|
||||
position: relative !important;
|
||||
/* 如果父元素内容非常多且溢出,开启此项可防止 loading 跟着滚动 */
|
||||
/* overflow: hidden !important; */
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ body,
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
@keyframes loading {
|
||||
@@ -33,10 +33,5 @@ body,
|
||||
}
|
||||
}
|
||||
|
||||
.background-pink {
|
||||
background-color: rgba(248, 247, 245, 1);
|
||||
background-image: url('@/assets/images/home-bg.png');
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
|
||||
|
||||
|
||||
3
src/assets/icons/CDownload.svg
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg width="22" height="22" viewBox="0 0 22 22" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M11 2.75C11.5063 2.75 11.9167 3.16041 11.9167 3.66667V11.537L14.0185 9.43515C14.3765 9.07717 14.9569 9.07717 15.3148 9.43515C15.6728 9.79313 15.6728 10.3735 15.3148 10.7315L11.6482 14.3982C11.2902 14.7562 10.7098 14.7562 10.3518 14.3982L6.68515 10.7315C6.32717 10.3735 6.32717 9.79313 6.68515 9.43515C7.04313 9.07717 7.62354 9.07717 7.98152 9.43515L10.0833 11.537V3.66667C10.0833 3.16041 10.4937 2.75 11 2.75ZM3.66667 12.8333C4.17293 12.8333 4.58333 13.2437 4.58333 13.75V13.9333C4.58333 14.7185 4.58405 15.2523 4.61776 15.6649C4.65059 16.0668 4.71011 16.2723 4.78316 16.4156C4.95892 16.7606 5.23939 17.0411 5.58435 17.2168C5.72772 17.2899 5.93324 17.3494 6.33512 17.3822C6.7477 17.416 7.28147 17.4167 8.06667 17.4167H13.9333C14.7185 17.4167 15.2523 17.416 15.6649 17.3822C16.0668 17.3494 16.2723 17.2899 16.4157 17.2168C16.7606 17.0411 17.0411 16.7606 17.2168 16.4156C17.2899 16.2723 17.3494 16.0668 17.3822 15.6649C17.416 15.2523 17.4167 14.7185 17.4167 13.9333V13.75C17.4167 13.2437 17.8271 12.8333 18.3333 12.8333C18.8396 12.8333 19.25 13.2437 19.25 13.75V13.9712C19.25 14.7091 19.25 15.3181 19.2095 15.8142C19.1674 16.3294 19.077 16.8031 18.8504 17.248C18.4988 17.9379 17.9379 18.4988 17.248 18.8504C16.8031 19.077 16.3294 19.1674 15.8142 19.2095C15.3181 19.25 14.7091 19.25 13.9712 19.25H8.02879C7.29091 19.25 6.68192 19.25 6.18583 19.2095C5.67057 19.1674 5.19693 19.077 4.75204 18.8504C4.06211 18.4988 3.50118 17.9379 3.14964 17.248C2.92296 16.8031 2.83261 16.3294 2.79051 15.8142C2.74998 15.3181 2.74999 14.7091 2.75 13.9712L2.75 13.75C2.75 13.2437 3.16041 12.8333 3.66667 12.8333Z" fill="currentColor"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
BIN
src/assets/images/award/anniewong.png
Executable file
|
After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 198 B After Width: | Height: | Size: 224 B |
BIN
src/assets/images/award/close-menu.png
Normal file
|
After Width: | Height: | Size: 407 B |
BIN
src/assets/images/award/copy.png
Normal file
|
After Width: | Height: | Size: 269 B |
BIN
src/assets/images/award/drawer-arrow.png
Normal file
|
After Width: | Height: | Size: 323 B |
BIN
src/assets/images/award/drawer-mail.png
Normal file
|
After Width: | Height: | Size: 369 B |
BIN
src/assets/images/award/instagram.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src/assets/images/award/mail.png
Normal file
|
After Width: | Height: | Size: 248 B |
BIN
src/assets/images/award/menu.png
Normal file
|
After Width: | Height: | Size: 326 B |
BIN
src/assets/images/award/rosiexia.png
Normal file
|
After Width: | Height: | Size: 241 KiB |
BIN
src/assets/images/award/xavenmak.png
Normal file
|
After Width: | Height: | Size: 160 KiB |
4
src/assets/images/award/youtube.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="20" height="20" rx="3" fill="white"/>
|
||||
<path d="M17.6865 6.63967C17.514 5.99433 17.0057 5.486 16.3603 5.3135C15.1907 5 10.5 5 10.5 5C10.5 5 5.80933 5 4.63967 5.3135C3.99433 5.486 3.486 5.99433 3.3135 6.63967C3 7.80933 3 10.25 3 10.25C3 10.25 3 12.6907 3.3135 13.8603C3.486 14.5057 3.99433 15.014 4.63967 15.1865C5.80933 15.5 10.5 15.5 10.5 15.5C10.5 15.5 15.1907 15.5 16.3603 15.1865C17.0057 15.014 17.514 14.5057 17.6865 13.8603C18 12.6907 18 10.25 18 10.25C18 10.25 18 7.80933 17.6865 6.63967ZM9 12.5V8L12.897 10.25L9 12.5Z" fill="#232323"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 659 B |
BIN
src/assets/images/mobile_version_background/apply_bg.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
src/assets/images/mobile_version_background/banner_bg.png
Normal file
|
After Width: | Height: | Size: 930 KiB |
BIN
src/assets/images/mobile_version_background/banner_mobile.mp4
Normal file
BIN
src/assets/images/mobile_version_background/banner_mobile_zh.mp4
Normal file
BIN
src/assets/images/mobile_version_background/bloom_bg.png
Normal file
|
After Width: | Height: | Size: 486 KiB |
BIN
src/assets/images/mobile_version_background/ellipse.png
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
src/assets/images/mobile_version_background/judge_bg.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
src/assets/images/mobile_version_background/people.png
Normal file
|
After Width: | Height: | Size: 444 KiB |
BIN
src/assets/images/mobile_version_background/prizes_bg.png
Normal file
|
After Width: | Height: | Size: 1.8 MiB |
BIN
src/assets/images/mobile_version_background/section_bg.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
src/assets/images/mobile_version_background/timeline_bg.png
Normal file
|
After Width: | Height: | Size: 1.2 MiB |
BIN
src/assets/images/pad_version/banner_pad.mp4
Normal file
BIN
src/assets/images/pad_version/banner_pad_chinese.mp4
Normal file
BIN
src/assets/images/pad_version/bloom_bg.png
Normal file
|
After Width: | Height: | Size: 1005 KiB |
BIN
src/assets/images/pad_version/prizes-bg.png
Normal file
|
After Width: | Height: | Size: 1.7 MiB |
BIN
src/assets/images/pad_version/timeline_bg.png
Normal file
|
After Width: | Height: | Size: 1015 KiB |
75
src/components/Message/Message.vue
Normal file
@@ -0,0 +1,75 @@
|
||||
<!-- Message.vue -->
|
||||
<template>
|
||||
<transition name="fade">
|
||||
<div v-if="visible" class="message" :class="`message--${type}`">
|
||||
<div class="message-content">
|
||||
<span>{{ message }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
message: string
|
||||
type: 'success' | 'error'
|
||||
duration?: number // 可选,自动关闭时长,默认 3000ms
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const visible = ref(true)
|
||||
|
||||
onMounted(() => {
|
||||
const timer = setTimeout(() => {
|
||||
visible.value = false
|
||||
clearTimeout(timer)
|
||||
setTimeout(() => emit('close'), 300) // 动画过渡后emit close
|
||||
}, props.duration || 3000)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.message {
|
||||
position: fixed;
|
||||
top: 10%;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
min-width: 300px;
|
||||
padding: 1.2rem 1.6rem;
|
||||
border-radius: 0.8rem;
|
||||
color: #232323;
|
||||
font-size: 1.4rem;
|
||||
z-index: 2000;
|
||||
transition: all 0.3s;
|
||||
|
||||
&--success {
|
||||
background-color: #f0f9eb;
|
||||
border: 0.1rem solid #e1f3d8;
|
||||
color: #67c23a;
|
||||
}
|
||||
|
||||
&--error {
|
||||
background-color: #fef0f0;
|
||||
border: 0.1rem solid #fde2e2;
|
||||
color: #f56c6c;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
61
src/components/Message/message.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
// message.ts
|
||||
import { createApp } from 'vue'
|
||||
import type { App } from 'vue'
|
||||
import { provide, inject } from 'vue' // 新增导入
|
||||
import type { InjectionKey } from 'vue'
|
||||
|
||||
import Message from './Message.vue'
|
||||
|
||||
interface MessageOptions {
|
||||
message: string
|
||||
type: 'success' | 'error'
|
||||
duration?: number
|
||||
}
|
||||
|
||||
const messageInstances: any[] = [] // 存储多个消息实例,防止重叠
|
||||
|
||||
const showMessage = (options: MessageOptions) => {
|
||||
const app = createApp(Message, {
|
||||
...options,
|
||||
onClose: () => {
|
||||
app.unmount()
|
||||
const index = messageInstances.indexOf(app)
|
||||
if (index > -1) messageInstances.splice(index, 1)
|
||||
container.remove()
|
||||
}
|
||||
})
|
||||
|
||||
const container = document.createElement('div')
|
||||
document.body.appendChild(container)
|
||||
app.mount(container)
|
||||
|
||||
messageInstances.push(app)
|
||||
}
|
||||
|
||||
// 定义 InjectionKey(TypeScript 类型安全)
|
||||
const MessageKey: InjectionKey<{
|
||||
success: (message: string, duration?: number) => void
|
||||
error: (message: string, duration?: number) => void
|
||||
}> = Symbol('message')
|
||||
|
||||
// 插件安装
|
||||
const messagePlugin = {
|
||||
install(app: App) {
|
||||
const message = {
|
||||
success: (message: string, duration?: number) =>
|
||||
showMessage({ message, type: 'success', duration }),
|
||||
error: (message: string, duration?: number) =>
|
||||
showMessage({ message, type: 'error', duration })
|
||||
}
|
||||
app.provide(MessageKey, message) // 使用 provide 注入
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:composable 函数,用于在组件中注入
|
||||
export const useMessage = () => {
|
||||
const message = inject(MessageKey)
|
||||
if (!message) throw new Error('useMessage must be used after messagePlugin is installed')
|
||||
return message
|
||||
}
|
||||
|
||||
export default messagePlugin
|
||||
69
src/directives/Loading.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
const vLoading = {
|
||||
mounted(el, binding) {
|
||||
// 1. 创建遮罩层
|
||||
const mask = document.createElement('div')
|
||||
mask.className = 'custom-loading-mask'
|
||||
mask.innerHTML = '<div class="custom-loading-spinner"></div>'
|
||||
|
||||
// 将 mask 存入 el 方便后续调用
|
||||
el.instance = mask
|
||||
|
||||
// 2. 初始判断
|
||||
if (binding.value) {
|
||||
appendMask(el)
|
||||
}
|
||||
},
|
||||
|
||||
updated(el, binding) {
|
||||
if (binding.value !== binding.oldValue) {
|
||||
if (binding.value) {
|
||||
appendMask(el)
|
||||
} else {
|
||||
removeMask(el)
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
unmounted(el) {
|
||||
removeMask(el)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入遮罩并修正尺寸
|
||||
*/
|
||||
function appendMask(el) {
|
||||
// 添加相对定位类
|
||||
el.classList.add('v-loading-parent--relative')
|
||||
|
||||
// 尺寸优化逻辑:
|
||||
// 如果父元素高度或宽度小于 40px,动态缩小图标
|
||||
const spinner = el.instance.querySelector('.custom-loading-spinner')
|
||||
if (spinner) {
|
||||
const minSize = Math.min(el.offsetWidth, el.offsetHeight)
|
||||
if (minSize > 0 && minSize < 40) {
|
||||
spinner.style.width = '16px'
|
||||
spinner.style.height = '16px'
|
||||
spinner.style.borderWidth = '2px'
|
||||
} else {
|
||||
// 恢复默认尺寸
|
||||
spinner.style.width = '30px'
|
||||
spinner.style.height = '30px'
|
||||
}
|
||||
}
|
||||
|
||||
// 插入 DOM
|
||||
el.appendChild(el.instance)
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除遮罩
|
||||
*/
|
||||
function removeMask(el) {
|
||||
el.classList.remove('v-loading-parent--relative')
|
||||
if (el.instance && el.contains(el.instance)) {
|
||||
el.removeChild(el.instance)
|
||||
}
|
||||
}
|
||||
|
||||
export default vLoading
|
||||
131
src/lang/en.ts
@@ -1,23 +1,34 @@
|
||||
export default {
|
||||
AwardsPage: {
|
||||
submitApplication: 'Submit your Application',
|
||||
applicationDeadline: 'Application Deadline:15th July 2026',
|
||||
beforeStart: 'Application Open Soon...',
|
||||
submitMobile: 'Apply',
|
||||
contactUs: 'Contact Us',
|
||||
contactHeader: "We're here to help",
|
||||
contactDesc:
|
||||
'For questions on eligibility, competition rules,\n judging criteria, or any other enquiries,\n feel free to reach out via our official email.',
|
||||
sendEmail: 'Send Email',
|
||||
copyMailAddress: 'Copy Email Address',
|
||||
copyMail: 'Successfully copied to clipboard!',
|
||||
sendEmailFailed: 'Failed to open email client, we have copied the email to your clipboard.',
|
||||
applicationDeadline: 'Application Deadline:\n15th July 2026',
|
||||
beforeStartDDL: 'Application Peroid:\nMay 15 – July 15, 2026 ',
|
||||
howToApply: 'How to Apply',
|
||||
stepByStep: 'Step by step',
|
||||
step1Title: 'Step 1. Become an\nAiDA Subscriber',
|
||||
step1Title: 'Step 1. Become an \nAiDA Subscriber',
|
||||
step1Desc:
|
||||
'All applicants must be active\nAiDA subscribers at the time of\nsubmission. You may subscribe\nunder either a monthly or yearly plan.',
|
||||
'All applicants must be active \nAiDA subscribers at the time of \nsubmission. You may subscribe\nunder either a monthly or yearly plan.',
|
||||
step2Title: 'Step 2. Create Your Design Using AiDA',
|
||||
step2Desc: 'Applicants must create their\ndesigns exclusively using the\nAiDA platform. ',
|
||||
step2Desc: 'Applicants must create their \ndesigns exclusively using the \nAiDA platform. ',
|
||||
step2ListTitle: 'Your work should clearly demonstrate:',
|
||||
step2List: [
|
||||
'· How AiDA is used as a creative tool',
|
||||
'· Your design concept and creative direction',
|
||||
'· The intergration of AI and human creativity'
|
||||
'How AiDA is used as a creative tool',
|
||||
'Your design concept and creative direction',
|
||||
'The integration of AI and human creativity'
|
||||
],
|
||||
step3Title: 'Step 3. Prepare Your Submission',
|
||||
processVideo: 'Process Video',
|
||||
processVideoDesc: 'Include a screen‑recorded video\nyour creative process\nusing AiDA.',
|
||||
processVideoDesc: 'Include a screen‑recorded video of your creative process using AiDA.',
|
||||
videoRequirements: 'Video requirements:',
|
||||
videoFormat: 'Format: MP4',
|
||||
videoResolution: 'Resolution: 1080×1920 px',
|
||||
@@ -27,6 +38,7 @@ export default {
|
||||
fileNameDesc: 'AiDAGlobalDesignAward\n2026_[Your Full Name]',
|
||||
designPortfolio: 'Design Portfolio(PDF)',
|
||||
submitPdf: 'Submit one single PDF file that includes:',
|
||||
designCollection:'A design collection of 4 outfits',
|
||||
requiredStructure: 'Required structure:',
|
||||
pdfDesignTitle: 'Design title',
|
||||
pdfMoodboard: 'Moodboard',
|
||||
@@ -42,19 +54,19 @@ export default {
|
||||
'The 20 finalists will be required to\nsubmit physical garments for final\nevaluation',
|
||||
finalistPieces: 'Number of pieces: 1 full outfit',
|
||||
finalistBasedOn: 'Garments must be produced\nbased on the submitted\nAiDA-generated designs',
|
||||
finalistShipping: 'Shipping instructions will be provided by\nCode-create',
|
||||
finalistShipping: 'Shipping instructions will be provided by\nCode-Create',
|
||||
bloomYourCreativity: 'Bloom Your Creativity',
|
||||
themeOf2026: 'Theme of 2026',
|
||||
bloomText: {
|
||||
desc1: {
|
||||
regular1: 'The',
|
||||
bold1: 'AiDA Global Design Award 2026',
|
||||
bold1: ' AiDA Global Design Award 2026 ',
|
||||
regular2: 'is an ',
|
||||
bold2: 'international design competition ',
|
||||
regular3: 'hosted by ',
|
||||
bold3: 'Code-create ',
|
||||
bold3: 'Code-Create',
|
||||
regular4: ', a globally leading\n',
|
||||
bold4: 'AI fashion solutions provider,',
|
||||
bold4: 'AI fashion solutions provider, ',
|
||||
regular5:
|
||||
'celebrating the future of creativity powered by artificial intelligence.\nBringing together designers from around the world, AiDA empowers AI as a creative partner—pushing fashion beyond traditional boundaries and unlocking new possibilities where technology amplifies human imagination.'
|
||||
},
|
||||
@@ -65,20 +77,27 @@ export default {
|
||||
'participants are invited to transform bold ideas into extraordinary designs, seamlessly merging human artistry with artificial intelligence to shape the next era of fashion.'
|
||||
}
|
||||
},
|
||||
bloomDesc1:
|
||||
'The AiDA Global Design Award 2026 is an\ninternational design competition hosted by\nCode‑Create, a globally leading AI fashion solutions provider,\ncelebrating the future of creativity powered by artificial intelligence.\nBringing together designers from around the world, AiDA empowers AI as a creative partner—pushing fashion beyond traditional boundaries and unlocking new possibilities where technology amplifies human imagination.',
|
||||
bloomDesc1: `The <span class="arial-bold">AiDA Global Design Award 2026</span> is an <span class="arial-bold">international design competition</span> hosted by <span class="arial-bold">Code‑Create</span>,\n a globally leading <span class="arial-bold">AI fashion solutions provider</span>, celebrating the future of creativity powered by artificial\nintelligence.Bringing together designers from around the world, AiDA empowers AI as a creative partner\n — pushing fashion beyond traditional boundaries and unlocking new possibilities where technology\namplifies human imagination.`,
|
||||
bloomDesc2:
|
||||
'Under the theme “Where Imagination Meets Innovation, Creativity Blooms,” participants are invited to transform bold ideas into extraordinary designs, seamlessly merging human artistry with artificial intelligence to shape the next era of fashion.',
|
||||
'Under the theme <span class="arial-bold">“Where Imagination Meets Innovation, Creativity Blooms,”</span> participants are invited \n to transform bold ideas into extraordinary designs, seamlessly merging human artistry with artificial \n intelligence to shape the next era of fashion.',
|
||||
panelOfJudges: 'Panel of Judges',
|
||||
expertise: 'Expertise',
|
||||
judgesHat: {
|
||||
jae: 'Code-create\nKorea Branch Director\nBesfxxk creative director',
|
||||
diego: 'Co-founder & Chief Father\nOfficer of OnTheList\n(Hong Kong)',
|
||||
gregory: 'Senior Designer at\nGabriela Heasrst (Italy)',
|
||||
vincenzo: 'Cheif Editor of SCMP Style\n(Hong Kong)',
|
||||
tim: 'Group Fashion Direction of\n Modern Media Group\n(Shanghai)',
|
||||
desmond: 'Cheif Editor of Vogue\n(Singapore)'
|
||||
jae: 'Director, Code-Create Korea Creative Director, Besfxxk Former Stylistic Consultant Max Mara & Corelate',
|
||||
jaeCountry: '(Korea)',
|
||||
rosie: 'Associate Merchandising Director, Galeries Lafayette China',
|
||||
rosieCountry: '(Mainland China)',
|
||||
gregory:
|
||||
'Senior Designer, Gabriela Hearst Former Menswear Head Designer Bottega Veneta & Burberry',
|
||||
gregoryCountry: '(Italy)',
|
||||
annie: 'Chief Operating Officer,\nChow Tai Fook Jewellery Group',
|
||||
annieCountry: '(Hong Kong, China)',
|
||||
xaven: 'General Manager & Editorial Director,\nMadame Figaro Hong Kong',
|
||||
xavenCountry: '(Hong Kong, China)',
|
||||
desmond: 'Editor-in-Chief, Vogue Singapore',
|
||||
desmondCountry: '(Singapore)'
|
||||
},
|
||||
tbd: 'To be determined',
|
||||
awardPrizes: 'Award & Prizes',
|
||||
recognition: 'Recognition',
|
||||
grandMoney: 'US$5,000',
|
||||
@@ -105,7 +124,7 @@ export default {
|
||||
executionDesc: 'Quality of presentation and\ntechnical craftsmanship',
|
||||
totalCashPrizes: 'UP TO\nUS$9000',
|
||||
totalCashPrizesLabel: 'In total cash prizes',
|
||||
globalMediaExpose: 'GLOBAL MEDIA\nEXPOSE',
|
||||
globalMediaExpose: 'GLOBAL MEDIA\nEXPOSURE',
|
||||
globalMediaExposeLabel: 'Showcased by top\ninternational media platforms',
|
||||
networkingOpportunities: 'NETWORKING\nOPPORTUNITIES',
|
||||
networkingOpportunitiesLabel: 'Build connections with\ndesigners and industry leaders',
|
||||
@@ -116,43 +135,46 @@ export default {
|
||||
timelineApplicationLabel: 'Application',
|
||||
timelineDeadlineLabel: 'Deadline',
|
||||
timeJul15: 'Jul 15',
|
||||
applicationDeadlineDesc: 'Application deadline and\nentry review process\nbegins.',
|
||||
twentyFinalistsAnnounced: '20 Finallists',
|
||||
announcedLabel: 'Announced',
|
||||
applicationDeadlineDesc: 'Applications close and\nreview begins',
|
||||
twentyFinalistsAnnounced: 'Announcement of',
|
||||
announcedLabel: '20 Finalists',
|
||||
timeAug30: 'Aug 30',
|
||||
twentyFinalistsDesc: 'Announcement of 20\nfinalists entering final\nevaluation stage.',
|
||||
finalistSubmission: 'Finallist\nSubmission',
|
||||
twentyFinalistsDesc: '20 finalists selected for\nfinal evaluation.',
|
||||
finalistSubmission: 'Finalist\nSubmission',
|
||||
submissionLabel: 'Deadline',
|
||||
timeSept30: 'Sept 30',
|
||||
finalistSubmissionDesc: 'Finalists submit\ncompleted outfits for\nfinal assessment.',
|
||||
receivingOutfits: 'Receiving Outfits',
|
||||
fromFinalistsLabel: 'from Finallists',
|
||||
timeOctober: 'October',
|
||||
receivingOutfitsDesc: 'AiDA receives physical\noutfits from all 20\nfinalists.',
|
||||
finalistSubmissionDesc:
|
||||
'Finalists submit entries\nand ship garments to\nCode-Create',
|
||||
receivingOutfits: 'Outfits Delivery',
|
||||
fromFinalistsLabel: 'Deadline',
|
||||
timeOctober: 'Oct 23',
|
||||
receivingOutfitsDesc: 'Code-Create confirms receipt of all garments',
|
||||
awardCeremony: 'Award',
|
||||
ceremonyLabel: 'Ceremony',
|
||||
timeNov12: 'Nov 12',
|
||||
awardCeremonyDesc: 'Award Ceremony &\nCommunity Gathering\n– Soho House.',
|
||||
awardCeremonyDesc: 'Ceremony and gathering at Soho House, Hong Kong',
|
||||
submissionSuccessful: 'Submission Successful',
|
||||
submissionSuccessfulDesc:
|
||||
'Please review your submitted information in the AiDA in-platform message.\nYou may edit it if needed. Competition updates and results will be sent via email.',
|
||||
deadlinePassed: 'Application Deadline Passed',
|
||||
deadlinePassedDesc:
|
||||
'The submission deadline for AiDA Global Fashion Award 2026 has ended.\nWe are no longer accepting new applications.',
|
||||
'The submission deadline for AiDA Global Design Award 2026 has ended.\nWe are no longer accepting new applications.',
|
||||
uploadInProgress: 'Upload in progress…',
|
||||
uploadSuccess: 'Uploaded Successfully',
|
||||
uploadFailed: 'Upload failed',
|
||||
pdfFileTip: 'PDF file, max 20MB',
|
||||
videoFileTip: 'Video file (MP4, MOV), 1080p, max 100MB',
|
||||
videoFileTip: 'Video file (MP4), 1080p, max 100MB',
|
||||
wechatTitle: 'WeChat Official Account',
|
||||
wechatDesc: 'Scan the QR code in WeChat'
|
||||
wechatDesc: 'Scan the QR code in WeChat',
|
||||
copyRight: '© 2026 Code-Create. All rights reserved.',
|
||||
copyRightTips: '*All information is subject to change by Code-Create.'
|
||||
},
|
||||
AwardApply: {
|
||||
// 页面主标题区域
|
||||
applicationForm: 'Application Form',
|
||||
emailVerification: 'Email Verification',
|
||||
aidaUsersOnly: 'AiDA Users Only',
|
||||
slogan: 'BLOOM YOUR CREATIVITY • AIDA GLOBAL DESIGN AWARDS 2026',
|
||||
slogan: 'BLOOM YOUR CREATIVITY • AiDA GLOBAL DESIGN AWARDS 2026',
|
||||
// 邮箱验证部分
|
||||
emailAddress: 'Email Address',
|
||||
sendCode: 'Send Code',
|
||||
@@ -183,19 +205,20 @@ export default {
|
||||
submissionFiles: 'Submission Files',
|
||||
uploadYourDesignMaterials: 'Upload your design materials',
|
||||
submissionRequirements: 'Submission Requirements',
|
||||
pdfRequirement: `Single PDF file\n Title, mood board, elaboration\n+ 4 outfit design with materials (max 15 pages)`,
|
||||
pdfRequirement: `Single PDF file\n Title, mood board, elaboration\n+ a design collection of 4 outfits (max 15 pages)`,
|
||||
rightContent: {
|
||||
format: 'Format: Single PDF file, 15 pages, maximum 20MB',
|
||||
video: `Video: Design process, 1080×1920 pixels (9:16 ratio), maximum 60 seconds`
|
||||
},
|
||||
// PDF 上传
|
||||
uploadPdfTitle: 'How will you use AiDA in your design process?',
|
||||
uploadPdfTitle:
|
||||
'Please submit a single PDF with your design topic, mood board, and AiDA concept statement.',
|
||||
clickToUploadPdf: 'Click to upload or drag and drop',
|
||||
pdfFileLimit: 'PDF file, max 20MB',
|
||||
// 视频上传
|
||||
uploadVideoTitle: 'How will you use AiDA in your design process?',
|
||||
uploadVideoTitle: 'Please submit a video showcasing your creative process using AiDA.',
|
||||
clickToUploadVideo: 'Click to upload or drag and drop',
|
||||
videoFileLimit: 'Video file (MP4, MOV), 1080p, max 100MB',
|
||||
videoFileLimit: 'Video file (MP4), 1080p, max 100MB',
|
||||
// 条款与条件
|
||||
termsAndConditions: 'Terms & Conditions',
|
||||
conditionFirst: 'I confirm that all submitted work is original and created by me.',
|
||||
@@ -212,6 +235,12 @@ export default {
|
||||
checkYourEmail: 'Check your email',
|
||||
enterSixDigitCode: 'Enter the 6-digit code sent to',
|
||||
verify: 'Verify',
|
||||
verifyCode: 'Verify Code',
|
||||
verifyPlaceholder: 'enter 6-digital code',
|
||||
stepTabVerify: 'Verify',
|
||||
stepTabProfile: 'Profile',
|
||||
stepTabDesign: 'Design Material',
|
||||
stepTabTerms: 'Terms',
|
||||
resendCode: 'Resend',
|
||||
resendCodeIn: 'Resend Code in',
|
||||
// 验证消息
|
||||
@@ -240,9 +269,25 @@ export default {
|
||||
pleaseUploadPdf: 'Please upload your PDF',
|
||||
pleaseUploadVideo: 'Please upload your video',
|
||||
uploadPdfOnly: 'Please upload a PDF file only.',
|
||||
uploadVideoOnly: 'Please upload a MP4 or MOV file only.',
|
||||
uploadVideoOnly: 'Please upload a MP4 file only.',
|
||||
fileSizeExceeds: 'File size exceeds {sizeLimit} limit. Please upload a smaller file.',
|
||||
videoDurationExceeds: 'Video duration exceeds 60 seconds limit. Please upload a shorter video.',
|
||||
uploadFailed: 'Upload failed'
|
||||
uploadFailed: 'Upload failed',
|
||||
nextStep: 'Next Step',
|
||||
stepTips: 'Please complete this form in one sitting.',
|
||||
backToIntroduction: 'Back to Introduction'
|
||||
},
|
||||
Preview: {
|
||||
title: 'Global Awards Preview',
|
||||
total: 'Total Applications Submitted',
|
||||
downloadExcel: 'Download Full Information Table (Excel)',
|
||||
range: 'Set the download range',
|
||||
startIndex: 'start ID',
|
||||
endIndex: 'end ID',
|
||||
download: 'Download',
|
||||
limit: 'Maximum 10 entries can be downloaded at once',
|
||||
minIndex: 'Minimum ID is 10000',
|
||||
maxIndex: 'Maximum ID is {maxIndex}',
|
||||
indexError: 'End ID must be greater than or equal to Start ID'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,26 @@ import { createI18n } from 'vue-i18n'
|
||||
import enLocale from './en'
|
||||
import zhLocale from './zh-cn'
|
||||
|
||||
export type AppLocale = 'ENGLISH' | 'CHINESE_SIMPLIFIED'
|
||||
|
||||
const DEFAULT_LOCALE: AppLocale = 'ENGLISH'
|
||||
const LANG_PREFIX_RE = /^\/(cn|en)(?=\/|$)/
|
||||
const CN_PREFIX_RE = /^\/cn(?=\/|$)/
|
||||
|
||||
export const getLocaleByPath = (path = ''): AppLocale => {
|
||||
return CN_PREFIX_RE.test(path) ? 'CHINESE_SIMPLIFIED' : DEFAULT_LOCALE
|
||||
}
|
||||
|
||||
export const getPathByLocale = (path: string, locale: AppLocale) => {
|
||||
const normalizedPath = path.replace(LANG_PREFIX_RE, '') || '/'
|
||||
|
||||
if (locale === 'CHINESE_SIMPLIFIED') {
|
||||
return normalizedPath === '/' ? '/cn' : `/cn${normalizedPath}`
|
||||
}
|
||||
|
||||
return normalizedPath
|
||||
}
|
||||
|
||||
// 语言配置整合
|
||||
const messages = {
|
||||
ENGLISH: {
|
||||
@@ -20,7 +40,7 @@ const messages = {
|
||||
const i18n = createI18n({
|
||||
legacy: false,
|
||||
globalInjection: true, // 全局模式,可以直接使用 $t
|
||||
locale: 'ENGLISH',
|
||||
locale: getLocaleByPath(typeof window === 'undefined' ? '/' : window.location.pathname),
|
||||
messages: messages
|
||||
})
|
||||
|
||||
|
||||
@@ -1,15 +1,26 @@
|
||||
export default {
|
||||
AwardsPage: {
|
||||
submitApplication: '提交申请',
|
||||
applicationDeadline: '申请期限:2026年7月15日',
|
||||
beforeStart: '申请开启在即...',
|
||||
submitMobile: '申请',
|
||||
contactUs: '联系我们',
|
||||
contactHeader: '有关比赛,欢迎咨询',
|
||||
contactDesc:
|
||||
'无论是报名资格、参赛细则、评审标准,还是其他比赛相关问题,欢迎通过以下邮箱联络我们:',
|
||||
sendEmail: '发送邮件',
|
||||
copyMail: '已成功复制到剪贴板',
|
||||
copyMailAddress: '复制邮件地址',
|
||||
sendEmailFailed: '无法打开邮件客户端,我们已将邮箱地址复制到您的剪贴板。',
|
||||
applicationDeadline: '申请期限:\n2026年7月15日',
|
||||
beforeStartDDL: '开放申请:\n2026.05.15 – 07.15',
|
||||
howToApply: '申请方法',
|
||||
stepByStep: '步骤指南',
|
||||
step1Title: '1. 成为 AiDA 订阅用户',
|
||||
step1Desc: '所有申请者在提交时必须是\n活跃的AiDA 订阅用户。\n您可以选择按月或按年订阅。',
|
||||
step1Desc: '所有申请者在提交时必须是\n活跃的AiDA订阅用户。\n您可以选择按月或按年订阅。',
|
||||
step2Title: '2. 通过 AiDA 设计您的作品',
|
||||
step2Desc: '申请者必须仅使用AiDA\n平台完成设计作品。',
|
||||
step2Desc: '申请者必须仅使用AiDA\n平台完成设计作品。',
|
||||
step2ListTitle: '您的作品应清楚体现以下内容:',
|
||||
step2List: ['· AiDA在创作中的应用方式', '· 您的设计理念和创意方向', '· AI与人类创意的融合'],
|
||||
step2List: ['AiDA在创作中的应用方式', '您的设计理念和创意方向', 'AI与人类创意的融合'],
|
||||
step3Title: '3. 准备提交材料',
|
||||
processVideo: '创作过程视频',
|
||||
processVideoDesc: '请提供一段屏幕录制视频,展示您\n使用AiDA的创作过程。',
|
||||
@@ -22,6 +33,7 @@ export default {
|
||||
fileNameDesc: 'AiDAGlobalDesignAward\n2026_[你的名字]',
|
||||
designPortfolio: '设计作品集(PDF)',
|
||||
submitPdf: '提交一份包含以下内容的单一PDF文件:',
|
||||
designCollection: '一个包含4套服装的系列',
|
||||
requiredStructure: '',
|
||||
pdfDesignTitle: '设计标题',
|
||||
pdfMoodboard: '灵感板,情绪板',
|
||||
@@ -31,12 +43,12 @@ export default {
|
||||
pdfMaxPages: '最多15页',
|
||||
pdfMaxSize: '最大文件大小:不超过20MB',
|
||||
pdfLanguage: '语言:英文,或本国语言附带英文翻译',
|
||||
step4Title: '4. 决赛入围选手提交要求',
|
||||
step4Title: '4. 入围决赛选手提交要求',
|
||||
step4Subtitle: '(前20名设计师)',
|
||||
step4Desc: '入围的20名决赛选手需提交实体服装以供最终评审。',
|
||||
step4Desc: '20名入围决赛选手需提交实体服装以供最终评审。',
|
||||
finalistPieces: '件数:1件套装',
|
||||
finalistBasedOn: '服装要求:必须根据提交的AiDA生成设计制作',
|
||||
finalistShipping: '运输说明:\n将由Code-create提供',
|
||||
finalistShipping: '运输说明由Code-Create提供',
|
||||
bloomYourCreativity: '绽放你的创造力',
|
||||
themeOf2026: '赛事主题',
|
||||
bloomText: {
|
||||
@@ -44,11 +56,11 @@ export default {
|
||||
regular1: '',
|
||||
bold1: 'AiDA全球设计奖2026',
|
||||
regular2: '是由全球领先的AI时尚解决方案提供商',
|
||||
bold2: ' Code-create ',
|
||||
bold2: ' Code-Create ',
|
||||
regular3: '主办的',
|
||||
bold3: '国际设计竞赛,\n',
|
||||
regular4:
|
||||
'旨在庆祝人工只能赋能下的未来创意。该赛事汇聚来自世界各地的设计师,\n将AI视为创意伙伴,突破传统时尚边界,探索技术与人类想象力结合的无限可能。',
|
||||
'旨在庆祝人工只能赋能下的未来创意。该赛事汇聚来自世界各地的设计师,\n将AI视为创意伙伴,突破传统时尚边界,探索技术与人类想象力结合的无限可能。',
|
||||
bold4: '',
|
||||
regular5: ''
|
||||
},
|
||||
@@ -59,16 +71,26 @@ export default {
|
||||
'为主题,邀请参赛者将大胆创意转化为非凡设计,\n在 AI 辅助下实现艺术与科技的完美融合。AiDA 鼓励设计师突破常规,挑战时尚边界,\n并通过平台展示才华,与全球同行、行业领袖及 AI 专家建立深度联系,共同探索未来设计的可能。'
|
||||
}
|
||||
},
|
||||
bloomDesc1: `AiDA<span class="arial-bold">全球设计奖2026</span>是由全球领先的<span class="arial-bold">AI时尚解决方案提供商Code-Create</span>主办的<span class="arial-bold">国际设计竞赛</span>,\n旨在庆祝人工智能赋能下的未来创意。该赛事汇聚来自世界各地的设计师,将<span class="arial-bold">AI视为创意伙伴</span>,\n突破传统时尚边界,探索技术与人类想象力结合的无限可能。`,
|
||||
bloomDesc2:
|
||||
'本届大赛以"想象遇见创新,创意绽放"为主题,邀请参赛者将大胆创意转化为非凡设计,\n在 AI 辅助下实现艺术与科技的完美融合,共同探索未来设计的可能。',
|
||||
panelOfJudges: '终审评委团',
|
||||
expertise: '权威阵容',
|
||||
judgesHat: {
|
||||
jae: 'Code‑Create 韩国分公司总监\nBesfxxk 创意总监',
|
||||
diego: 'OnTheList(香港)\n联合创始人兼首席执行官',
|
||||
gregory: 'Gabriela Hearst\n(意大利)高级设计师',
|
||||
vincenzo: '《南华早报》Style 杂志\n(香港)主编',
|
||||
tim: '现代传播集团\n(上海)时尚总监',
|
||||
desmond: '《Vogue》\n(新加坡)主编'
|
||||
jae: 'Code-Create 韩国负责人\nBesfxxk 创意总监\nMax Mara & Corelate前造型顾问',
|
||||
jaeCountry: '(韩国)',
|
||||
rosie: '老佛爷百货公司商品部副总监',
|
||||
rosieCountry: '(中国内地)',
|
||||
gregory: 'Gabriela Hearst 高级设计师\nBottega Veneta & Burberry\n前男装设计负责人',
|
||||
gregoryCountry: '(意大利)',
|
||||
annie: '周大福珠宝集团首席运营官',
|
||||
annieCountry: '(中国香港)',
|
||||
xaven: '《Madame Figaro 香港版》总经理兼编辑总监',
|
||||
xavenCountry: '(中国香港)',
|
||||
desmond: '《Vogue 新加坡版》主编',
|
||||
desmondCountry: '(新加坡)'
|
||||
},
|
||||
tbd: '待定',
|
||||
awardPrizes: '奖项与奖金',
|
||||
recognition: '荣誉认可',
|
||||
grandMoney: '5,000美元',
|
||||
@@ -77,12 +99,12 @@ export default {
|
||||
grandAwards: '最高奖项',
|
||||
goldAwards: '金奖',
|
||||
silverAwards: '银奖',
|
||||
finalists: '决赛选手',
|
||||
finalists: '入围决赛选手',
|
||||
cashAward: '现金奖励',
|
||||
awardCertificate: '获奖证书',
|
||||
globalMediaExposure: '全球媒体曝光',
|
||||
awardCertification: '获奖认证',
|
||||
TravelAllowance: '差旅补贴',
|
||||
TravelAllowance: '差旅津贴',
|
||||
selectionCriteria: '作品评选',
|
||||
evaluation: '考量标准',
|
||||
originality: '原创性',
|
||||
@@ -91,7 +113,7 @@ export default {
|
||||
creativityDesc: '作品应展现设计师的艺术视野与卓越设计水准,体现高水平的创意表达与专业执行力。',
|
||||
aidaIntegration: 'AiDA 创意整合程度',
|
||||
aidaIntegrationDesc:
|
||||
'作品应充分利用 AiDA 功能, 展现 AI 辅助创作在设计中的 有效应用与创新整合。',
|
||||
'作品应充分利用 AiDA 功能, 展现 AI 辅助创作在设计中的有效应用与创新整合。',
|
||||
execution: '样衣做工',
|
||||
executionDesc: '作品应具备高水平的呈现质量与精湛的技术工艺,体现专业执行力与细节把控能力。',
|
||||
totalCashPrizes: '最高可达9,000美元',
|
||||
@@ -101,29 +123,30 @@ export default {
|
||||
networkingOpportunities: '链接全球行业人脉',
|
||||
networkingOpportunitiesLabel: '对接设计师与行业领军人物',
|
||||
awardCeremonyHongKong: '香港颁奖盛会',
|
||||
awardCeremonyLabel: '入围者享有差旅支持',
|
||||
awardCeremonyLabel: '入围决赛选手享有差旅津贴',
|
||||
competitionTimeline: '赛事时间表',
|
||||
shapingTheFuture: '重要节点',
|
||||
timelineApplicationLabel: '申请期限',
|
||||
timelineDeadlineLabel: '',
|
||||
timeJul15: '7月15日',
|
||||
applicationDeadlineDesc: '申请截止日期及\n作品审核流程开始',
|
||||
twentyFinalistsAnnounced: '20名入围者揭晓',
|
||||
announcedLabel: '',
|
||||
applicationDeadlineDesc: '申请截止并开始评审',
|
||||
twentyFinalistsAnnounced: '20名入围决赛',
|
||||
announcedLabel: '选手揭晓',
|
||||
timeAug30: '8月30日',
|
||||
twentyFinalistsDesc: '公布进入终评阶段的 20 名入围者',
|
||||
finalistSubmission: '入围设计作品',
|
||||
twentyFinalistsDesc: '20名入围者被选入\n最终评审阶段',
|
||||
finalistSubmission: '入围作品',
|
||||
submissionLabel: '提交最后期限',
|
||||
timeSept30: '9月30日',
|
||||
finalistSubmissionDesc: '入围者上传完成的设计\n作品以进行终评',
|
||||
receivingOutfits: '入围者',
|
||||
fromFinalistsLabel: '提交成衣',
|
||||
timeOctober: '10月',
|
||||
receivingOutfitsDesc: 'AiDA 接收每位入围\n的1套实物服装',
|
||||
finalistSubmissionDesc:
|
||||
'入围者提交作品并将服装寄送至Code-Create',
|
||||
receivingOutfits: '收到成衣期限',
|
||||
fromFinalistsLabel: '',
|
||||
timeOctober: '10月23日',
|
||||
receivingOutfitsDesc: 'Code-Create确认收到\n所有服装作品',
|
||||
awardCeremony: '奖项颁发仪式',
|
||||
ceremonyLabel: '',
|
||||
timeNov12: '11月12日',
|
||||
awardCeremonyDesc: '颁奖盛典与设计师社\n群聚会 – Soho House',
|
||||
awardCeremonyDesc: '颁奖盛典及交流活动于\n香港 Soho House 举行',
|
||||
submissionSuccessful: '提交成功',
|
||||
submissionSuccessfulDesc:
|
||||
'请在 AiDA 平台内的消息中查看您提交的信息。如有需要,您可以进行修改。\n比赛的最新消息和结果将通过邮箱发送。',
|
||||
@@ -133,9 +156,11 @@ export default {
|
||||
uploadSuccess: '上传成功',
|
||||
uploadFailed: '上传失败',
|
||||
pdfFileTip: 'PDF文件,不超过20MB',
|
||||
videoFileTip: '视频文件(MP4, MOV),1080p,不超过100MB',
|
||||
videoFileTip: '视频文件(MP4),1080p,不超过100MB',
|
||||
wechatTitle: '微信公众号',
|
||||
wechatDesc: '请使用微信扫描二维码'
|
||||
wechatDesc: '请使用微信扫描二维码',
|
||||
copyRight: '© 2026 Code-Create | 保留所有权利',
|
||||
copyRightTips: '*所有信息如有变动,请以官方最新公布为准。'
|
||||
},
|
||||
AwardApply: {
|
||||
// 页面主标题区域
|
||||
@@ -172,19 +197,19 @@ export default {
|
||||
submissionFiles: '作品上传',
|
||||
uploadYourDesignMaterials: '上传你的设计材料',
|
||||
submissionRequirements: '提交要求',
|
||||
pdfRequirement: `单独PDF文件\n 作品标题、灵感板及情绪板,设计说明\n+ 4套服装设计及材料说明(页数:最多15页)`,
|
||||
pdfRequirement: `单独PDF文件\n 作品标题、灵感板及情绪板,设计说明\n+ 一个包含4套服装的系列(页数:最多15页)`,
|
||||
rightContent: {
|
||||
format: '格式:单个 PDF 文件,最多15页,最大 20MB',
|
||||
video: `视频:创作过程,分辨率1080×1920 像素\n(9:16 纵向比例),最长 60 秒`
|
||||
},
|
||||
// PDF 上传
|
||||
uploadPdfTitle: '您在设计过程中如何使用 AiDA?',
|
||||
uploadPdfTitle: '请提供一份包含设计主题、灵感情绪板及 AiDA 创作概念说明的单一 PDF 文件。',
|
||||
clickToUploadPdf: '点击选择或拖拽文件上传',
|
||||
pdfFileLimit: 'PDF 文件,不超过 20MB',
|
||||
// 视频上传
|
||||
uploadVideoTitle: '您在设计过程中如何使用 AiDA?',
|
||||
uploadVideoTitle: '请提供一段屏幕录制视频,展示您使用 AiDA 的创作过程。',
|
||||
clickToUploadVideo: '点击选择或拖拽文件上传',
|
||||
videoFileLimit: '视频文件(MP4, MOV),1080P,不超过100MB',
|
||||
videoFileLimit: '视频文件(MP4),1080P,不超过100MB',
|
||||
// 条款与条件
|
||||
termsAndConditions: '参赛条款',
|
||||
conditionFirst: '我确认所提交的作品均为原创,且由我本人独立创作。',
|
||||
@@ -198,6 +223,12 @@ export default {
|
||||
checkYourEmail: '请查看您的邮箱',
|
||||
enterSixDigitCode: '请输入发送到邮箱的 6 位验证码',
|
||||
verify: '验证',
|
||||
verifyCode: '验证码',
|
||||
verifyPlaceholder: '输入6位数字的验证码',
|
||||
stepTabVerify: '验证',
|
||||
stepTabProfile: '资料',
|
||||
stepTabDesign: '设计材料',
|
||||
stepTabTerms: '条款',
|
||||
resendCode: '重新发送',
|
||||
resendCodeIn: '重新发送',
|
||||
// 验证消息
|
||||
@@ -226,9 +257,25 @@ export default {
|
||||
pleaseUploadPdf: '请上传您的PDF文件',
|
||||
pleaseUploadVideo: '请上传您的视频文件',
|
||||
uploadPdfOnly: '请仅上传 PDF 文件。',
|
||||
uploadVideoOnly: '请仅上传 MP4 或 MOV 文件。',
|
||||
uploadVideoOnly: '请仅上传 MP4 文件。',
|
||||
fileSizeExceeds: '文件大小超过 {sizeLimit} 限制。请上传较小的文件。',
|
||||
videoDurationExceeds: '视频时长不可超过60秒',
|
||||
uploadFailed: '上传失败'
|
||||
uploadFailed: '上传失败',
|
||||
nextStep: '下一步',
|
||||
stepTips: '请一次性完成这个表单。',
|
||||
backToIntroduction: '赛事介绍'
|
||||
},
|
||||
Preview: {
|
||||
title: 'Global Awards 数据总览',
|
||||
total: '已提交申请总数',
|
||||
downloadExcel: '下载全量信息表 (Excel)',
|
||||
range: '设定下载区间',
|
||||
startIndex: '起始序号',
|
||||
endIndex: '结束序号',
|
||||
download: '下载',
|
||||
limit: '一次最多下载10条数据',
|
||||
minIndex: '最小序号为10000',
|
||||
maxIndex: '最大序号为 {maxIndex}',
|
||||
indexError: '结束序号必须大于或等于起始序号'
|
||||
}
|
||||
}
|
||||
|
||||
23
src/main.ts
@@ -5,22 +5,19 @@ import router from './router'
|
||||
import store from './stores/index'
|
||||
import 'normalize.css'
|
||||
import './assets/css/style.css'
|
||||
import SvgIcon from "@/components/SvgIcon/index.vue";
|
||||
import "virtual:svg-icons-register";
|
||||
import SvgIcon from '@/components/SvgIcon/index.vue'
|
||||
import 'virtual:svg-icons-register'
|
||||
import messagePlugin from './components/Message/message'
|
||||
import vLoading from '@/directives/Loading'
|
||||
|
||||
import i18n from "./lang/index";
|
||||
import i18n from './lang/index'
|
||||
|
||||
import flexible from "./utils/flexible.js";
|
||||
import flexible from './utils/flexible.js'
|
||||
|
||||
import "./router/router-config" // 路由守卫,做动态路由的地方
|
||||
import './router/router-config' // 路由守卫,做动态路由的地方
|
||||
|
||||
const app = createApp(App)
|
||||
app.use(router)
|
||||
.use(store)
|
||||
.component("SvgIcon", SvgIcon)
|
||||
.use(i18n)
|
||||
.mount('#app')
|
||||
|
||||
|
||||
flexible();
|
||||
app.directive('loading', vLoading)
|
||||
app.use(router).use(store).component('SvgIcon', SvgIcon).use(i18n).use(messagePlugin).mount('#app')
|
||||
|
||||
flexible()
|
||||
|
||||
@@ -10,24 +10,30 @@ const router = createRouter({
|
||||
history: createWebHistory('/'),
|
||||
// history: createWebHistory(import.meta.env.VITE_APP_URL),
|
||||
routes: [
|
||||
// {
|
||||
// path: '/',
|
||||
// },
|
||||
{
|
||||
path: '/',
|
||||
name:'award',
|
||||
component:()=>import('@/views/AwardPage/container.vue'),
|
||||
children:[
|
||||
path: '/:lang?',
|
||||
component: () => import('@/views/AwardPage/container.vue'),
|
||||
children: [
|
||||
{
|
||||
path:'',
|
||||
name:'AwardIndex',
|
||||
component:()=>import('@/views/AwardPage/index.vue'),
|
||||
path: '',
|
||||
name: 'AwardIndex',
|
||||
component: () => import('@/views/AwardPage/index.vue')
|
||||
},
|
||||
{
|
||||
path:'contestants',
|
||||
component:()=>import('@/views/AwardPage/apply.vue')
|
||||
path: 'contestants',
|
||||
name: 'Contestants',
|
||||
component: () => import('@/views/AwardPage/contestants.vue')
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
path: '/preview',
|
||||
name: 'Preview',
|
||||
component: () => import('@/views/Preview/index.vue')
|
||||
},
|
||||
{
|
||||
path: '/:pathMatch(.*)',
|
||||
name: '404',
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import i18n, { getLocaleByPath } from '@/lang'
|
||||
import { trackRouteEntry } from '@/utils/tracking'
|
||||
import router from './index'
|
||||
|
||||
router.beforeEach((to, from, next) => {
|
||||
next()
|
||||
router.beforeEach((to, _from, next) => {
|
||||
i18n.global.locale.value = getLocaleByPath(to.path)
|
||||
next()
|
||||
})
|
||||
|
||||
router.afterEach(() => {
|
||||
trackRouteEntry()
|
||||
})
|
||||
|
||||
@@ -30,9 +30,9 @@ let flexible = (designWidth, maxWidth, minWidth) => {
|
||||
width < minWidth && (width = minWidth)
|
||||
designWidth = oldDesignWidth
|
||||
} else {
|
||||
designWidth = 393
|
||||
designWidth = 750
|
||||
}
|
||||
console.log(width, designWidth)
|
||||
// console.log(width, designWidth)
|
||||
|
||||
// var rem = width * 10 / designWidth;
|
||||
var rem = Math.round((width * 10) / designWidth)
|
||||
|
||||
97
src/utils/isMobile.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { ref, onMounted, onUnmounted, onActivated, onDeactivated } from 'vue'
|
||||
|
||||
export const useIsMobile = () => {
|
||||
const isMobile = ref(false)
|
||||
|
||||
let resizeTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const checkDevice = () => {
|
||||
if (resizeTimer) {
|
||||
clearTimeout(resizeTimer)
|
||||
}
|
||||
resizeTimer = setTimeout(() => {
|
||||
const ua = navigator.userAgent.toLowerCase()
|
||||
const mobileRegex = /android|webos|iphone|ipod|blackberry|iemobile|opera mini/i
|
||||
const tabletRegex = /ipad|tablet|playbook|silk/i
|
||||
|
||||
const hasTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 1
|
||||
const smallScreen = window.innerWidth <= 768
|
||||
const tabletScreen = window.innerWidth > 768 && window.innerWidth <= 1200
|
||||
|
||||
const uaMobile = mobileRegex.test(ua)
|
||||
const uaTablet = tabletRegex.test(ua)
|
||||
|
||||
isMobile.value = (uaMobile && !uaTablet) || smallScreen
|
||||
}, 100)
|
||||
}
|
||||
|
||||
const handleResize = () => {
|
||||
checkDevice()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
checkDevice()
|
||||
window.addEventListener('resize', handleResize)
|
||||
window.addEventListener('orientationchange', checkDevice)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
window.removeEventListener('orientationchange', checkDevice)
|
||||
if (resizeTimer) {
|
||||
clearTimeout(resizeTimer)
|
||||
}
|
||||
})
|
||||
|
||||
onActivated(() => {
|
||||
checkDevice()
|
||||
})
|
||||
|
||||
return { isMobile }
|
||||
}
|
||||
|
||||
export const useIsTablet = () => {
|
||||
const isTablet = ref(false)
|
||||
|
||||
let resizeTimer: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
const checkDevice = () => {
|
||||
if (resizeTimer) {
|
||||
clearTimeout(resizeTimer)
|
||||
}
|
||||
resizeTimer = setTimeout(() => {
|
||||
const ua = navigator.userAgent.toLowerCase()
|
||||
const tabletRegex = /ipad|tablet|playbook|silk/i
|
||||
const hasTouch = 'ontouchstart' in window || navigator.maxTouchPoints > 0
|
||||
const tabletScreen = window.innerWidth >= 769 && window.innerWidth <= 1200
|
||||
|
||||
const uaTablet = tabletRegex.test(ua)
|
||||
|
||||
isTablet.value = (uaTablet && tabletScreen) || (hasTouch && tabletScreen)
|
||||
}, 100)
|
||||
}
|
||||
|
||||
const handleResize = () => {
|
||||
checkDevice()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
checkDevice()
|
||||
window.addEventListener('resize', handleResize)
|
||||
window.addEventListener('orientationchange', checkDevice)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener('resize', handleResize)
|
||||
window.removeEventListener('orientationchange', checkDevice)
|
||||
if (resizeTimer) {
|
||||
clearTimeout(resizeTimer)
|
||||
}
|
||||
})
|
||||
|
||||
onActivated(() => {
|
||||
checkDevice()
|
||||
})
|
||||
|
||||
return { isTablet }
|
||||
}
|
||||
@@ -1,191 +1,218 @@
|
||||
import axios from 'axios'
|
||||
// import qs from 'qs'
|
||||
// import message from '@/components/public/message/src'
|
||||
import router from '@/router/index'
|
||||
import { useGlobalStore, useUserInfoStore } from '@/stores'
|
||||
import { getCookie, clonAllCookie } from '@/utils/cookie'
|
||||
// import cookie from '@/tools/cookie.js'
|
||||
|
||||
// 扩展 AxiosRequestConfig 接口
|
||||
declare module 'axios' {
|
||||
interface AxiosRequestConfig {
|
||||
loading?: boolean
|
||||
loadingDom?: any
|
||||
repeatRequest?: boolean
|
||||
meta?: {
|
||||
responseAll?: boolean
|
||||
}
|
||||
axios.defaults.timeout = 600000 //响应时间
|
||||
// axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded;charset=UTF-8'; //配置请求头
|
||||
axios.defaults.headers.post['Content-Type'] = 'application/json'
|
||||
|
||||
axios.defaults.headers.post['lang'] = 'en' //配置语言请求头
|
||||
axios.defaults.withCredentials = true //跨域携带cookie
|
||||
import { message } from 'ant-design-vue'
|
||||
|
||||
const getBaseURL = () => {
|
||||
const defaultBaseURL = import.meta.env.VITE_APP_BASE_URL
|
||||
|
||||
const searchParams = new URL(window.location.href).searchParams
|
||||
if (searchParams.get('env') === 'dev') {
|
||||
console.log('设置为dev环境')
|
||||
return 'https://develop.api.aida.com.hk'
|
||||
} else {
|
||||
return defaultBaseURL
|
||||
}
|
||||
}
|
||||
|
||||
// 创建axios实例
|
||||
// console.log(import.meta.env,123)
|
||||
axios.defaults.baseURL = getBaseURL() //配置接口地址
|
||||
console.log('axios baseURL:', axios.defaults.baseURL)
|
||||
|
||||
const service = axios.create({
|
||||
// baseURL: import.meta.env.VITE_APP_URL, // api的base_url
|
||||
timeout: 60000 // 请求超时时间
|
||||
})
|
||||
if (import.meta.env.MODE != 'development') {
|
||||
service.defaults.baseURL = import.meta.env.VITE_APP_URL
|
||||
}
|
||||
axios.defaults.headers.post['Content-Type'] = 'application/json'
|
||||
axios.defaults.headers.post['lang'] = 'en' //配置语言请求头
|
||||
axios.defaults.withCredentials = true //跨域携带cookie
|
||||
// 创建取消令牌
|
||||
const CancelToken = axios.CancelToken
|
||||
const source = CancelToken.source()
|
||||
// console.log(import.meta.env.VITE_APP_BASE_URL);
|
||||
let isLoginTime = false
|
||||
//POST传参序列化(添加请求拦截器)
|
||||
axios.interceptors.request.use(
|
||||
(config) => {
|
||||
//在发送请求之前做某件事
|
||||
|
||||
// request拦截器
|
||||
service.interceptors.request.use(
|
||||
(config: any) => {
|
||||
removePending(config)
|
||||
// 如果repeatRequest不配置,那么默认该请求就取消重复接口请求
|
||||
!config.repeatRequest && addPending(config)
|
||||
// 打开loading
|
||||
if (config.loading) {
|
||||
LoadingInstance._count++
|
||||
if (LoadingInstance._count === 1) {
|
||||
openLoading(config.loadingDom)
|
||||
}
|
||||
}
|
||||
// 如果登录了,有token,则请求携带token
|
||||
// Do something before request is sent
|
||||
const token = useUserInfoStore().state.token
|
||||
if (token) {
|
||||
config.headers.Authorization = token // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改
|
||||
// config.headers['X-Token'] = getLocal('token') // 让每个请求携带token--['X-Token']为自定义key 请根据实际情况自行修改
|
||||
if (config.method === 'post' || config.method === 'put' || config.method === 'delete') {
|
||||
// config.data = qs.stringify(config.data);
|
||||
}
|
||||
config.headers.Authorization = getCookie('token')
|
||||
return config
|
||||
},
|
||||
(error) => {
|
||||
// Do something with request error
|
||||
console.log(error) // for debug
|
||||
Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// respone拦截器
|
||||
service.interceptors.response.use(
|
||||
// response => response,
|
||||
/**
|
||||
* 下面的注释为通过response自定义code来标示请求状态,当code返回如下情况为权限有问题,登出并返回到登录页
|
||||
* 如通过xmlhttprequest 状态码标识 逻辑可写在下面error中
|
||||
*/
|
||||
(response: any) => {
|
||||
// 如果是llm/streamChat这样的流式接口,不走这样的处理
|
||||
if (response.config.url.includes('llm/streamChat')) {
|
||||
return response
|
||||
}
|
||||
|
||||
// 已完成请求的删除请求中数组
|
||||
removePending(response.config)
|
||||
// 关闭loading
|
||||
if (response.config.loading) {
|
||||
closeLoading()
|
||||
}
|
||||
const res = response.data
|
||||
// 处理异常的情况
|
||||
// console.log(res)
|
||||
if (res.code != 0) {
|
||||
// showToast({
|
||||
// message: res.errMsg || res.message,
|
||||
// // type: 'fail',
|
||||
// duration: 5000,
|
||||
// position: 'top',
|
||||
// icon: 'none'
|
||||
// })
|
||||
return Promise.reject(new Error(res.errMsg || res.message || 'error'))
|
||||
} else {
|
||||
// 默认只返回data,不返回状态码和message
|
||||
// 通过 meta 中的 responseAll 配置来取决后台是否返回所有数据(包括状态码,message和data)
|
||||
const isbackAll = response.config.meta && response.config.meta.responseAll
|
||||
if (isbackAll) {
|
||||
return res
|
||||
} else {
|
||||
return res.data
|
||||
}
|
||||
}
|
||||
},
|
||||
(error) => {
|
||||
if(error?.response){
|
||||
if (error.config?.loading) closeLoading() // 关闭loading
|
||||
if(error?.response?.status === 401){//如果是记录浏览器页面就不跳转login
|
||||
// showConfirmDialog({
|
||||
// title: '确定登出',
|
||||
// message: '你已被登出,可以取消继续留在该页面,或者重新登录',
|
||||
// confirmButtonText: '重新登录',
|
||||
// cancelButtonText: '取消'
|
||||
// }).then(() => {
|
||||
// store.loginOut().then(() => {
|
||||
// location.reload() // 为了重新实例化vue-router对象 避免bug
|
||||
// })
|
||||
// })
|
||||
// showToast({
|
||||
// message: 'Please log in and try again.',
|
||||
// duration: 5000
|
||||
// })
|
||||
// router.push('/login')
|
||||
// useGenerateStore().clearGenerateData()
|
||||
return Promise.reject(false)
|
||||
}
|
||||
error.config && removePending(error.config)
|
||||
console.log('err' + error) // for debug
|
||||
// showToast({
|
||||
// message: error.message,
|
||||
// type: 'fail',
|
||||
// duration: 5000
|
||||
// })
|
||||
}
|
||||
return Promise.reject(error)
|
||||
}
|
||||
)
|
||||
|
||||
// --------------------------------取消接口重复请求的函数-----------------------------------
|
||||
// axios.js
|
||||
const pendingMap = new Map()
|
||||
/**
|
||||
* 生成每个请求唯一的键
|
||||
* @param {*} config
|
||||
* @returns string
|
||||
*/
|
||||
function getPendingKey(config: any) {
|
||||
const { url, method, params } = config
|
||||
let { data } = config
|
||||
if (typeof data === 'string') data = JSON.parse(data) // response里面返回的config.data是个字符串对象
|
||||
return [url, method, JSON.stringify(params), JSON.stringify(data)].join('&')
|
||||
const binaryToUrl = (binary, type = 'application/json', res) => {
|
||||
let blob = new Blob([binary], { 'content-type': type })
|
||||
let url = URL.createObjectURL(blob)
|
||||
return url
|
||||
}
|
||||
//返回状态判断(添加响应拦截器)
|
||||
axios.interceptors.response.use(
|
||||
(res) => {
|
||||
// 允许透传完整响应:请求时传 config.fullData = true
|
||||
|
||||
/**
|
||||
* 储存每个请求唯一值, 也就是cancel()方法, 用于取消请求
|
||||
* @param {*} config
|
||||
*/
|
||||
function addPending(config: any) {
|
||||
const pendingKey = getPendingKey(config)
|
||||
config.cancelToken =
|
||||
config.cancelToken ||
|
||||
new axios.CancelToken((cancel) => {
|
||||
if (!pendingMap.has(pendingKey)) {
|
||||
pendingMap.set(pendingKey, cancel)
|
||||
// if(res.data.data == null){
|
||||
// message.warning(res.data.errMsg)
|
||||
// return Promise.reject(res.data);
|
||||
// }else
|
||||
if (res?.config?.env?.binary) {
|
||||
let url = binaryToUrl(res.data, res.config.env.binaryType, res)
|
||||
return Promise.resolve({ url, data: res.data })
|
||||
}
|
||||
if (res.data instanceof Blob) {
|
||||
return Promise.resolve(res)
|
||||
}
|
||||
|
||||
if (res?.data) {
|
||||
if (res?.data?.errCode === 0) {
|
||||
// message.error(res?.data?.errMsg)
|
||||
if (res?.config?.fullData) {
|
||||
return Promise.resolve(res.data)
|
||||
}
|
||||
return Promise.resolve(res?.data?.data)
|
||||
} else if (res?.data?.errCode === 1) {
|
||||
message.warning(res?.data?.errMsg)
|
||||
return Promise.reject(res?.data)
|
||||
} else if (res?.data?.errCode === 2) {
|
||||
return Promise.reject(res?.data)
|
||||
} else if (res?.data?.errCode === -1) {
|
||||
message.error(res?.data?.errMsg)
|
||||
return Promise.reject(res?.data)
|
||||
}
|
||||
})
|
||||
}
|
||||
/**
|
||||
* 删除重复的请求
|
||||
* @param {*} config
|
||||
*/
|
||||
function removePending(config: any) {
|
||||
const pendingKey = getPendingKey(config)
|
||||
if (pendingMap.has(pendingKey)) {
|
||||
const cancelToken = pendingMap.get(pendingKey)
|
||||
cancelToken(pendingKey)
|
||||
pendingMap.delete(pendingKey)
|
||||
} else {
|
||||
if (res?.data?.errCode === 0) {
|
||||
message.warning(res?.data?.errMsg)
|
||||
return Promise.reject(res?.data)
|
||||
} else if (res?.data?.errCode === 1) {
|
||||
message.warning(res?.data?.errMsg)
|
||||
return Promise.reject(res?.data)
|
||||
} else if (res?.data?.errCode === 2) {
|
||||
return Promise.reject(res?.data)
|
||||
} else if (res?.data?.errCode === -1) {
|
||||
message.error(res?.data?.errMsg)
|
||||
return Promise.reject(res?.data)
|
||||
}
|
||||
}
|
||||
},
|
||||
function (error) {
|
||||
if (error?.response?.status === 401 && router.currentRoute._value.name != 'setIdentification') {
|
||||
//如果是记录浏览器页面就不跳转login
|
||||
clonAllCookie()
|
||||
if (!isLoginTime) {
|
||||
isLoginTime = true
|
||||
let isSystemUserRouteList = ['/Square'] //如果是这两个页面就无需跳转未登录页
|
||||
let sSystemUser = false
|
||||
for (let index = 0; index < isSystemUserRouteList.length; index++) {
|
||||
if (router.currentRoute.value.path.indexOf(isSystemUserRouteList[index]) > -1) {
|
||||
sSystemUser = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if (!sSystemUser) {
|
||||
router.replace('/')
|
||||
}
|
||||
message.warning('Please login and try again~')
|
||||
setTimeout(() => [(isLoginTime = false)], 2000)
|
||||
}
|
||||
// source.cancel('取消后续接口调用');
|
||||
return Promise.reject()
|
||||
}
|
||||
let data_new = error?.response?.data
|
||||
// message.error(data_new?.errMsg || 'Error: server exception')
|
||||
return Promise.reject(data_new)
|
||||
}
|
||||
}
|
||||
// ----------------------------------loading的函数-------------------------------
|
||||
const LoadingInstance: { _count: number } = {
|
||||
_count: 0
|
||||
}
|
||||
function openLoading(loadingDom: any) {
|
||||
useGlobalStore().setLoading(true)
|
||||
}
|
||||
function closeLoading() {
|
||||
if (LoadingInstance._count > 0) LoadingInstance._count--
|
||||
if (LoadingInstance._count === 0) {
|
||||
useGlobalStore().setLoading(false)
|
||||
}
|
||||
}
|
||||
)
|
||||
export const Https = {
|
||||
httpUrls: {
|
||||
// award页面
|
||||
checkEmail: '/api/global-award/checkEmail', // 检查邮箱是否存在
|
||||
checkOTP: '/api/global-award/checkCode', // 检查验证码是否正确
|
||||
initPdfUpload: '/api/global-award/uploads/pdf/init', // 初始化pdf上传
|
||||
initVideoUpload: '/api/global-award/uploads/video/init', // 初始化video上传
|
||||
uploadPDF: '/api/global-award/uploads/pdf/chunk', // 上传pdf
|
||||
uploadVideo: '/api/global-award/uploads/video/chunk', // 上传video
|
||||
uploadPDFComplete: '/api/global-award/uploads/pdf/complete', // 上传pdf完成
|
||||
uploadVideoComplete: '/api/global-award/uploads/video/complete', // 上传video完成
|
||||
submitForm: '/api/global-award/contestants/save', // 提交表单
|
||||
getContestantByID: '/api/global-award/contestants/', // 获取表单
|
||||
getContestCount: '/api/global-award/contestants/count', // 获取已提交申请总数
|
||||
getExcel: '/api/global-award/contestants/export', // 导出excel
|
||||
postExportFile: '/api/global-award/contestants/export/files', // 下载指定范围文件
|
||||
visitTracking: '/api/global-award/page/visit' // 每次都调用的埋点接口
|
||||
},
|
||||
|
||||
export default service
|
||||
axiosGet(url, config) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (isLoginTime && url != '/api/portfolio/page') {
|
||||
resolve('')
|
||||
return
|
||||
}
|
||||
axios
|
||||
.get(url, config)
|
||||
.then((response) => {
|
||||
resolve(response)
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
axiosPut(url, data) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (isLoginTime && url != '/api/portfolio/page') {
|
||||
resolve('')
|
||||
return
|
||||
}
|
||||
axios
|
||||
.put(url, data)
|
||||
.then((response) => {
|
||||
resolve(response)
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
axiosPost(url, data, config) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (isLoginTime && url != '/api/portfolio/page') {
|
||||
resolve('')
|
||||
return
|
||||
}
|
||||
axios
|
||||
.post(url, data, config)
|
||||
.then((response) => {
|
||||
resolve(response)
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
},
|
||||
|
||||
axiosDelete(url, newData) {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (isLoginTime && url != '/api/portfolio/page') {
|
||||
resolve('')
|
||||
return
|
||||
}
|
||||
axios
|
||||
.delete(url, { data: newData })
|
||||
.then((response) => {
|
||||
resolve(response)
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
30
src/utils/tracking.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Https } from '@/utils/request'
|
||||
import { generateUuid } from '@/utils/util'
|
||||
|
||||
const VISIT_TRACKING_SESSION_ID_KEY = 'global_award_visit_tracking_session_id'
|
||||
|
||||
const getVisitTrackingSessionId = () => {
|
||||
try {
|
||||
const storedSessionId = window.sessionStorage.getItem(VISIT_TRACKING_SESSION_ID_KEY)
|
||||
if (storedSessionId) return storedSessionId
|
||||
|
||||
const sessionId = generateUuid()
|
||||
window.sessionStorage.setItem(VISIT_TRACKING_SESSION_ID_KEY, sessionId)
|
||||
return sessionId
|
||||
} catch (error) {
|
||||
console.warn('[tracking] sessionStorage access failed', error)
|
||||
return generateUuid()
|
||||
}
|
||||
}
|
||||
|
||||
const requestVisitTracking = async () => {
|
||||
await Https.axiosPost(Https.httpUrls.visitTracking, {
|
||||
sessionId: getVisitTrackingSessionId()
|
||||
})
|
||||
}
|
||||
|
||||
export const trackRouteEntry = () => {
|
||||
requestVisitTracking().catch((error) => {
|
||||
console.warn('[tracking] request failed', error)
|
||||
})
|
||||
}
|
||||
@@ -676,6 +676,16 @@ function sketchToMask(sketchImage) {
|
||||
img.src = sketchImage
|
||||
})
|
||||
}
|
||||
|
||||
// 生成Uuid
|
||||
function generateUuid() {
|
||||
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||
const r = (Math.random() * 16) | 0
|
||||
const v = c === 'x' ? r : (r & 0x3) | 0x8
|
||||
return v.toString(16)
|
||||
})
|
||||
}
|
||||
|
||||
export {
|
||||
isEmail,
|
||||
getUploadUrl,
|
||||
@@ -699,5 +709,6 @@ export {
|
||||
calculateGradientCoordinate,
|
||||
segmentImage,
|
||||
UrlToFile,
|
||||
sketchToMask
|
||||
sketchToMask,
|
||||
generateUuid
|
||||
}
|
||||
|
||||
@@ -1,202 +1,272 @@
|
||||
<template>
|
||||
<div class="bloom flex flex-col align-center">
|
||||
<div
|
||||
class="title"
|
||||
ref="titleRef"
|
||||
>
|
||||
{{ $t('AwardsPage.bloomYourCreativity') }}
|
||||
</div>
|
||||
<div class="bloom flex flex-col align-center" :class="{ mobile: isMobile, pad: isPad }">
|
||||
<img
|
||||
v-if="isMobile"
|
||||
src="@/assets/images/mobile_version_background/ellipse.png"
|
||||
class="top-bg"
|
||||
alt=""
|
||||
/>
|
||||
<div class="title" ref="titleRef">
|
||||
{{ $t('AwardsPage.bloomYourCreativity') }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="season"
|
||||
ref="subtitleRef"
|
||||
>
|
||||
{{ $t('AwardsPage.themeOf2026') }}
|
||||
</div>
|
||||
<div
|
||||
class="desc"
|
||||
ref="textRef"
|
||||
>
|
||||
<p class="section-1">
|
||||
{{ $t('AwardsPage.bloomText.desc1.regular1') }}
|
||||
<span class="arial-bold">
|
||||
{{ $t('AwardsPage.bloomText.desc1.bold1') }}
|
||||
</span>
|
||||
{{ $t('AwardsPage.bloomText.desc1.regular2') }}
|
||||
<span class="arial-bold">
|
||||
{{ $t('AwardsPage.bloomText.desc1.bold2') }}
|
||||
</span>
|
||||
{{ $t('AwardsPage.bloomText.desc1.regular3') }}
|
||||
<span class="arial-bold">
|
||||
{{ $t('AwardsPage.bloomText.desc1.bold3') }}
|
||||
</span>
|
||||
{{ $t('AwardsPage.bloomText.desc1.regular4') }}
|
||||
<span class="arial-bold">
|
||||
{{ $t('AwardsPage.bloomText.desc1.bold4') }}
|
||||
</span>
|
||||
{{ $t('AwardsPage.bloomText.desc1.regular5') }}
|
||||
</p>
|
||||
<p class="section-2">
|
||||
{{ $t('AwardsPage.bloomText.desc2.regular1') }}
|
||||
<span class="arial-bold">
|
||||
{{ $t('AwardsPage.bloomText.desc2.bold1') }}
|
||||
</span>
|
||||
{{ $t('AwardsPage.bloomText.desc2.regular2') }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="season" ref="subtitleRef">
|
||||
{{ $t('AwardsPage.themeOf2026') }}
|
||||
</div>
|
||||
<div class="desc" ref="textRef">
|
||||
<p class="section-1">
|
||||
<span>{{ $t('AwardsPage.bloomText.desc1.regular1') }}</span>
|
||||
<span class="arial-bold">
|
||||
{{ $t('AwardsPage.bloomText.desc1.bold1') }}
|
||||
</span>
|
||||
<span>{{ $t('AwardsPage.bloomText.desc1.regular2') }}</span>
|
||||
<span class="arial-bold">
|
||||
{{ $t('AwardsPage.bloomText.desc1.bold2') }}
|
||||
</span>
|
||||
<span>{{ $t('AwardsPage.bloomText.desc1.regular3') }}</span>
|
||||
<span class="arial-bold">
|
||||
{{ $t('AwardsPage.bloomText.desc1.bold3') }}
|
||||
</span>
|
||||
<span>{{ $t('AwardsPage.bloomText.desc1.regular4') }}</span>
|
||||
<span class="arial-bold">
|
||||
{{ $t('AwardsPage.bloomText.desc1.bold4') }}
|
||||
</span>
|
||||
<span>{{ $t('AwardsPage.bloomText.desc1.regular5') }}</span>
|
||||
</p>
|
||||
<!-- <p class="section-1 pad" v-show="isPad" v-html="padSection1"></p> -->
|
||||
<p class="section-2">
|
||||
{{ $t('AwardsPage.bloomText.desc2.regular1') }}
|
||||
<span class="arial-bold">
|
||||
{{ $t('AwardsPage.bloomText.desc2.bold1') }}
|
||||
</span>
|
||||
{{ $t('AwardsPage.bloomText.desc2.regular2') }}
|
||||
</p>
|
||||
</div>
|
||||
<img
|
||||
v-if="isMobile"
|
||||
src="@/assets/images/mobile_version_background/people.png"
|
||||
class="people"
|
||||
alt=""
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { gsap } from 'gsap'
|
||||
import { ref, onMounted, onBeforeUnmount, nextTick, inject } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { gsap } from 'gsap'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { t } = useI18n()
|
||||
const isMobile = inject<boolean>('isMobile')
|
||||
const isPad = inject<boolean>('isPad')
|
||||
|
||||
const titleRef = ref<HTMLElement | null>(null)
|
||||
const subtitleRef = ref<HTMLElement | null>(null)
|
||||
const textRef = ref<HTMLElement | null>(null)
|
||||
const titleRef = ref<HTMLElement | null>(null)
|
||||
const subtitleRef = ref<HTMLElement | null>(null)
|
||||
const textRef = ref<HTMLElement | null>(null)
|
||||
|
||||
const hasPlayedBloomAnim = ref(false)
|
||||
let bloomObserver: IntersectionObserver | null = null
|
||||
// const padSection1 = computed(() => {
|
||||
// return t('AwardsPage.bloomDesc1')
|
||||
// })
|
||||
|
||||
const setupBloomInitialState = () => {
|
||||
const titleEls = [titleRef.value, subtitleRef.value].filter(
|
||||
Boolean
|
||||
) as HTMLElement[]
|
||||
if (titleEls.length) {
|
||||
gsap.set(titleEls, {
|
||||
opacity: 0,
|
||||
// start larger than final size, then animate down to scale:1
|
||||
scale: 1.6,
|
||||
transformOrigin: '50% 50%'
|
||||
})
|
||||
}
|
||||
const hasPlayedBloomAnim = ref(false)
|
||||
let bloomObserver: IntersectionObserver | null = null
|
||||
|
||||
if (textRef.value) {
|
||||
// start below and hidden
|
||||
gsap.set(textRef.value, {
|
||||
opacity: 0,
|
||||
y: 60
|
||||
})
|
||||
}
|
||||
}
|
||||
const setupBloomInitialState = () => {
|
||||
const titleEls = [titleRef.value, subtitleRef.value].filter(Boolean) as HTMLElement[]
|
||||
if (titleEls.length) {
|
||||
gsap.set(titleEls, {
|
||||
opacity: 0,
|
||||
// start larger than final size, then animate down to scale:1
|
||||
scale: 1.6,
|
||||
transformOrigin: '50% 50%'
|
||||
})
|
||||
}
|
||||
|
||||
const playBloomAnimation = () => {
|
||||
if (hasPlayedBloomAnim.value) return
|
||||
const titleEls = [titleRef.value, subtitleRef.value].filter(
|
||||
Boolean
|
||||
) as HTMLElement[]
|
||||
const textEl = textRef.value
|
||||
if (!titleEls.length || !textEl) return
|
||||
if (textRef.value) {
|
||||
// start below and hidden
|
||||
gsap.set(textRef.value, {
|
||||
opacity: 0,
|
||||
y: 60
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const tl = gsap.timeline({ defaults: { ease: 'power2.out' } })
|
||||
const playBloomAnimation = () => {
|
||||
if (hasPlayedBloomAnim.value) return
|
||||
const titleEls = [titleRef.value, subtitleRef.value].filter(Boolean) as HTMLElement[]
|
||||
const textEl = textRef.value
|
||||
if (!titleEls.length || !textEl) return
|
||||
|
||||
tl.to(titleEls, {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
duration: 0.9,
|
||||
ease: 'back.out(1.6)',
|
||||
stagger: 0.12
|
||||
})
|
||||
const tl = gsap.timeline({ defaults: { ease: 'power2.out' } })
|
||||
|
||||
tl.to(
|
||||
textEl,
|
||||
{
|
||||
opacity: 1,
|
||||
y: -12,
|
||||
scale: 1.05,
|
||||
duration: 0.3,
|
||||
ease: 'power2.out'
|
||||
},
|
||||
'-=0.3'
|
||||
)
|
||||
tl.to(
|
||||
textEl,
|
||||
{
|
||||
y: 0,
|
||||
scale: 1,
|
||||
duration: 0.18,
|
||||
ease: 'bounce.out'
|
||||
},
|
||||
'+=0.08'
|
||||
)
|
||||
tl.to(titleEls, {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
duration: 0.9,
|
||||
ease: 'back.out(1.6)',
|
||||
stagger: 0.12
|
||||
})
|
||||
|
||||
hasPlayedBloomAnim.value = true
|
||||
bloomObserver?.disconnect()
|
||||
}
|
||||
tl.to(
|
||||
textEl,
|
||||
{
|
||||
opacity: 1,
|
||||
y: -12,
|
||||
scale: 1.05,
|
||||
duration: 0.3,
|
||||
ease: 'power2.out'
|
||||
},
|
||||
'-=0.3'
|
||||
)
|
||||
tl.to(
|
||||
textEl,
|
||||
{
|
||||
y: 0,
|
||||
scale: 1,
|
||||
duration: 0.18,
|
||||
ease: 'bounce.out'
|
||||
},
|
||||
'+=0.08'
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
setupBloomInitialState()
|
||||
if ('IntersectionObserver' in window) {
|
||||
bloomObserver = new IntersectionObserver(
|
||||
entries => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
playBloomAnimation()
|
||||
}
|
||||
})
|
||||
},
|
||||
{ threshold: 0.3 }
|
||||
)
|
||||
if (titleRef.value) {
|
||||
bloomObserver.observe(titleRef.value)
|
||||
}
|
||||
} else {
|
||||
// fallback
|
||||
playBloomAnimation()
|
||||
}
|
||||
})
|
||||
})
|
||||
hasPlayedBloomAnim.value = true
|
||||
bloomObserver?.disconnect()
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
bloomObserver?.disconnect()
|
||||
})
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
setupBloomInitialState()
|
||||
if ('IntersectionObserver' in window) {
|
||||
bloomObserver = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
playBloomAnimation()
|
||||
}
|
||||
})
|
||||
},
|
||||
{ threshold: 0.3 }
|
||||
)
|
||||
if (titleRef.value) {
|
||||
bloomObserver.observe(titleRef.value)
|
||||
}
|
||||
} else {
|
||||
// fallback
|
||||
playBloomAnimation()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
bloomObserver?.disconnect()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.arial-bold {
|
||||
font-family: 'ArialBold';
|
||||
font-weight: 700;
|
||||
}
|
||||
.bloom {
|
||||
height: 108rem;
|
||||
padding-top: 12.8rem;
|
||||
font-family: 'Poppins';
|
||||
background: url('@/assets/images/award/bloom_bg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
.title {
|
||||
font-size: 4rem;
|
||||
color: #232323;
|
||||
margin-bottom: 2.4rem;
|
||||
}
|
||||
.logo {
|
||||
margin-bottom: 2.2rem;
|
||||
}
|
||||
.season {
|
||||
font-size: 3rem;
|
||||
color: #c7342c;
|
||||
margin-bottom: 6.6rem;
|
||||
}
|
||||
.desc {
|
||||
font-family: 'Arial';
|
||||
font-weight: 400;
|
||||
font-size: 2.4rem;
|
||||
color: #585858;
|
||||
text-align: center;
|
||||
padding: 0 21.5rem;
|
||||
line-height: 4.5rem;
|
||||
margin-bottom: 12.3rem;
|
||||
white-space: pre-line;
|
||||
.section-2 {
|
||||
margin-top: 4rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
:deep(.arial-bold) {
|
||||
font-family: 'ArialBold', Arial, sans-serif;
|
||||
font-weight: 700;
|
||||
}
|
||||
.bloom {
|
||||
height: 108rem;
|
||||
padding-top: 12.8rem;
|
||||
font-family: 'PoppinsBold';
|
||||
background: url('@/assets/images/award/bloom_bg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
.title {
|
||||
font-size: 4rem;
|
||||
font-weight: 600;
|
||||
color: #232323;
|
||||
margin-bottom: 2.4rem;
|
||||
}
|
||||
.logo {
|
||||
margin-bottom: 2.2rem;
|
||||
}
|
||||
.season {
|
||||
font-size: 3rem;
|
||||
color: #c7342c;
|
||||
font-family: 'PoppinsMedium';
|
||||
margin-bottom: 6.6rem;
|
||||
}
|
||||
.desc {
|
||||
font-family: 'Arial';
|
||||
font-weight: 400;
|
||||
font-size: 2.4rem;
|
||||
color: #585858;
|
||||
text-align: center;
|
||||
padding: 0 21.5rem;
|
||||
line-height: 4.5rem;
|
||||
margin-bottom: 12.3rem;
|
||||
white-space: pre-line;
|
||||
.section-1 {
|
||||
font-size: 0;
|
||||
span {
|
||||
font-size: 2.4rem;
|
||||
}
|
||||
}
|
||||
.section-2 {
|
||||
margin-top: 4rem;
|
||||
}
|
||||
}
|
||||
&.mobile {
|
||||
background: #fff;
|
||||
background-size: 100% 100%;
|
||||
height: auto;
|
||||
padding-top: 6rem;
|
||||
position: relative;
|
||||
.top-bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
// left: 50%;
|
||||
// transform: translateX(-50%);
|
||||
width: 100%;
|
||||
height: 37.5rem;
|
||||
}
|
||||
.title {
|
||||
font-size: 3.2rem;
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
.season {
|
||||
font-size: 2.4rem;
|
||||
margin-bottom: 6.2rem;
|
||||
}
|
||||
.desc {
|
||||
padding: 0 8.5vw;
|
||||
font-size: 2rem;
|
||||
line-height: 3rem;
|
||||
white-space: normal;
|
||||
margin-bottom: 7rem;
|
||||
.section-1 {
|
||||
font-size: 0;
|
||||
span {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
.section-2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
.people {
|
||||
margin: 0 6.6rem;
|
||||
width: calc(100% - 13.2rem);
|
||||
}
|
||||
}
|
||||
&.pad {
|
||||
height: 84.6vw;
|
||||
background: url('@/assets/images/pad_version/bloom_bg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
.arial-bold {
|
||||
font-family: 'ArialBold';
|
||||
font-weight: 700;
|
||||
}
|
||||
// .desc {
|
||||
// // font-size: 2.4rem;
|
||||
// }
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,67 +1,89 @@
|
||||
<template>
|
||||
<div class="judges-container flex flex-col align-center">
|
||||
<div class="title" ref="judgesTitleRef">{{ $t('AwardsPage.panelOfJudges') }}</div>
|
||||
<!-- <img src="@/assets/images/award/bloom_logo.png" class="logo" /> -->
|
||||
<div class="sub-title" ref="judgesSubTitleRef">{{ $t('AwardsPage.expertise') }}</div>
|
||||
<div class="judgement-list" ref="judgementListRef">
|
||||
<div
|
||||
class="judgement-item flex flex-col align-center"
|
||||
v-for="item in judgements"
|
||||
:key="item.name"
|
||||
>
|
||||
<img :src="item.picture" class="picture" />
|
||||
<div class="name">{{ $t(item.name) }}</div>
|
||||
<div class="desc">{{ $t(item.desc) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="judges-container flex flex-col align-center" :class="{ mobile: isMobile && !isPad }">
|
||||
<div class="title" ref="judgesTitleRef">{{ $t('AwardsPage.panelOfJudges') }}</div>
|
||||
<!-- <img src="@/assets/images/award/bloom_logo.png" class="logo" /> -->
|
||||
<div class="sub-title" ref="judgesSubTitleRef">{{ $t('AwardsPage.expertise') }}</div>
|
||||
<div class="judgement-list" ref="judgementListRef">
|
||||
<div
|
||||
class="judgement-item flex flex-col align-center"
|
||||
v-for="item in judgements"
|
||||
:key="item.name"
|
||||
>
|
||||
<template v-if="!item.tbd">
|
||||
<img :src="item.picture" class="picture" />
|
||||
<div class="name">{{ $t(item.name) }}</div>
|
||||
<div class="desc">{{ $t(item.desc) }}</div>
|
||||
<div class="region arial-bold">{{ $t(item.region) }}</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="picture tbd"></div>
|
||||
<div class="name">{{ $t('AwardsPage.tbd') }}</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeUnmount, onMounted, nextTick, ref } from 'vue'
|
||||
import { onBeforeUnmount, onMounted, nextTick, ref, inject, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { gsap } from 'gsap'
|
||||
import jae from '@/assets/images/award/jae.png'
|
||||
import diego from '@/assets/images/award/diego.png'
|
||||
import rosiexia from '@/assets/images/award/rosiexia.png'
|
||||
import gregory from '@/assets/images/award/gregory.png'
|
||||
import vincenzo from '@/assets/images/award/vincenzo.png'
|
||||
import tim from '@/assets/images/award/tim.png'
|
||||
import annie from '@/assets/images/award/anniewong.png'
|
||||
import xaven from '@/assets/images/award/xavenmak.png'
|
||||
import desmond from '@/assets/images/award/desmond.png'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const judgements = [
|
||||
{
|
||||
picture: jae,
|
||||
name: 'Jae Hyuk Lim',
|
||||
desc: 'AwardsPage.judgesHat.jae'
|
||||
},
|
||||
{
|
||||
picture: diego,
|
||||
name: 'Diego Dultzin Lacoste',
|
||||
desc: 'AwardsPage.judgesHat.diego'
|
||||
},
|
||||
{
|
||||
picture: gregory,
|
||||
name: 'Gregory de la Hogue Moran',
|
||||
desc: 'AwardsPage.judgesHat.gregory'
|
||||
},
|
||||
{
|
||||
picture: vincenzo,
|
||||
name: 'Vincenzo La Torre',
|
||||
desc: 'AwardsPage.judgesHat.vincenzo'
|
||||
},
|
||||
{
|
||||
picture: tim,
|
||||
name: 'Tim Lim',
|
||||
desc: 'AwardsPage.judgesHat.tim'
|
||||
},
|
||||
{
|
||||
picture: desmond,
|
||||
name: 'Desmond Lim',
|
||||
desc: 'AwardsPage.judgesHat.desmond'
|
||||
}
|
||||
]
|
||||
const isMobile = inject<boolean>('isMobile')
|
||||
const isPad = inject<boolean>('isPad')
|
||||
const judgements = computed(() => {
|
||||
return [
|
||||
{
|
||||
picture: jae,
|
||||
name: 'Jae Hyuk Lim',
|
||||
desc: 'AwardsPage.judgesHat.jae',
|
||||
region: 'AwardsPage.judgesHat.jaeCountry',
|
||||
order: 1
|
||||
},
|
||||
{
|
||||
picture: rosiexia,
|
||||
name: 'Rosie Xia',
|
||||
desc: 'AwardsPage.judgesHat.rosie',
|
||||
region: 'AwardsPage.judgesHat.rosieCountry',
|
||||
order: isMobile.value ? 3 : 2
|
||||
},
|
||||
{
|
||||
picture: gregory,
|
||||
name: 'Gregory de la Hogue Moran',
|
||||
desc: 'AwardsPage.judgesHat.gregory',
|
||||
region: 'AwardsPage.judgesHat.gregoryCountry',
|
||||
order: isMobile.value ? 2 : 4
|
||||
},
|
||||
{
|
||||
picture: annie,
|
||||
name: 'Annie Wong',
|
||||
desc: 'AwardsPage.judgesHat.annie',
|
||||
region: 'AwardsPage.judgesHat.annieCountry',
|
||||
order: isMobile.value ? 4 : 5
|
||||
},
|
||||
{
|
||||
picture: xaven,
|
||||
name: 'Xaven Mak',
|
||||
desc: 'AwardsPage.judgesHat.xaven',
|
||||
region: 'AwardsPage.judgesHat.xavenCountry',
|
||||
order: 6
|
||||
},
|
||||
{
|
||||
picture: desmond,
|
||||
name: 'Desmond Lim',
|
||||
desc: 'AwardsPage.judgesHat.desmond',
|
||||
region: 'AwardsPage.judgesHat.desmondCountry',
|
||||
order: isMobile.value ? 5 : 3
|
||||
}
|
||||
].sort((a,b)=>a.order - b.order)
|
||||
})
|
||||
|
||||
const judgesTitleRef = ref<HTMLElement | null>(null)
|
||||
const judgesSubTitleRef = ref<HTMLElement | null>(null)
|
||||
@@ -70,164 +92,207 @@ const hasPlayedJudgementAnim = ref(false)
|
||||
let judgementObserver: IntersectionObserver | null = null
|
||||
|
||||
const setupJudgementInitialState = () => {
|
||||
const titleEls = [judgesTitleRef.value, judgesSubTitleRef.value].filter(
|
||||
Boolean
|
||||
) as HTMLElement[]
|
||||
if (titleEls.length) {
|
||||
gsap.set(titleEls, {
|
||||
opacity: 0,
|
||||
scale: 0,
|
||||
transformOrigin: '50% 50%'
|
||||
})
|
||||
}
|
||||
const items =
|
||||
judgementListRef.value?.querySelectorAll<HTMLElement>('.judgement-item')
|
||||
if (items?.length) {
|
||||
gsap.set(items, {
|
||||
opacity: 0,
|
||||
clipPath: 'inset(0 0 100% 0)'
|
||||
})
|
||||
}
|
||||
const titleEls = [judgesTitleRef.value, judgesSubTitleRef.value].filter(Boolean) as HTMLElement[]
|
||||
if (titleEls.length) {
|
||||
gsap.set(titleEls, {
|
||||
opacity: 0,
|
||||
scale: 0,
|
||||
transformOrigin: '50% 50%'
|
||||
})
|
||||
}
|
||||
const items = judgementListRef.value?.querySelectorAll<HTMLElement>('.judgement-item')
|
||||
if (items?.length) {
|
||||
gsap.set(items, {
|
||||
opacity: 0,
|
||||
clipPath: 'inset(0 0 100% 0)'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const playJudgementAnimation = () => {
|
||||
if (hasPlayedJudgementAnim.value) return
|
||||
const titleEls = [judgesTitleRef.value, judgesSubTitleRef.value].filter(
|
||||
Boolean
|
||||
) as HTMLElement[]
|
||||
const listEl = judgementListRef.value
|
||||
if (!titleEls.length || !listEl) return
|
||||
if (hasPlayedJudgementAnim.value) return
|
||||
const titleEls = [judgesTitleRef.value, judgesSubTitleRef.value].filter(Boolean) as HTMLElement[]
|
||||
const listEl = judgementListRef.value
|
||||
if (!titleEls.length || !listEl) return
|
||||
|
||||
const items = Array.from(
|
||||
listEl.querySelectorAll<HTMLElement>('.judgement-item')
|
||||
)
|
||||
const tl = gsap.timeline({ defaults: { ease: 'power2.out' } })
|
||||
const items = Array.from(listEl.querySelectorAll<HTMLElement>('.judgement-item'))
|
||||
const tl = gsap.timeline({ defaults: { ease: 'power2.out' } })
|
||||
|
||||
tl.to(titleEls, {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
duration: 0.4,
|
||||
ease: 'back.out(1.6)',
|
||||
stagger: 0.1
|
||||
})
|
||||
if (items.length) {
|
||||
const firstRow = items.slice(0, 3)
|
||||
const secondRow = items.slice(3)
|
||||
tl.to(titleEls, {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
duration: 0.4,
|
||||
ease: 'back.out(1.6)',
|
||||
stagger: 0.1
|
||||
})
|
||||
if (items.length) {
|
||||
const firstRow = items.slice(0, 3)
|
||||
const secondRow = items.slice(3)
|
||||
|
||||
if (firstRow.length) {
|
||||
tl.to(
|
||||
firstRow,
|
||||
{
|
||||
opacity: 1,
|
||||
clipPath: 'inset(0% 0% 0% 0%)',
|
||||
duration: 0.45,
|
||||
stagger: 0.05
|
||||
},
|
||||
'-=0.2'
|
||||
)
|
||||
}
|
||||
if (firstRow.length) {
|
||||
tl.to(
|
||||
firstRow,
|
||||
{
|
||||
opacity: 1,
|
||||
clipPath: 'inset(0% 0% 0% 0%)',
|
||||
duration: 0.45,
|
||||
stagger: 0.05
|
||||
},
|
||||
'-=0.2'
|
||||
)
|
||||
}
|
||||
|
||||
if (secondRow.length) {
|
||||
tl.to(
|
||||
secondRow,
|
||||
{
|
||||
opacity: 1,
|
||||
clipPath: 'inset(0% 0% 0% 0%)',
|
||||
duration: 0.45,
|
||||
stagger: 0.05
|
||||
},
|
||||
'+=0.1'
|
||||
)
|
||||
}
|
||||
}
|
||||
if (secondRow.length) {
|
||||
tl.to(
|
||||
secondRow,
|
||||
{
|
||||
opacity: 1,
|
||||
clipPath: 'inset(0% 0% 0% 0%)',
|
||||
duration: 0.45,
|
||||
stagger: 0.05
|
||||
},
|
||||
'+=0.1'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
hasPlayedJudgementAnim.value = true
|
||||
judgementObserver?.disconnect()
|
||||
hasPlayedJudgementAnim.value = true
|
||||
judgementObserver?.disconnect()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
setupJudgementInitialState()
|
||||
if ('IntersectionObserver' in window) {
|
||||
judgementObserver = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
playJudgementAnimation()
|
||||
}
|
||||
})
|
||||
},
|
||||
{ threshold: 0.3 }
|
||||
)
|
||||
if (judgementListRef.value) {
|
||||
judgementObserver.observe(judgementListRef.value)
|
||||
}
|
||||
} else {
|
||||
// Fallback: play immediately if IntersectionObserver unsupported
|
||||
playJudgementAnimation()
|
||||
}
|
||||
})
|
||||
nextTick(() => {
|
||||
setupJudgementInitialState()
|
||||
if ('IntersectionObserver' in window) {
|
||||
judgementObserver = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
playJudgementAnimation()
|
||||
}
|
||||
})
|
||||
},
|
||||
{ threshold: 0.3 }
|
||||
)
|
||||
if (judgementListRef.value) {
|
||||
judgementObserver.observe(judgementListRef.value)
|
||||
}
|
||||
} else {
|
||||
// Fallback: play immediately if IntersectionObserver unsupported
|
||||
playJudgementAnimation()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
judgementObserver?.disconnect()
|
||||
judgementObserver?.disconnect()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.judges-container {
|
||||
height: 147.4rem;
|
||||
background: url('@/assets/images/award/judges_bg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
padding-top: 12.8rem;
|
||||
.title {
|
||||
color: #232323;
|
||||
font-family: 'PoppinsBold';
|
||||
font-weight: 600;
|
||||
font-size: 4rem;
|
||||
}
|
||||
.logo {
|
||||
margin: 2.4rem 0 2.2rem;
|
||||
}
|
||||
.sub-title {
|
||||
color: #b10000;
|
||||
font-family: 'Arial';
|
||||
font-weight: 400;
|
||||
font-size: 3rem;
|
||||
margin-bottom: 12rem;
|
||||
}
|
||||
.judgement-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
column-gap: 23.22rem;
|
||||
row-gap: 8rem;
|
||||
padding: 0 25rem 0 26.6rem;
|
||||
div{
|
||||
text-align: center;
|
||||
}
|
||||
.judgement-item {
|
||||
overflow: hidden;
|
||||
.picture {
|
||||
width: 20.2rem;
|
||||
height: 26rem;
|
||||
border-radius: 0.8rem;
|
||||
}
|
||||
.name {
|
||||
margin: 3rem 0 2.4rem;
|
||||
color: #232323;
|
||||
font-family: 'PoppinsBold';
|
||||
font-weight: 600;
|
||||
font-size: 2.4rem;
|
||||
}
|
||||
.desc {
|
||||
color: #585858;
|
||||
font-family: 'Arial';
|
||||
font-weight: 400;
|
||||
font-size: 2rem;
|
||||
white-space: pre-line;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
height: 147.4rem;
|
||||
background: url('@/assets/images/award/judges_bg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
padding-top: 12.8rem;
|
||||
.title {
|
||||
color: #232323;
|
||||
font-family: 'PoppinsBold';
|
||||
font-weight: 600;
|
||||
font-size: 4rem;
|
||||
margin-bottom: 2.4rem;
|
||||
}
|
||||
.logo {
|
||||
margin: 2.4rem 0 2.2rem;
|
||||
}
|
||||
.sub-title {
|
||||
color: #b10000;
|
||||
font-family: 'Arial';
|
||||
font-weight: 400;
|
||||
font-size: 3rem;
|
||||
margin-bottom: 12rem;
|
||||
}
|
||||
.judgement-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
column-gap: 19rem;
|
||||
row-gap: 9.2rem;
|
||||
padding: 0 25rem 0 26.6rem;
|
||||
div {
|
||||
text-align: center;
|
||||
}
|
||||
.judgement-item {
|
||||
overflow: hidden;
|
||||
line-height: 2.4rem;
|
||||
.picture {
|
||||
width: 20.2rem;
|
||||
height: 26rem;
|
||||
border-radius: 0.8rem;
|
||||
&.tbd {
|
||||
background-color: #d7d7d7;
|
||||
}
|
||||
}
|
||||
.name {
|
||||
margin: 3rem 0 2.4rem;
|
||||
color: #232323;
|
||||
font-family: 'PoppinsBold';
|
||||
font-weight: 600;
|
||||
font-size: 2.4rem;
|
||||
width: 90%;
|
||||
}
|
||||
.desc {
|
||||
color: #585858;
|
||||
font-family: 'Arial';
|
||||
font-weight: 400;
|
||||
font-size: 2rem;
|
||||
white-space: pre-line;
|
||||
text-align: center;
|
||||
}
|
||||
.region {
|
||||
font-family: 'ArialBold';
|
||||
font-weight: 700;
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.mobile {
|
||||
height: auto;
|
||||
background: url('@/assets/images/mobile_version_background/judge_bg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
padding-top: 6rem;
|
||||
padding-bottom: 16.9rem;
|
||||
.title {
|
||||
font-size: 3.2rem;
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
.sub-title {
|
||||
font-family: 'PoppinsMedium';
|
||||
font-weight: 500;
|
||||
font-size: 2.4rem;
|
||||
margin-bottom: 5.8rem;
|
||||
}
|
||||
.judgement-list {
|
||||
padding: 0 8.6rem;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
column-gap: 6.8rem;
|
||||
row-gap: 6rem;
|
||||
// row-gap: 4rem;
|
||||
// padding: 0 1.6rem;
|
||||
.judgement-item {
|
||||
.picture {
|
||||
width: 15.6rem;
|
||||
height: 20rem;
|
||||
}
|
||||
.name {
|
||||
font-size: 2.4rem;
|
||||
margin: 1.2rem 0;
|
||||
}
|
||||
.desc {
|
||||
font-family: 'Instrument';
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,276 +1,296 @@
|
||||
<template>
|
||||
<div
|
||||
class="prizes-container container flex align-center space-between"
|
||||
ref="prizesRef"
|
||||
>
|
||||
<div class="left flex flex-col flex-center">
|
||||
<div
|
||||
class="title"
|
||||
ref="prizesTitleRef"
|
||||
>
|
||||
{{ $t('AwardsPage.awardPrizes') }}
|
||||
</div>
|
||||
<!-- <img src="@/assets/images/award/bloom_logo.png" class="logo" /> -->
|
||||
<div
|
||||
class="desc"
|
||||
ref="prizesSubTitleRef"
|
||||
>
|
||||
{{ $t('AwardsPage.recognition') }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="right"
|
||||
ref="prizesRightRef"
|
||||
>
|
||||
<div
|
||||
class="prize-item flex flex-col flex-center"
|
||||
:class="{ smaller: item.smaller }"
|
||||
v-for="item in prizes"
|
||||
:key="item.name"
|
||||
>
|
||||
<div class="prize-money">
|
||||
{{ $t(item.money) }}
|
||||
</div>
|
||||
<div class="prize-name">{{ $t(item.name) }}</div>
|
||||
<div class="prize-desc flex flex-col flex-center">
|
||||
<div
|
||||
class="desc-item"
|
||||
v-for="el in item.desc"
|
||||
>
|
||||
{{ $t(el) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="prizes-container container flex align-center space-between"
|
||||
ref="prizesRef"
|
||||
:class="{ mobile: isMobile && !isPad, 'flex-col': isMobile && !isPad, 'is-pad': isPad }"
|
||||
>
|
||||
<div class="left flex flex-col flex-center">
|
||||
<div class="title" ref="prizesTitleRef">
|
||||
{{ $t('AwardsPage.awardPrizes') }}
|
||||
</div>
|
||||
<!-- <img src="@/assets/images/award/bloom_logo.png" class="logo" /> -->
|
||||
<div class="desc" ref="prizesSubTitleRef">
|
||||
{{ $t('AwardsPage.recognition') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="right" ref="prizesRightRef">
|
||||
<div
|
||||
class="prize-item flex flex-col flex-center"
|
||||
:class="{ smaller: item.smaller }"
|
||||
v-for="item in prizes"
|
||||
:key="item.name"
|
||||
>
|
||||
<div class="prize-money">
|
||||
{{ $t(item.money) }}
|
||||
</div>
|
||||
<div class="prize-name">{{ $t(item.name) }}</div>
|
||||
<div class="prize-desc flex flex-col flex-center">
|
||||
<div class="desc-item" v-for="el in item.desc">
|
||||
{{ $t(el) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { gsap } from 'gsap'
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref, inject } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { gsap } from 'gsap'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { t } = useI18n()
|
||||
const isMobile = inject<boolean>('isMobile')
|
||||
const isPad = inject<boolean>('isPad')
|
||||
|
||||
const props = defineProps({
|
||||
isZh: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
const props = defineProps({
|
||||
isZh: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
}
|
||||
})
|
||||
|
||||
const prizes = [
|
||||
{
|
||||
money: 'AwardsPage.grandMoney',
|
||||
name: 'AwardsPage.grandAwards',
|
||||
desc: [
|
||||
'AwardsPage.cashAward',
|
||||
'AwardsPage.awardCertificate',
|
||||
'AwardsPage.globalMediaExposure'
|
||||
]
|
||||
},
|
||||
{
|
||||
money: 'AwardsPage.goldMoney',
|
||||
name: 'AwardsPage.goldAwards',
|
||||
desc: [
|
||||
'AwardsPage.cashAward',
|
||||
'AwardsPage.awardCertificate',
|
||||
'AwardsPage.globalMediaExposure'
|
||||
]
|
||||
},
|
||||
{
|
||||
money: 'AwardsPage.silverMoney',
|
||||
name: 'AwardsPage.silverAwards',
|
||||
desc: [
|
||||
'AwardsPage.cashAward',
|
||||
'AwardsPage.awardCertificate',
|
||||
'AwardsPage.globalMediaExposure'
|
||||
]
|
||||
},
|
||||
{
|
||||
money: 'AwardsPage.awardCertification',
|
||||
name: 'AwardsPage.finalists',
|
||||
desc: ['AwardsPage.TravelAllowance', 'AwardsPage.globalMediaExposure'],
|
||||
smaller: !props.isZh
|
||||
}
|
||||
]
|
||||
const prizes = [
|
||||
{
|
||||
money: 'AwardsPage.grandMoney',
|
||||
name: 'AwardsPage.grandAwards',
|
||||
desc: ['AwardsPage.cashAward', 'AwardsPage.awardCertificate', 'AwardsPage.globalMediaExposure']
|
||||
},
|
||||
{
|
||||
money: 'AwardsPage.goldMoney',
|
||||
name: 'AwardsPage.goldAwards',
|
||||
desc: ['AwardsPage.cashAward', 'AwardsPage.awardCertificate', 'AwardsPage.globalMediaExposure']
|
||||
},
|
||||
{
|
||||
money: 'AwardsPage.silverMoney',
|
||||
name: 'AwardsPage.silverAwards',
|
||||
desc: ['AwardsPage.cashAward', 'AwardsPage.awardCertificate', 'AwardsPage.globalMediaExposure']
|
||||
},
|
||||
{
|
||||
money: 'AwardsPage.awardCertification',
|
||||
name: 'AwardsPage.finalists',
|
||||
desc: ['AwardsPage.TravelAllowance', 'AwardsPage.globalMediaExposure'],
|
||||
smaller: !props.isZh
|
||||
}
|
||||
]
|
||||
|
||||
const prizesRef = ref<HTMLElement | null>(null)
|
||||
const prizesTitleRef = ref<HTMLElement | null>(null)
|
||||
const prizesSubTitleRef = ref<HTMLElement | null>(null)
|
||||
const prizesRightRef = ref<HTMLElement | null>(null)
|
||||
const hasPlayedPrizesAnim = ref(false)
|
||||
let prizesObserver: IntersectionObserver | null = null
|
||||
const prizesRef = ref<HTMLElement | null>(null)
|
||||
const prizesTitleRef = ref<HTMLElement | null>(null)
|
||||
const prizesSubTitleRef = ref<HTMLElement | null>(null)
|
||||
const prizesRightRef = ref<HTMLElement | null>(null)
|
||||
const hasPlayedPrizesAnim = ref(false)
|
||||
let prizesObserver: IntersectionObserver | null = null
|
||||
|
||||
const setupPrizesInitialState = () => {
|
||||
const titleEls = [prizesTitleRef.value, prizesSubTitleRef.value].filter(
|
||||
Boolean
|
||||
) as HTMLElement[]
|
||||
if (titleEls.length) {
|
||||
gsap.set(titleEls, {
|
||||
opacity: 0,
|
||||
scale: 0,
|
||||
transformOrigin: '50% 50%'
|
||||
})
|
||||
}
|
||||
if (prizesRightRef.value) {
|
||||
gsap.set(prizesRightRef.value, {
|
||||
'opacity': 0,
|
||||
'y': 40,
|
||||
'scale': 1.08,
|
||||
'--prize-row-gap': '2rem',
|
||||
'--prize-col-gap': '2rem'
|
||||
})
|
||||
}
|
||||
}
|
||||
const setupPrizesInitialState = () => {
|
||||
const titleEls = [prizesTitleRef.value, prizesSubTitleRef.value].filter(Boolean) as HTMLElement[]
|
||||
if (titleEls.length) {
|
||||
gsap.set(titleEls, {
|
||||
opacity: 0,
|
||||
scale: 0,
|
||||
transformOrigin: '50% 50%'
|
||||
})
|
||||
}
|
||||
if (prizesRightRef.value) {
|
||||
gsap.set(prizesRightRef.value, {
|
||||
opacity: 0,
|
||||
y: 40,
|
||||
scale: 1.08,
|
||||
'--prize-row-gap': '2rem',
|
||||
'--prize-col-gap': '2rem'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const playPrizesAnimation = () => {
|
||||
if (hasPlayedPrizesAnim.value) return
|
||||
const titleEls = [prizesTitleRef.value, prizesSubTitleRef.value].filter(
|
||||
Boolean
|
||||
) as HTMLElement[]
|
||||
const playPrizesAnimation = () => {
|
||||
if (hasPlayedPrizesAnim.value) return
|
||||
const titleEls = [prizesTitleRef.value, prizesSubTitleRef.value].filter(Boolean) as HTMLElement[]
|
||||
|
||||
const tl = gsap.timeline({ defaults: { ease: 'power2.out' } })
|
||||
if (titleEls.length) {
|
||||
tl.to(titleEls, {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
duration: 0.6,
|
||||
ease: 'back.out(1.6)',
|
||||
stagger: 0.1
|
||||
})
|
||||
}
|
||||
if (prizesRightRef.value) {
|
||||
tl.to(
|
||||
prizesRightRef.value,
|
||||
{
|
||||
'opacity': 1,
|
||||
'y': 0,
|
||||
'scale': 1,
|
||||
'--prize-row-gap': '4.2rem',
|
||||
'--prize-col-gap': '4.4rem',
|
||||
'duration': 0.55,
|
||||
'ease': 'back.out(1.4)'
|
||||
},
|
||||
titleEls.length ? '-=0.15' : 0
|
||||
)
|
||||
}
|
||||
const tl = gsap.timeline({ defaults: { ease: 'power2.out' } })
|
||||
if (titleEls.length) {
|
||||
tl.to(titleEls, {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
duration: 0.6,
|
||||
ease: 'back.out(1.6)',
|
||||
stagger: 0.1
|
||||
})
|
||||
}
|
||||
if (prizesRightRef.value) {
|
||||
tl.to(
|
||||
prizesRightRef.value,
|
||||
{
|
||||
opacity: 1,
|
||||
y: 0,
|
||||
scale: 1,
|
||||
'--prize-row-gap': '4.2rem',
|
||||
'--prize-col-gap': '4.4rem',
|
||||
duration: 0.55,
|
||||
ease: 'back.out(1.4)'
|
||||
},
|
||||
titleEls.length ? '-=0.15' : 0
|
||||
)
|
||||
}
|
||||
|
||||
hasPlayedPrizesAnim.value = true
|
||||
prizesObserver?.disconnect()
|
||||
}
|
||||
hasPlayedPrizesAnim.value = true
|
||||
prizesObserver?.disconnect()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
setupPrizesInitialState()
|
||||
if ('IntersectionObserver' in window) {
|
||||
prizesObserver = new IntersectionObserver(
|
||||
entries => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
playPrizesAnimation()
|
||||
}
|
||||
})
|
||||
},
|
||||
{ threshold: 0.25 }
|
||||
)
|
||||
if (prizesRef.value) prizesObserver.observe(prizesRef.value)
|
||||
} else {
|
||||
playPrizesAnimation()
|
||||
}
|
||||
})
|
||||
})
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
setupPrizesInitialState()
|
||||
if ('IntersectionObserver' in window) {
|
||||
prizesObserver = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
playPrizesAnimation()
|
||||
}
|
||||
})
|
||||
},
|
||||
{ threshold: 0.25 }
|
||||
)
|
||||
if (prizesRef.value) prizesObserver.observe(prizesRef.value)
|
||||
} else {
|
||||
playPrizesAnimation()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
prizesObserver?.disconnect()
|
||||
})
|
||||
onBeforeUnmount(() => {
|
||||
prizesObserver?.disconnect()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.prizes-container {
|
||||
background: url('@/assets/images/award/prizes_bg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
padding: 0 21.4rem 0 34.2rem;
|
||||
box-sizing: border-box;
|
||||
.left {
|
||||
row-gap: 3.6rem;
|
||||
.title {
|
||||
text-align: center;
|
||||
font-family: 'PoppinsBold';
|
||||
font-weight: 600;
|
||||
font-size: 4rem;
|
||||
color: #fff;
|
||||
}
|
||||
.desc {
|
||||
text-align: center;
|
||||
color: #f95750;
|
||||
font-family: 'Poppins';
|
||||
font-weight: 400;
|
||||
font-size: 3rem;
|
||||
}
|
||||
}
|
||||
.right {
|
||||
// height: 45.4rem;
|
||||
// padding: 4.6rem 6.1rem 4.6rem 0;
|
||||
box-sizing: border-box;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: repeat(2, 1fr);
|
||||
row-gap: var(--prize-row-gap, 4.2rem);
|
||||
column-gap: var(--prize-col-gap, 4.4rem);
|
||||
// flex: 1;
|
||||
.prize-item {
|
||||
width: 35.5rem;
|
||||
height: 32.8rem;
|
||||
color: #fff;
|
||||
padding: 4.5rem 0 4.8rem 0;
|
||||
justify-content: space-between;
|
||||
background: url('@/assets/images/award/first_bg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
&:nth-of-type(2) {
|
||||
background: url('@/assets/images/award/second_bg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
&:nth-of-type(3) {
|
||||
background: url('@/assets/images/award/grand_bg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
&:nth-of-type(4) {
|
||||
background: url('@/assets/images/award/certification_bg.png')
|
||||
no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
&.smaller {
|
||||
.prize-money {
|
||||
font-size: 3.6rem;
|
||||
line-height: 3.8rem;
|
||||
}
|
||||
}
|
||||
.prize-money {
|
||||
font-family: 'PoppinsBold';
|
||||
font-weight: bold;
|
||||
font-size: 4rem;
|
||||
white-space: pre-line;
|
||||
text-align: center;
|
||||
line-height: 7.6rem;
|
||||
&.smaller {
|
||||
font-size: 3.6rem;
|
||||
}
|
||||
}
|
||||
.prize-name {
|
||||
font-family: 'PoppinsMedium';
|
||||
font-weight: 500;
|
||||
font-size: 2.8rem;
|
||||
}
|
||||
.prize-desc {
|
||||
color: #e0e0e0;
|
||||
font-family: 'Arial';
|
||||
font-weight: 400;
|
||||
font-size: 2rem;
|
||||
line-height: 3rem;
|
||||
height: 8.9rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.prizes-container {
|
||||
background: url('@/assets/images/award/prizes_bg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
padding: 0 21.4rem 0 34.2rem;
|
||||
box-sizing: border-box;
|
||||
.left {
|
||||
row-gap: 3.6rem;
|
||||
.title {
|
||||
text-align: center;
|
||||
font-family: 'PoppinsBold';
|
||||
font-weight: 600;
|
||||
font-size: 4rem;
|
||||
color: #fff;
|
||||
}
|
||||
.desc {
|
||||
text-align: center;
|
||||
color: #f95750;
|
||||
font-family: 'Poppins';
|
||||
font-weight: 400;
|
||||
font-size: 3rem;
|
||||
}
|
||||
}
|
||||
.right {
|
||||
// height: 45.4rem;
|
||||
// padding: 4.6rem 6.1rem 4.6rem 0;
|
||||
box-sizing: border-box;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: repeat(2, 1fr);
|
||||
row-gap: var(--prize-row-gap, 4.2rem);
|
||||
column-gap: var(--prize-col-gap, 4.4rem);
|
||||
// flex: 1;
|
||||
.prize-item {
|
||||
width: 35.5rem;
|
||||
height: 32.8rem;
|
||||
color: #fff;
|
||||
padding: 4.5rem 0 4.8rem 0;
|
||||
justify-content: space-between;
|
||||
background: url('@/assets/images/award/first_bg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
&:nth-of-type(2) {
|
||||
background: url('@/assets/images/award/second_bg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
&:nth-of-type(3) {
|
||||
background: url('@/assets/images/award/grand_bg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
&:nth-of-type(4) {
|
||||
background: url('@/assets/images/award/certification_bg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
&.smaller {
|
||||
.prize-money {
|
||||
font-size: 3.6rem;
|
||||
line-height: 3.8rem;
|
||||
}
|
||||
}
|
||||
.prize-money {
|
||||
font-family: 'PoppinsBold';
|
||||
font-weight: bold;
|
||||
font-size: 4rem;
|
||||
white-space: pre-line;
|
||||
text-align: center;
|
||||
line-height: 7.6rem;
|
||||
&.smaller {
|
||||
font-size: 3.6rem;
|
||||
line-height: 3.8rem;
|
||||
}
|
||||
}
|
||||
.prize-name {
|
||||
font-family: 'PoppinsMedium';
|
||||
font-weight: 500;
|
||||
font-size: 2.8rem;
|
||||
}
|
||||
.prize-desc {
|
||||
color: #e0e0e0;
|
||||
font-family: 'Arial';
|
||||
font-weight: 400;
|
||||
font-size: 2rem;
|
||||
line-height: 3rem;
|
||||
height: 8.9rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.mobile {
|
||||
height: 102.8rem;
|
||||
padding: 6rem 6.6rem 0;
|
||||
background: url('@/assets/images/mobile_version_background/prizes_bg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
justify-content: flex-start;
|
||||
row-gap: 6rem;
|
||||
.left {
|
||||
row-gap: 0.8rem;
|
||||
.title {
|
||||
font-size: 3.2rem;
|
||||
}
|
||||
.desc {
|
||||
font-size: 2.4rem;
|
||||
}
|
||||
}
|
||||
.right {
|
||||
.prize-item {
|
||||
width: 29.2rem;
|
||||
height: auto;
|
||||
padding: 5rem 3rem;
|
||||
row-gap: 1.8rem;
|
||||
&,
|
||||
&.smaller .prize-money {
|
||||
font-size: 2.8rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.prize-name {
|
||||
font-size: 2.4rem;
|
||||
}
|
||||
.prize-desc {
|
||||
font-size: 2rem;
|
||||
line-height: 2.6rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&.is-pad {
|
||||
background: url('@/assets/images/pad_version/prizes-bg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
padding: 0 6rem;
|
||||
justify-content: center;
|
||||
column-gap: 10rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,27 +1,24 @@
|
||||
<template>
|
||||
<div
|
||||
class="selection-container container flex flex-col align-center"
|
||||
ref="selectionRef"
|
||||
>
|
||||
<div class="title">{{ $t('AwardsPage.selectionCriteria') }}</div>
|
||||
<!-- <img src="@/assets/images/award/bloom_logo.png" class="logo" /> -->
|
||||
<div class="sub-title">{{ $t('AwardsPage.evaluation') }}</div>
|
||||
<div class="criteria-list flex" ref="criteriaListRef">
|
||||
<div
|
||||
class="item flex flex-col align-center"
|
||||
v-for="item in criteriaList"
|
||||
:key="item.name"
|
||||
>
|
||||
<img :src="item.icon" class="icon" :style="item.style" />
|
||||
<div class="name">{{ $t(item.name) }}</div>
|
||||
<div class="desc">{{ $t(item.desc) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="selection-container container flex flex-col align-center"
|
||||
ref="selectionRef"
|
||||
:class="{ mobile: isMobile }"
|
||||
>
|
||||
<div class="title">{{ $t('AwardsPage.selectionCriteria') }}</div>
|
||||
<!-- <img src="@/assets/images/award/bloom_logo.png" class="logo" /> -->
|
||||
<div class="sub-title">{{ $t('AwardsPage.evaluation') }}</div>
|
||||
<div class="criteria-list flex" ref="criteriaListRef">
|
||||
<div class="item flex flex-col align-center" v-for="item in criteriaList" :key="item.name">
|
||||
<img :src="item.icon" class="icon" :style="item.style" />
|
||||
<div class="name">{{ $t(item.name) }}</div>
|
||||
<div class="desc">{{ $t(item.desc) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref, inject } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { gsap } from 'gsap'
|
||||
import criteria1 from '@/assets/images/award/criteria_1.png'
|
||||
@@ -31,31 +28,32 @@ import criteria4 from '@/assets/images/award/criteria_4.png'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
const isMobile = inject('isMobile')
|
||||
const criteriaList = ref([
|
||||
{
|
||||
icon: criteria1,
|
||||
name: 'AwardsPage.originality',
|
||||
desc: 'AwardsPage.originalityDesc',
|
||||
style: { width: '13rem', height: '17rem' }
|
||||
},
|
||||
{
|
||||
icon: criteria2,
|
||||
name: 'AwardsPage.creativity',
|
||||
desc: 'AwardsPage.creativityDesc',
|
||||
style: { width: '16rem', height: '18rem' }
|
||||
},
|
||||
{
|
||||
icon: criteria3,
|
||||
name: 'AwardsPage.aidaIntegration',
|
||||
desc: 'AwardsPage.aidaIntegrationDesc',
|
||||
style: { width: '16rem', height: '18rem' }
|
||||
},
|
||||
{
|
||||
icon: criteria4,
|
||||
name: 'AwardsPage.execution',
|
||||
desc: 'AwardsPage.executionDesc',
|
||||
style: { width: '18.8rem', height: '18rem' }
|
||||
}
|
||||
{
|
||||
icon: criteria1,
|
||||
name: 'AwardsPage.originality',
|
||||
desc: 'AwardsPage.originalityDesc',
|
||||
style: { width: '13rem', height: '17rem' }
|
||||
},
|
||||
{
|
||||
icon: criteria2,
|
||||
name: 'AwardsPage.creativity',
|
||||
desc: 'AwardsPage.creativityDesc',
|
||||
style: { width: '16rem', height: '18rem' }
|
||||
},
|
||||
{
|
||||
icon: criteria3,
|
||||
name: 'AwardsPage.aidaIntegration',
|
||||
desc: 'AwardsPage.aidaIntegrationDesc',
|
||||
style: { width: '16rem', height: '18rem' }
|
||||
},
|
||||
{
|
||||
icon: criteria4,
|
||||
name: 'AwardsPage.execution',
|
||||
desc: 'AwardsPage.executionDesc',
|
||||
style: { width: '18.8rem', height: '18rem' }
|
||||
}
|
||||
])
|
||||
|
||||
const selectionRef = ref<HTMLElement | null>(null)
|
||||
@@ -64,114 +62,159 @@ const hasPlayedSelectionAnim = ref(false)
|
||||
let selectionObserver: IntersectionObserver | null = null
|
||||
|
||||
const setupSelectionInitialState = () => {
|
||||
const items =
|
||||
criteriaListRef.value?.querySelectorAll<HTMLElement>('.item') ?? []
|
||||
if (items.length) {
|
||||
gsap.set(items, {
|
||||
opacity: 0,
|
||||
scale: 0,
|
||||
transformOrigin: '50% 50%'
|
||||
})
|
||||
}
|
||||
const items = criteriaListRef.value?.querySelectorAll<HTMLElement>('.item') ?? []
|
||||
if (items.length) {
|
||||
gsap.set(items, {
|
||||
opacity: 0,
|
||||
scale: 0,
|
||||
transformOrigin: '50% 50%'
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const playSelectionAnimation = () => {
|
||||
if (hasPlayedSelectionAnim.value) return
|
||||
const items =
|
||||
criteriaListRef.value?.querySelectorAll<HTMLElement>('.item') ?? []
|
||||
if (!items.length) return
|
||||
if (hasPlayedSelectionAnim.value) return
|
||||
const items = criteriaListRef.value?.querySelectorAll<HTMLElement>('.item') ?? []
|
||||
if (!items.length) return
|
||||
|
||||
gsap.to(items, {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
duration: 0.6,
|
||||
ease: 'back.out(1.6)',
|
||||
stagger: 0.3
|
||||
})
|
||||
gsap.to(items, {
|
||||
opacity: 1,
|
||||
scale: 1,
|
||||
duration: 0.6,
|
||||
ease: 'back.out(1.6)',
|
||||
stagger: 0.3
|
||||
})
|
||||
|
||||
hasPlayedSelectionAnim.value = true
|
||||
selectionObserver?.disconnect()
|
||||
hasPlayedSelectionAnim.value = true
|
||||
selectionObserver?.disconnect()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(() => {
|
||||
setupSelectionInitialState()
|
||||
if ('IntersectionObserver' in window) {
|
||||
selectionObserver = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
playSelectionAnimation()
|
||||
}
|
||||
})
|
||||
},
|
||||
{ threshold: 0.25 }
|
||||
)
|
||||
if (selectionRef.value) {
|
||||
selectionObserver.observe(selectionRef.value)
|
||||
}
|
||||
} else {
|
||||
playSelectionAnimation()
|
||||
}
|
||||
})
|
||||
nextTick(() => {
|
||||
setupSelectionInitialState()
|
||||
if ('IntersectionObserver' in window) {
|
||||
selectionObserver = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
playSelectionAnimation()
|
||||
}
|
||||
})
|
||||
},
|
||||
{ threshold: 0.25 }
|
||||
)
|
||||
if (selectionRef.value) {
|
||||
selectionObserver.observe(selectionRef.value)
|
||||
}
|
||||
} else {
|
||||
playSelectionAnimation()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
selectionObserver?.disconnect()
|
||||
selectionObserver?.disconnect()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.selection-container {
|
||||
background: url('@/assets/images/award/selection_bg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
padding-top: 9.3rem;
|
||||
.title {
|
||||
color: #fff;
|
||||
font-family: 'PoppinsBold';
|
||||
font-weight: 600;
|
||||
font-size: 4rem;
|
||||
}
|
||||
.logo {
|
||||
margin: 2.3rem 0 2.3rem;
|
||||
}
|
||||
.sub-title {
|
||||
color: #f95750;
|
||||
font-family: 'Popins';
|
||||
font-weight: 400;
|
||||
font-size: 3rem;
|
||||
margin-bottom: 11.8rem;
|
||||
}
|
||||
.criteria-list {
|
||||
column-gap: 6rem;
|
||||
.item {
|
||||
height: 44rem;
|
||||
width: 32.2rem;
|
||||
box-sizing: border-box;
|
||||
&:nth-of-type(3) {
|
||||
background: url('@/assets/images/award/criteria_bg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
.icon {
|
||||
width: 18.8rem;
|
||||
height: 18rem;
|
||||
}
|
||||
.name {
|
||||
font-family: 'PoppinsMedium';
|
||||
font-weight: 500;
|
||||
font-size: 2.8rem;
|
||||
color: #fff;
|
||||
margin: 2rem 0 5rem;
|
||||
}
|
||||
.desc {
|
||||
font-family: 'Arial';
|
||||
font-weight: 400;
|
||||
font-size: 2.4rem;
|
||||
color: #e0e0e0;
|
||||
text-align: center;
|
||||
white-space: pre-line;
|
||||
}
|
||||
}
|
||||
}
|
||||
background: url('@/assets/images/award/selection_bg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
padding-top: 9.3rem;
|
||||
.title {
|
||||
color: #fff;
|
||||
font-family: 'PoppinsBold';
|
||||
font-weight: 600;
|
||||
font-size: 4rem;
|
||||
margin-bottom: 2.4rem;
|
||||
}
|
||||
.logo {
|
||||
margin: 2.3rem 0 2.3rem;
|
||||
}
|
||||
.sub-title {
|
||||
color: #f95750;
|
||||
font-family: 'PoppinsMedium';
|
||||
font-weight: 400;
|
||||
font-size: 3rem;
|
||||
margin-bottom: 11.8rem;
|
||||
}
|
||||
.criteria-list {
|
||||
column-gap: 6rem;
|
||||
.item {
|
||||
height: 44rem;
|
||||
width: 32.2rem;
|
||||
box-sizing: border-box;
|
||||
&:nth-of-type(3) {
|
||||
background: url('@/assets/images/award/criteria_bg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
.icon {
|
||||
width: 18.8rem;
|
||||
height: 18rem;
|
||||
}
|
||||
.name {
|
||||
font-family: 'PoppinsMedium';
|
||||
font-weight: 500;
|
||||
font-size: 2.8rem;
|
||||
color: #fff;
|
||||
margin: 2rem 0 5rem;
|
||||
}
|
||||
.desc {
|
||||
font-family: 'Arial';
|
||||
font-weight: 400;
|
||||
font-size: 2rem;
|
||||
color: #e0e0e0;
|
||||
text-align: center;
|
||||
white-space: pre-line;
|
||||
line-height: 3rem;
|
||||
width: 28.2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.mobile {
|
||||
height: auto;
|
||||
padding: 6rem 0 13rem;
|
||||
background: url('@/assets/images/mobile_version_background/section_bg.png') no-repeat;
|
||||
background-size: cover;
|
||||
.title {
|
||||
font-size: 3.2rem;
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
.sub-title {
|
||||
font-size: 2.4rem;
|
||||
margin-bottom: 6rem;
|
||||
}
|
||||
.criteria-list {
|
||||
// flex-wrap: wrap;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
grid-template-rows: repeat(2, 1fr);
|
||||
grid-column-gap: 6rem;
|
||||
grid-row-gap: 7rem;
|
||||
.item {
|
||||
width: initial;
|
||||
height: initial;
|
||||
|
||||
&:nth-of-type(3) {
|
||||
width: 30rem;
|
||||
height: auto;
|
||||
padding: 3.2rem 0;
|
||||
}
|
||||
.icon {
|
||||
width: 16rem;
|
||||
height: 15.3rem;
|
||||
}
|
||||
.name {
|
||||
font-size: 2.4rem;
|
||||
margin: 0.8rem 0 1.2rem;
|
||||
}
|
||||
.desc {
|
||||
width: 23rem;
|
||||
line-height: 2.4rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,156 +1,282 @@
|
||||
<template>
|
||||
<div
|
||||
class="blocks-list flex"
|
||||
ref="root"
|
||||
:class="{ 'in-view': inView }"
|
||||
>
|
||||
<div
|
||||
class="block-item flex flex-col flex-center"
|
||||
v-for="(item, idx) in blocksList"
|
||||
:key="item.number"
|
||||
:style="{ '--delay': `${idx * 0.18}s` }"
|
||||
>
|
||||
<div class="number">{{ $t(item.number) }}</div>
|
||||
<div class="label">{{ $t(item.label) }}</div>
|
||||
<div class="line"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="blocks-list flex" ref="root" :class="{ 'in-view': inView, mobile: isMobile }">
|
||||
<div
|
||||
class="block-item flex flex-col flex-center"
|
||||
v-for="(item, idx) in blocksList"
|
||||
:key="item.number"
|
||||
:style="{ '--delay': `${idx * 0.18}s` }"
|
||||
>
|
||||
<div class="number">{{ $t(item.number) }}</div>
|
||||
<div class="label">{{ $t(item.label) }}</div>
|
||||
<div class="line"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, onMounted, onUnmounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { computed, ref, onMounted, onUnmounted, inject } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { t } = useI18n()
|
||||
const isMobile = inject('isMobile')
|
||||
const blocksList = ref([
|
||||
{
|
||||
number: 'AwardsPage.totalCashPrizes',
|
||||
label: 'AwardsPage.totalCashPrizesLabel'
|
||||
},
|
||||
{
|
||||
number: 'AwardsPage.globalMediaExpose',
|
||||
label: 'AwardsPage.globalMediaExposeLabel'
|
||||
},
|
||||
{
|
||||
number: 'AwardsPage.networkingOpportunities',
|
||||
label: 'AwardsPage.networkingOpportunitiesLabel'
|
||||
},
|
||||
{
|
||||
number: 'AwardsPage.awardCeremonyHongKong',
|
||||
label: 'AwardsPage.awardCeremonyLabel'
|
||||
}
|
||||
])
|
||||
const root = ref<HTMLElement | null>(null)
|
||||
const inView = ref(false)
|
||||
let io: IntersectionObserver | null = null
|
||||
|
||||
const blocksList = ref([
|
||||
{
|
||||
number: 'AwardsPage.totalCashPrizes',
|
||||
label: 'AwardsPage.totalCashPrizesLabel'
|
||||
},
|
||||
{
|
||||
number: 'AwardsPage.globalMediaExpose',
|
||||
label: 'AwardsPage.globalMediaExposeLabel'
|
||||
},
|
||||
{
|
||||
number: 'AwardsPage.networkingOpportunities',
|
||||
label: 'AwardsPage.networkingOpportunitiesLabel'
|
||||
},
|
||||
{
|
||||
number: 'AwardsPage.awardCeremonyHongKong',
|
||||
label: 'AwardsPage.awardCeremonyLabel'
|
||||
}
|
||||
])
|
||||
const root = ref<HTMLElement | null>(null)
|
||||
const inView = ref(false)
|
||||
let io: IntersectionObserver | null = null
|
||||
onMounted(() => {
|
||||
io = new IntersectionObserver(
|
||||
(entries) => {
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting) {
|
||||
// 延迟 0.5s 后触发动画并断开观察
|
||||
setTimeout(() => {
|
||||
inView.value = true
|
||||
}, 500)
|
||||
if (io) {
|
||||
io.disconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ threshold: 0.05 }
|
||||
)
|
||||
if (root.value) {
|
||||
io.observe(root.value)
|
||||
}
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
io = new IntersectionObserver(
|
||||
entries => {
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting) {
|
||||
// 延迟 0.5s 后触发动画并断开观察
|
||||
setTimeout(() => {
|
||||
inView.value = true
|
||||
}, 500)
|
||||
if (io) {
|
||||
io.disconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ threshold: 0.05 }
|
||||
)
|
||||
if (root.value) {
|
||||
io.observe(root.value)
|
||||
}
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
io?.disconnect()
|
||||
})
|
||||
onUnmounted(() => {
|
||||
io?.disconnect()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.blocks-list {
|
||||
height: 31.4rem;
|
||||
background: linear-gradient(98.55deg, #232323 18.22%, #898989 101.1%);
|
||||
.blocks-list {
|
||||
height: 31.4rem;
|
||||
background: linear-gradient(98.55deg, #232323 18.22%, #898989 101.1%);
|
||||
|
||||
.block-item {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
color: #fff;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
white-space: pre-line;
|
||||
row-gap: 3rem;
|
||||
/* text scale-in animations */
|
||||
.number {
|
||||
font-size: 3.6rem;
|
||||
font-family: 'PoppinsBold';
|
||||
font-weight: 600;
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
.label {
|
||||
font-size: 2.4rem;
|
||||
font-family: 'Arial';
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.05em;
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
/* vertical line grows top -> bottom */
|
||||
.line {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
/* 固定 top 为最终高度的一半位置,这样 height 从 0 -> 27.4rem 时会从上向下增长 */
|
||||
top: calc(50% - 13.7rem);
|
||||
width: 0.1rem;
|
||||
height: 0;
|
||||
background-color: #8d8d8d;
|
||||
will-change: height;
|
||||
}
|
||||
}
|
||||
}
|
||||
.block-item {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
color: #fff;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
white-space: pre-line;
|
||||
row-gap: 3rem;
|
||||
/* text scale-in animations */
|
||||
.number {
|
||||
font-size: 3.6rem;
|
||||
font-family: 'PoppinsBold';
|
||||
font-weight: 600;
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
.label {
|
||||
font-size: 2.4rem;
|
||||
font-family: 'Arial';
|
||||
font-weight: 400;
|
||||
letter-spacing: 0.05em;
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
/* vertical line grows top -> bottom */
|
||||
.line {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
/* 固定 top 为最终高度的一半位置,这样 height 从 0 -> 27.4rem 时会从上向下增长 */
|
||||
top: calc(50% - 13.7rem);
|
||||
width: 0.1rem;
|
||||
height: 0;
|
||||
background-color: #8d8d8d;
|
||||
will-change: height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* 当组件进入视口并且等待 0.5s 后,.in-view 会加入根节点,下面规则触发动画 */
|
||||
.in-view .block-item .number {
|
||||
animation: scaleIn 0.48s cubic-bezier(0.2, 0.9, 0.2, 1) forwards;
|
||||
animation-delay: var(--delay);
|
||||
}
|
||||
/* 当组件进入视口并且等待 0.5s 后,.in-view 会加入根节点,下面规则触发动画 */
|
||||
.in-view .block-item .number {
|
||||
animation: scaleIn 0.48s cubic-bezier(0.2, 0.9, 0.2, 1) forwards;
|
||||
animation-delay: var(--delay);
|
||||
}
|
||||
|
||||
.in-view .block-item .label {
|
||||
animation: scaleIn 0.48s cubic-bezier(0.2, 0.9, 0.2, 1) forwards;
|
||||
animation-delay: calc(var(--delay) + 0.12s);
|
||||
}
|
||||
.in-view .block-item .label {
|
||||
animation: scaleIn 0.48s cubic-bezier(0.2, 0.9, 0.2, 1) forwards;
|
||||
animation-delay: calc(var(--delay) + 0.12s);
|
||||
}
|
||||
|
||||
.in-view .block-item .line {
|
||||
animation: growLine 0.7s cubic-bezier(0.2, 0.9, 0.2, 1) forwards;
|
||||
animation-delay: calc(var(--delay) + 0.18s);
|
||||
}
|
||||
.in-view .block-item .line {
|
||||
animation: growLine 0.7s cubic-bezier(0.2, 0.9, 0.2, 1) forwards;
|
||||
animation-delay: calc(var(--delay) + 0.18s);
|
||||
}
|
||||
|
||||
/* keyframes */
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
/* keyframes */
|
||||
@keyframes scaleIn {
|
||||
from {
|
||||
transform: scale(0);
|
||||
opacity: 0;
|
||||
}
|
||||
to {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes growLine {
|
||||
from {
|
||||
height: 0;
|
||||
}
|
||||
to {
|
||||
height: 27.4rem;
|
||||
}
|
||||
}
|
||||
@keyframes growLine {
|
||||
from {
|
||||
height: 0;
|
||||
}
|
||||
to {
|
||||
height: 27.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* 移动端 <= 1200px 四宫格布局 */
|
||||
.mobile {
|
||||
.blocks-list {
|
||||
height: auto;
|
||||
flex-wrap: wrap;
|
||||
padding: 2rem 1.5rem;
|
||||
column-gap: 0;
|
||||
row-gap: 0;
|
||||
|
||||
.block-item {
|
||||
flex: none;
|
||||
width: 50%;
|
||||
height: 18rem;
|
||||
padding: 2rem 1rem;
|
||||
box-sizing: border-box;
|
||||
|
||||
.number {
|
||||
font-size: 2.8rem;
|
||||
}
|
||||
|
||||
.label {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.line {
|
||||
display: none;
|
||||
}
|
||||
&:nth-child(1)::after,
|
||||
&:nth-child(2)::after,
|
||||
&:nth-child(3)::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
background-color: #8d8d8d;
|
||||
transform: scaleX(0);
|
||||
}
|
||||
/* 第一行的两个item底部需要分隔线 */
|
||||
&:nth-child(1)::after,
|
||||
&:nth-child(2)::after {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 0.1rem;
|
||||
}
|
||||
|
||||
/* 第一个item右侧需要分隔线 */
|
||||
&:nth-child(1)::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
width: 0.1rem;
|
||||
background-color: #8d8d8d;
|
||||
transform: scaleY(0);
|
||||
}
|
||||
|
||||
&:nth-child(4)::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 0.1rem;
|
||||
height: 100%;
|
||||
background-color: #8d8d8d;
|
||||
transform: translateX(-100%) scaleY(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
.in-view {
|
||||
.block-item {
|
||||
&:nth-child(1)::before {
|
||||
transform-origin: bottom;
|
||||
transform: scaleY(0);
|
||||
animation: growUp 1s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||
// animation-delay: calc(var(--delay) + 0.5s);
|
||||
}
|
||||
&:nth-child(1)::after {
|
||||
transform-origin: right;
|
||||
transform: scaleX(0);
|
||||
animation: growRight 1s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||
// animation-delay: calc(var(--delay) + 0.5s);
|
||||
}
|
||||
&:nth-child(2)::after {
|
||||
transform-origin: left;
|
||||
transform: scaleX(0);
|
||||
animation: growLeft 1s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||
// animation-delay: calc(var(--delay) + 0.5s);
|
||||
}
|
||||
&:nth-child(4)::before {
|
||||
transform: translateX(-100%) scaleY(0);
|
||||
transform-origin: top;
|
||||
animation: growDown 1s cubic-bezier(0.4, 0, 0.2, 1) forwards;
|
||||
// animation-delay: calc(var(--delay) + 0.5s);
|
||||
}
|
||||
}
|
||||
}
|
||||
@keyframes growUp {
|
||||
0% {
|
||||
transform: scaleY(0);
|
||||
}
|
||||
100% {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
}
|
||||
@keyframes growDown {
|
||||
0% {
|
||||
transform: translateX(-100%) scaleY(0);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(-100%) scaleY(1);
|
||||
}
|
||||
}
|
||||
@keyframes growLeft {
|
||||
0% {
|
||||
transform: scaleX(0);
|
||||
}
|
||||
100% {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
}
|
||||
@keyframes growRight {
|
||||
0% {
|
||||
transform: scaleX(0);
|
||||
}
|
||||
100% {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
140
src/views/AwardPage/components/Step.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<nav
|
||||
class="step-bar flex align-center space-between"
|
||||
role="tablist"
|
||||
:aria-label="t('AwardApply.applicationForm')"
|
||||
>
|
||||
<button
|
||||
v-for="(key, index) in tabKeys"
|
||||
:key="key"
|
||||
type="button"
|
||||
class="step-item"
|
||||
:class="{ active: modelValue === index }"
|
||||
role="tab"
|
||||
:aria-selected="modelValue === index"
|
||||
:tabindex="modelValue === index ? 0 : -1"
|
||||
@click="onSelect(index)"
|
||||
>
|
||||
<div class="step-cluster flex flex-center">
|
||||
<div class="step-head">
|
||||
<span class="step-badge">{{ index + 1 }}</span>
|
||||
<span class="step-label">{{ t(`AwardApply.${key}`) }}</span>
|
||||
</div>
|
||||
<div class="step-indicator" aria-hidden="true">
|
||||
<span v-if="modelValue === index" class="step-indicator-bar" />
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { useI18n } from 'vue-i18n'
|
||||
|
||||
const tabKeys = ['stepTabVerify', 'stepTabProfile', 'stepTabDesign', 'stepTabTerms'] as const
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
modelValue?: number
|
||||
}>(),
|
||||
{ modelValue: 0 }
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [index: number]
|
||||
change: [index: number]
|
||||
}>()
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
function onSelect(index: number) {
|
||||
if (props.modelValue === index) return
|
||||
emit('update:modelValue', index)
|
||||
emit('change', index)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
@active-red: #c7342c;
|
||||
@label-active: #585858;
|
||||
@label-inactive: #9e9e9e;
|
||||
@badge-inactive-bg: #dcdcdc;
|
||||
@badge-inactive-text: #9f9f9f;
|
||||
@bar-height: 0.6rem;
|
||||
|
||||
.step-bar {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background: #fff;
|
||||
border-bottom: 0.1rem solid #dcdcdc;
|
||||
padding: 0 3.8rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.step-item {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin: 0;
|
||||
border: none;
|
||||
background: transparent;
|
||||
position: relative;
|
||||
height: 100%;
|
||||
|
||||
&:focus-visible {
|
||||
outline: 2px solid @active-red;
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.step-cluster {
|
||||
height: 100%;
|
||||
.step-head {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 1.2rem;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.step-badge {
|
||||
font-family: 'Poppins';
|
||||
font-weight: 400;
|
||||
font-size: 2.4rem;
|
||||
color: #9f9f9f;
|
||||
}
|
||||
|
||||
.step-label {
|
||||
font-family: 'Poppins';
|
||||
font-weight: 400;
|
||||
font-size: 2.4rem;
|
||||
color: @label-inactive;
|
||||
white-space: nowrap;
|
||||
color: #9f9f9f;
|
||||
}
|
||||
.step-indicator {
|
||||
width: 100%;
|
||||
height: @bar-height;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
.step-indicator-bar {
|
||||
display: block;
|
||||
height: @bar-height;
|
||||
width: 100%;
|
||||
// min-width: 0;
|
||||
background: @active-red;
|
||||
border-radius: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.active {
|
||||
.step-badge,
|
||||
.step-label {
|
||||
color: #c7342c;
|
||||
font-family: 'PoppinsBold';
|
||||
font-weight: 700;
|
||||
// background: @active-red;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,370 +1,528 @@
|
||||
<template>
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="timeline-container container flex flex-col align-center"
|
||||
>
|
||||
<div class="timeline-title">{{ $t('AwardsPage.competitionTimeline') }}</div>
|
||||
<div class="desc">{{ $t('AwardsPage.shapingTheFuture') }}</div>
|
||||
<div
|
||||
class="timeline-point"
|
||||
ref="timelineRef"
|
||||
>
|
||||
<!-- 顶部标签行 -->
|
||||
<div class="grid-row labels-row">
|
||||
<div
|
||||
class="grid-cell label-cell"
|
||||
v-for="item in points"
|
||||
:key="'label-' + item.time"
|
||||
>
|
||||
<div class="main-label">{{ $t(item.label) }}</div>
|
||||
<div
|
||||
class="sub-label"
|
||||
v-if="item.subLabel"
|
||||
>
|
||||
{{ $t(item.subLabel) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 图标行 -->
|
||||
<div class="grid-row icons-row">
|
||||
<div class="timeline-line"></div>
|
||||
<div
|
||||
class="grid-cell icon-cell"
|
||||
v-for="item in points"
|
||||
:key="'icon-' + item.time"
|
||||
>
|
||||
<img
|
||||
src="@/assets/images/award/point.png"
|
||||
class="point-icon"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 时间行 -->
|
||||
<div class="grid-row times-row">
|
||||
<div
|
||||
class="grid-cell time-cell"
|
||||
v-for="item in points"
|
||||
:key="'time-' + item.time"
|
||||
>
|
||||
{{ $t(item.time) }}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
ref="containerRef"
|
||||
class="timeline-container container flex flex-col align-center"
|
||||
:class="{ mobile: isMobile && !isPad, vertical: isMobile && !isPad, 'is-pad': isPad }"
|
||||
>
|
||||
<div class="timeline-title">{{ $t('AwardsPage.competitionTimeline') }}</div>
|
||||
<div class="desc">{{ $t('AwardsPage.shapingTheFuture') }}</div>
|
||||
|
||||
<!-- 描述行 -->
|
||||
<div class="grid-row descs-row">
|
||||
<div
|
||||
class="grid-cell desc-cell"
|
||||
v-for="item in points"
|
||||
:key="'desc-' + item.time"
|
||||
>
|
||||
<div class="txt">
|
||||
{{ $t(item.desc) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 纵向时间线:移动端(排除平板) -->
|
||||
<div
|
||||
v-if="isMobile && !isPad"
|
||||
class="timeline-point timeline-vertical flex flex-col"
|
||||
ref="timelineRef"
|
||||
>
|
||||
<div class="vertical-line"></div>
|
||||
<div v-for="(item, index) in points" :key="'vertical-' + item.time" class="vertical-item">
|
||||
<div class="vertical-node">
|
||||
<img src="@/assets/images/award/point.png" class="point-icon" alt="" />
|
||||
</div>
|
||||
<div class="vertical-content">
|
||||
<div class="vertical-time">{{ $t(item.time) }}</div>
|
||||
<div class="vertical-label">
|
||||
{{ $t(item.label) }}{{ locale === 'ENGLISH' ? ' ' + $t(item.subLabel) : $t(item.subLabel) }}
|
||||
</div>
|
||||
<div class="vertical-desc">
|
||||
{{ locale === 'CHINESE_SIMPLIFIED' ? $t(item.desc).replace(/\n/g, '') : $t(item.desc) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 横向时间线:桌面端 -->
|
||||
<div v-else class="timeline-point" ref="timelineRef">
|
||||
<!-- 顶部标签行 -->
|
||||
<div class="grid-row labels-row">
|
||||
<div class="grid-cell label-cell" v-for="item in points" :key="'label-' + item.time">
|
||||
<div class="main-label">{{ $t(item.label) }}</div>
|
||||
<div class="sub-label" v-if="item.subLabel">
|
||||
{{ $t(item.subLabel) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- 图标行 -->
|
||||
<div class="grid-row icons-row">
|
||||
<div class="timeline-line"></div>
|
||||
<div class="grid-cell icon-cell" v-for="item in points" :key="'icon-' + item.time">
|
||||
<img src="@/assets/images/award/point.png" class="point-icon" alt="" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- 时间行 -->
|
||||
<div class="grid-row times-row">
|
||||
<div class="grid-cell time-cell" v-for="item in points" :key="'time-' + item.time">
|
||||
{{ $t(item.time) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 描述行 -->
|
||||
<div class="grid-row descs-row">
|
||||
<div class="grid-cell desc-cell" v-for="item in points" :key="'desc-' + item.time">
|
||||
<div class="txt">
|
||||
{{ $t(item.desc) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { gsap } from 'gsap'
|
||||
import { nextTick, onBeforeUnmount, onMounted, ref, computed, inject } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { gsap } from 'gsap'
|
||||
|
||||
const { t } = useI18n()
|
||||
const { locale } = useI18n()
|
||||
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const timelineRef = ref<HTMLElement | null>(null)
|
||||
const hasAnimated = ref(false)
|
||||
const isMobile = inject<boolean>('isMobile')
|
||||
const isPad = inject<boolean>('isPad')
|
||||
const windowWidth = ref(typeof window !== 'undefined' ? window.innerWidth : 1201)
|
||||
|
||||
const points = ref([
|
||||
{
|
||||
label: 'AwardsPage.timelineApplicationLabel',
|
||||
subLabel: 'AwardsPage.timelineDeadlineLabel',
|
||||
time: 'AwardsPage.timeJul15',
|
||||
desc: 'AwardsPage.applicationDeadlineDesc'
|
||||
},
|
||||
{
|
||||
label: 'AwardsPage.twentyFinalistsAnnounced',
|
||||
subLabel: 'AwardsPage.announcedLabel',
|
||||
time: 'AwardsPage.timeAug30',
|
||||
desc: 'AwardsPage.twentyFinalistsDesc'
|
||||
},
|
||||
{
|
||||
label: 'AwardsPage.finalistSubmission',
|
||||
subLabel: 'AwardsPage.submissionLabel',
|
||||
time: 'AwardsPage.timeSept30',
|
||||
desc: 'AwardsPage.finalistSubmissionDesc'
|
||||
},
|
||||
{
|
||||
label: 'AwardsPage.receivingOutfits',
|
||||
subLabel: 'AwardsPage.fromFinalistsLabel',
|
||||
time: 'AwardsPage.timeOctober',
|
||||
desc: 'AwardsPage.receivingOutfitsDesc'
|
||||
},
|
||||
{
|
||||
label: 'AwardsPage.awardCeremony',
|
||||
subLabel: 'AwardsPage.ceremonyLabel',
|
||||
time: 'AwardsPage.timeNov12',
|
||||
desc: 'AwardsPage.awardCeremonyDesc'
|
||||
}
|
||||
])
|
||||
const isMobileOrNarrow = computed(() => isMobile.value && !isPad.value)
|
||||
|
||||
const playAnimation = () => {
|
||||
if (!containerRef.value || hasAnimated.value) return
|
||||
const title = containerRef.value.querySelector('.timeline-title')
|
||||
const subtitle = containerRef.value.querySelector('.desc')
|
||||
const line = containerRef.value.querySelector('.timeline-line')
|
||||
const timeline = containerRef.value.querySelector('.timeline-point')
|
||||
const updateWindowWidth = () => {
|
||||
windowWidth.value = window.innerWidth
|
||||
}
|
||||
|
||||
const tl = gsap.timeline()
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
const timelineRef = ref<HTMLElement | null>(null)
|
||||
const hasAnimated = ref(false)
|
||||
|
||||
// 我们使用一个统一的开始 label,使横线、timeline 裁剪与所有文字同时启动,
|
||||
// 点图标在它们完成后立即开始。
|
||||
tl.addLabel('start')
|
||||
const points = ref([
|
||||
{
|
||||
label: 'AwardsPage.timelineApplicationLabel',
|
||||
subLabel: 'AwardsPage.timelineDeadlineLabel',
|
||||
time: 'AwardsPage.timeJul15',
|
||||
desc: 'AwardsPage.applicationDeadlineDesc'
|
||||
},
|
||||
{
|
||||
label: 'AwardsPage.twentyFinalistsAnnounced',
|
||||
subLabel: 'AwardsPage.announcedLabel',
|
||||
time: 'AwardsPage.timeAug30',
|
||||
desc: 'AwardsPage.twentyFinalistsDesc'
|
||||
},
|
||||
{
|
||||
label: 'AwardsPage.finalistSubmission',
|
||||
subLabel: 'AwardsPage.submissionLabel',
|
||||
time: 'AwardsPage.timeSept30',
|
||||
desc: 'AwardsPage.finalistSubmissionDesc'
|
||||
},
|
||||
{
|
||||
label: 'AwardsPage.receivingOutfits',
|
||||
subLabel: 'AwardsPage.fromFinalistsLabel',
|
||||
time: 'AwardsPage.timeOctober',
|
||||
desc: 'AwardsPage.receivingOutfitsDesc'
|
||||
},
|
||||
{
|
||||
label: 'AwardsPage.awardCeremony',
|
||||
subLabel: 'AwardsPage.ceremonyLabel',
|
||||
time: 'AwardsPage.timeNov12',
|
||||
desc: 'AwardsPage.awardCeremonyDesc'
|
||||
}
|
||||
])
|
||||
|
||||
// 整体 timeline 的裁剪展开(与 start 同步)
|
||||
if (timeline) {
|
||||
tl.fromTo(
|
||||
timeline,
|
||||
{
|
||||
clipPath: 'inset(0 100% 0 0)'
|
||||
},
|
||||
{
|
||||
clipPath: 'inset(0 0% 0 0)',
|
||||
duration: 1.3,
|
||||
ease: 'power1.out'
|
||||
},
|
||||
'start'
|
||||
)
|
||||
}
|
||||
const playAnimation = () => {
|
||||
if (!containerRef.value || hasAnimated.value) return
|
||||
const title = containerRef.value.querySelector('.timeline-title')
|
||||
const subtitle = containerRef.value.querySelector('.desc')
|
||||
const timeline = containerRef.value.querySelector('.timeline-point')
|
||||
const line = containerRef.value.querySelector(
|
||||
isMobile.value && !isPad.value ? '.vertical-line' : '.timeline-line'
|
||||
)
|
||||
|
||||
// 线条动画(与 start 同步)
|
||||
if (line) {
|
||||
tl.from(
|
||||
line,
|
||||
{
|
||||
scaleX: 0,
|
||||
transformOrigin: '0% 50%',
|
||||
duration: 1.3,
|
||||
ease: 'power1.out'
|
||||
},
|
||||
'start'
|
||||
)
|
||||
}
|
||||
const tl = gsap.timeline()
|
||||
tl.addLabel('start')
|
||||
|
||||
// 标题与副标题(与 start 同步)
|
||||
if (title && subtitle) {
|
||||
tl.from(
|
||||
[title, subtitle],
|
||||
{
|
||||
scaleX: 0,
|
||||
autoAlpha: 0.5,
|
||||
transformOrigin: '50% 50%',
|
||||
duration: 0.6,
|
||||
stagger: 0.1,
|
||||
ease: 'power2.out'
|
||||
},
|
||||
'start'
|
||||
)
|
||||
}
|
||||
// 整体 timeline 的裁剪展开(仅横向使用)
|
||||
// 纵向时跳过裁剪动画,改用每个 item 从上方落下的动画
|
||||
if (timeline && !(isMobile.value && !isPad.value)) {
|
||||
tl.fromTo(
|
||||
timeline,
|
||||
{
|
||||
clipPath: 'inset(0 100% 0 0)'
|
||||
},
|
||||
{
|
||||
clipPath: 'inset(0 0% 0 0)',
|
||||
duration: 1.3,
|
||||
ease: 'power1.out'
|
||||
},
|
||||
'start'
|
||||
)
|
||||
}
|
||||
|
||||
// 行内文字(标签、时间、描述、图标)与 start 同步开始
|
||||
const textItems = containerRef.value.querySelectorAll('.grid-cell')
|
||||
if (textItems && textItems.length) {
|
||||
tl.from(
|
||||
textItems,
|
||||
{
|
||||
// autoAlpha: 0.5,
|
||||
duration: 0.7,
|
||||
stagger: 0.08,
|
||||
ease: 'power2.out'
|
||||
},
|
||||
'start'
|
||||
)
|
||||
}
|
||||
// 线条动画:横向 scaleX,纵向 scaleY
|
||||
// 纵向时线条与 item 动画同步进行
|
||||
if (line) {
|
||||
if (isMobile.value && !isPad.value) {
|
||||
tl.from(
|
||||
line,
|
||||
{
|
||||
scaleY: 0,
|
||||
transformOrigin: '0% 0%',
|
||||
duration: 1.0,
|
||||
ease: 'power1.out'
|
||||
},
|
||||
'start'
|
||||
)
|
||||
} else {
|
||||
tl.from(
|
||||
line,
|
||||
{
|
||||
scaleX: 0,
|
||||
transformOrigin: '0% 50%',
|
||||
duration: 1.3,
|
||||
ease: 'power1.out'
|
||||
},
|
||||
'start'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
hasAnimated.value = true
|
||||
}
|
||||
// 标题与副标题(与 start 同步)
|
||||
if (title && subtitle) {
|
||||
tl.from(
|
||||
[title, subtitle],
|
||||
{
|
||||
scaleX: 0,
|
||||
autoAlpha: 0.5,
|
||||
transformOrigin: '50% 50%',
|
||||
duration: 0.6,
|
||||
stagger: 0.1,
|
||||
ease: 'power2.out'
|
||||
},
|
||||
'start'
|
||||
)
|
||||
}
|
||||
|
||||
let observer: IntersectionObserver | null = null
|
||||
// 行内内容:桌面端用 .grid-cell,纵向用 .vertical-item
|
||||
// 纵向时,每个 item 从上方落下 + 渐显
|
||||
const textItems =
|
||||
isMobile.value && !isPad.value
|
||||
? containerRef.value.querySelectorAll('.vertical-item')
|
||||
: containerRef.value.querySelectorAll('.grid-cell')
|
||||
if (textItems && textItems.length) {
|
||||
if (isMobile.value && !isPad.value) {
|
||||
// 纵向:每个 item 从上方落下 + 渐显
|
||||
tl.from(
|
||||
textItems,
|
||||
{
|
||||
y: -60,
|
||||
opacity: 0,
|
||||
duration: 0.6,
|
||||
stagger: 0.15,
|
||||
ease: 'power2.out'
|
||||
},
|
||||
'start+=0.2'
|
||||
)
|
||||
} else {
|
||||
// 横向:保持原有动画
|
||||
tl.from(
|
||||
textItems,
|
||||
{
|
||||
duration: 0.7,
|
||||
stagger: 0.08,
|
||||
ease: 'power2.out'
|
||||
},
|
||||
'start'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
if (!containerRef.value) return
|
||||
observer = new IntersectionObserver(
|
||||
entries => {
|
||||
entries.forEach(entry => {
|
||||
if (entry.isIntersecting) {
|
||||
playAnimation()
|
||||
}
|
||||
})
|
||||
},
|
||||
{ threshold: 0.3 }
|
||||
)
|
||||
observer.observe(containerRef.value)
|
||||
})
|
||||
hasAnimated.value = true
|
||||
}
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (observer && containerRef.value) {
|
||||
observer.unobserve(containerRef.value)
|
||||
}
|
||||
observer = null
|
||||
})
|
||||
let observer: IntersectionObserver | null = null
|
||||
|
||||
onMounted(async () => {
|
||||
if (typeof window !== 'undefined') {
|
||||
windowWidth.value = window.innerWidth
|
||||
window.addEventListener('resize', updateWindowWidth)
|
||||
}
|
||||
await nextTick()
|
||||
if (!containerRef.value) return
|
||||
observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
playAnimation()
|
||||
}
|
||||
})
|
||||
},
|
||||
{ threshold: 0.3 }
|
||||
)
|
||||
observer.observe(containerRef.value)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.removeEventListener('resize', updateWindowWidth)
|
||||
}
|
||||
if (observer && containerRef.value) {
|
||||
observer.unobserve(containerRef.value)
|
||||
}
|
||||
observer = null
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.timeline-container {
|
||||
background: url('@/assets/images/award/timeline_bg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
position: relative;
|
||||
padding: 12.8rem 0 15.9rem;
|
||||
width: 100%;
|
||||
color: #fff;
|
||||
.timeline-title {
|
||||
font-family: 'PoppinsBold';
|
||||
font-weight: 600;
|
||||
font-size: 4rem;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.logo {
|
||||
margin: 2.4rem 0 2.2rem 0;
|
||||
}
|
||||
.desc {
|
||||
font-family: 'Arial';
|
||||
font-size: 3rem;
|
||||
font-weight: 400;
|
||||
color: #f95750;
|
||||
}
|
||||
.timeline-point {
|
||||
overflow: hidden;
|
||||
will-change: clip-path;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
margin-top: 11rem;
|
||||
padding: 0 13.8rem;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
.timeline-container {
|
||||
height: auto;
|
||||
min-height: 97rem;
|
||||
background: url('@/assets/images/award/timeline_bg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
position: relative;
|
||||
padding: 12.8rem 0 15.9rem;
|
||||
width: 100%;
|
||||
color: #fff;
|
||||
.timeline-title {
|
||||
font-family: 'PoppinsBold';
|
||||
font-weight: 600;
|
||||
font-size: 4rem;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
margin-bottom: 2.4rem;
|
||||
}
|
||||
.logo {
|
||||
margin: 2.4rem 0 2.2rem 0;
|
||||
}
|
||||
.desc {
|
||||
font-family: 'Arial';
|
||||
font-size: 3rem;
|
||||
font-weight: 400;
|
||||
color: #f95750;
|
||||
}
|
||||
.timeline-point {
|
||||
overflow: hidden;
|
||||
will-change: clip-path;
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
margin-top: 11rem;
|
||||
padding: 0 13.8rem;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
// 主网格布局:5列
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
grid-template-rows: auto auto auto auto;
|
||||
grid-column-gap: 0;
|
||||
grid-row-gap: 0;
|
||||
// 主网格布局:5列
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
grid-template-rows: auto auto auto auto;
|
||||
grid-column-gap: 0;
|
||||
grid-row-gap: 0;
|
||||
|
||||
// 所有 grid 子行的通用样式
|
||||
.grid-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
// 所有 grid 子行的通用样式
|
||||
.grid-row {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
.grid-cell {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
.grid-cell {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
min-width: 22rem;
|
||||
line-height: 3rem;
|
||||
}
|
||||
|
||||
// 图标行
|
||||
.icons-row {
|
||||
align-items: center;
|
||||
height: 6.4rem;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
margin-bottom: 1.6rem;
|
||||
// 图标行
|
||||
.icons-row {
|
||||
align-items: center;
|
||||
height: 6.4rem;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
margin-bottom: 1.6rem;
|
||||
|
||||
.timeline-line {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: -22rem;
|
||||
right: -21.2rem;
|
||||
height: 0.15rem;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(199, 52, 44, 0) 0%,
|
||||
rgba(199, 52, 44, 0.719626) 25.96%,
|
||||
#c7342c 51.44%,
|
||||
rgba(199, 52, 44, 0.762376) 75.96%,
|
||||
rgba(199, 52, 44, 0) 100%
|
||||
);
|
||||
transform: translateY(-50%);
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
.timeline-line {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: -22rem;
|
||||
right: -21.2rem;
|
||||
height: 0.15rem;
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
rgba(199, 52, 44, 0) 0%,
|
||||
rgba(199, 52, 44, 0.719626) 25.96%,
|
||||
#c7342c 51.44%,
|
||||
rgba(199, 52, 44, 0.762376) 75.96%,
|
||||
rgba(199, 52, 44, 0) 100%
|
||||
);
|
||||
transform: translateY(-50%);
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.icon-cell {
|
||||
position: relative;
|
||||
.point-icon {
|
||||
width: 6.4rem;
|
||||
height: 6.4rem;
|
||||
display: block;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
.icon-cell {
|
||||
position: relative;
|
||||
.point-icon {
|
||||
width: 6.4rem;
|
||||
height: 6.4rem;
|
||||
display: block;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 标签行
|
||||
.labels-row {
|
||||
margin-bottom: 8rem;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
.label-cell {
|
||||
flex-direction: column;
|
||||
color: #fff;
|
||||
font-family: 'PoppinsBold';
|
||||
font-weight: 600;
|
||||
font-size: 2.8rem;
|
||||
white-space: pre-line;
|
||||
justify-content: center;
|
||||
min-height: 6rem;
|
||||
|
||||
// .sub-label {
|
||||
// font-family: 'Arial';
|
||||
// font-weight: 400;
|
||||
// font-size: 1.4rem;
|
||||
// color: rgba(255, 255, 255, 0.8);
|
||||
// margin-top: 0.4rem;
|
||||
// }
|
||||
}
|
||||
}
|
||||
// 标签行
|
||||
.labels-row {
|
||||
margin-bottom: 8rem;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
.label-cell {
|
||||
flex-direction: column;
|
||||
color: #fff;
|
||||
font-family: 'PoppinsBold';
|
||||
font-weight: 600;
|
||||
font-size: 2.8rem;
|
||||
white-space: pre-line;
|
||||
justify-content: center;
|
||||
min-height: 6rem;
|
||||
|
||||
// 时间行
|
||||
.times-row {
|
||||
margin-bottom: 6rem;
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
.time-cell {
|
||||
color: #f95750;
|
||||
font-family: 'Arial';
|
||||
font-weight: 400;
|
||||
font-size: 2.8rem;
|
||||
line-height: 4.5rem;
|
||||
}
|
||||
}
|
||||
// .sub-label {
|
||||
// font-family: 'Arial';
|
||||
// font-weight: 400;
|
||||
// font-size: 1.4rem;
|
||||
// color: rgba(255, 255, 255, 0.8);
|
||||
// margin-top: 0.4rem;
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
// 描述行
|
||||
.descs-row {
|
||||
.desc-cell {
|
||||
.txt {
|
||||
font-family: 'Arial';
|
||||
font-weight: 400;
|
||||
font-size: 2rem;
|
||||
text-align: center;
|
||||
color: #e0e0e0;
|
||||
width: 100%;
|
||||
max-width: 31.2rem;
|
||||
min-height: 10.2rem;
|
||||
white-space: pre-line;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 时间行
|
||||
.times-row {
|
||||
margin-bottom: 6rem;
|
||||
z-index: 2;
|
||||
position: relative;
|
||||
.time-cell {
|
||||
color: #f95750;
|
||||
font-family: 'Arial';
|
||||
font-weight: 400;
|
||||
font-size: 2.8rem;
|
||||
line-height: 4.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
// 描述行
|
||||
.descs-row {
|
||||
.desc-cell {
|
||||
.txt {
|
||||
font-family: 'Arial';
|
||||
font-weight: 400;
|
||||
font-size: 2rem;
|
||||
text-align: center;
|
||||
color: #e0e0e0;
|
||||
width: 22rem;
|
||||
// max-width: 22rem;
|
||||
min-height: 10.2rem;
|
||||
white-space: pre-line;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&.mobile {
|
||||
height: auto;
|
||||
min-height: 127.4rem;
|
||||
padding: 6rem 7rem 6.6rem;
|
||||
background: url('@/assets/images/mobile_version_background/timeline_bg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
.timeline-vertical {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
margin-top: 4rem;
|
||||
padding: 0;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
will-change: clip-path;
|
||||
justify-content: center;
|
||||
row-gap: 6rem;
|
||||
.vertical-line {
|
||||
position: absolute;
|
||||
left: 1.5rem; // 与节点中心对齐 (3.2rem/2 - 0.1rem)
|
||||
top: 2.4rem;
|
||||
bottom: 2.4rem;
|
||||
width: 0.2rem;
|
||||
background: linear-gradient(
|
||||
180deg,
|
||||
rgba(199, 52, 44, 0) 0%,
|
||||
rgba(199, 52, 44, 0.72) 20%,
|
||||
#c7342c 50%,
|
||||
rgba(199, 52, 44, 0.76) 80%,
|
||||
rgba(199, 52, 44, 0) 100%
|
||||
);
|
||||
z-index: 1;
|
||||
transform-origin: 0% 0%;
|
||||
}
|
||||
|
||||
.vertical-item {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 2rem;
|
||||
padding-bottom: 3.2rem;
|
||||
z-index: 2;
|
||||
will-change: transform, opacity;
|
||||
|
||||
&:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.vertical-node {
|
||||
flex-shrink: 0;
|
||||
width: 3.2rem;
|
||||
height: 3.2rem;
|
||||
margin-left: 0;
|
||||
margin-top: 0.4rem;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
.point-icon {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
.vertical-content {
|
||||
flex: 1;
|
||||
text-align: left;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.vertical-time {
|
||||
font-family: 'Arial';
|
||||
font-size: 2rem;
|
||||
font-weight: 400;
|
||||
color: #f95750;
|
||||
line-height: 1.4;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.vertical-label {
|
||||
font-family: 'PoppinsBold';
|
||||
font-weight: 600;
|
||||
font-size: 2.2rem;
|
||||
color: #fff;
|
||||
line-height: 1.35;
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
.vertical-desc {
|
||||
font-family: 'Arial';
|
||||
font-size: 2rem;
|
||||
font-weight: 400;
|
||||
color: #e0e0e0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.is-pad {
|
||||
background: url('@/assets/images/pad_version/timeline_bg.png') no-repeat;
|
||||
background-size: 100% 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
1855
src/views/AwardPage/contestants.vue
Normal file
@@ -1,143 +1,216 @@
|
||||
<template>
|
||||
<div
|
||||
class="award-page"
|
||||
:class="{ 'is-zh': isZh }"
|
||||
>
|
||||
<div class="banner">
|
||||
<video
|
||||
:src="bannerUrl"
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
class="banner-video"
|
||||
playsinline
|
||||
webkit-playsinline
|
||||
x5-playsinline
|
||||
></video>
|
||||
<div
|
||||
class="submit-btn flex flex-center"
|
||||
@click="handleSubmitApplication"
|
||||
>
|
||||
<div>{{ $t('AwardsPage.submitApplication') }}</div>
|
||||
<img
|
||||
src="@/assets/images/award/arrow_right.png"
|
||||
alt=""
|
||||
class="arrow"
|
||||
/>
|
||||
<div class="ddl">{{ $t('AwardsPage.applicationDeadline') }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="award-page" :class="{ 'is-zh': isZh, mobile: isMobile }">
|
||||
<div class="banner" :class="{ mobile: isMobile, tablet: isTablet }">
|
||||
<video
|
||||
:src="bannerUrl"
|
||||
class="banner-video"
|
||||
autoplay
|
||||
muted
|
||||
loop
|
||||
playsinline
|
||||
webkit-playsinline
|
||||
x5-playsinline
|
||||
:controls="false"
|
||||
></video>
|
||||
<div
|
||||
class="submit-btn flex flex-center"
|
||||
:class="{ before: !start }"
|
||||
@click="handleSubmitApplication"
|
||||
>
|
||||
<div>{{ start ? $t('AwardsPage.submitApplication') : $t('AwardsPage.beforeStart') }}</div>
|
||||
<img v-if="start" src="@/assets/images/award/arrow_right.png" alt="" class="arrow" />
|
||||
<div class="ddl">
|
||||
{{ start ? $t('AwardsPage.applicationDeadline') : $t('AwardsPage.beforeStartDDL') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Slogan />
|
||||
<Bloom />
|
||||
<TimeLine />
|
||||
<JudgesSection />
|
||||
<PrizesSection :is-zh="isZh" />
|
||||
<ApplySection />
|
||||
<SelectionSection />
|
||||
</div>
|
||||
<Slogan />
|
||||
<Bloom />
|
||||
<TimeLine />
|
||||
<JudgesSection />
|
||||
<PrizesSection :is-zh="isZh" />
|
||||
<ApplySection />
|
||||
<SelectionSection />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import JudgesSection from './components/JudgesSection.vue'
|
||||
import SelectionSection from './components/SelectionSection.vue'
|
||||
import ApplySection from './components/ApplySection.vue'
|
||||
import PrizesSection from './components/PrizesSection.vue'
|
||||
import TimeLine from './components/TimeLine.vue'
|
||||
import Bloom from './components/Bloom.vue'
|
||||
import Slogan from './components/Slogan.vue'
|
||||
import { ref, computed, onMounted, provide, onUnmounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter, useRoute } from 'vue-router'
|
||||
import JudgesSection from './components/JudgesSection.vue'
|
||||
import SelectionSection from './components/SelectionSection.vue'
|
||||
import ApplySection from './components/ApplySection.vue'
|
||||
import PrizesSection from './components/PrizesSection.vue'
|
||||
import TimeLine from './components/TimeLine.vue'
|
||||
import Bloom from './components/Bloom.vue'
|
||||
import Slogan from './components/Slogan.vue'
|
||||
import banner from '@/assets/images/award/banner.mp4'
|
||||
import bannerZh from '@/assets/images/award/banner_chinese.mp4'
|
||||
import bannerPad from '@/assets/images/pad_version/banner_pad.mp4'
|
||||
import bannerPadZh from '@/assets/images/pad_version/banner_pad_chinese.mp4'
|
||||
import bannerMobile from '@/assets/images/mobile_version_background/banner_mobile.mp4'
|
||||
import bannerZhMobile from '@/assets/images/mobile_version_background/banner_mobile_zh.mp4'
|
||||
import { getPathByLocale, type AppLocale } from '@/lang'
|
||||
import { useIsMobile, useIsTablet } from '@/utils/isMobile'
|
||||
const { isMobile } = useIsMobile()
|
||||
const { isTablet } = useIsTablet()
|
||||
const router = useRouter()
|
||||
const route = useRoute()
|
||||
const { locale } = useI18n()
|
||||
|
||||
import banner from '@/assets/images/award/banner.mp4'
|
||||
import bannerZh from '@/assets/images/award/banner_chinese.mp4'
|
||||
provide('isMobile', isMobile)
|
||||
|
||||
const router = useRouter()
|
||||
const { locale } = useI18n()
|
||||
provide('isPad', isTablet)
|
||||
|
||||
const isZh = computed(() => {
|
||||
return locale.value === 'CHINESE_SIMPLIFIED'
|
||||
})
|
||||
const start = computed(() => {
|
||||
const targetDate = new Date('2026-05-15T00:00:00')
|
||||
const now = new Date()
|
||||
return now > targetDate
|
||||
})
|
||||
|
||||
const bannerUrl = computed(() => {
|
||||
return isZh.value ? bannerZh : banner
|
||||
})
|
||||
const isZh = computed(() => {
|
||||
return locale.value === 'CHINESE_SIMPLIFIED'
|
||||
})
|
||||
|
||||
const handleSubmitApplication = () => {
|
||||
router.push('/award/contestants')
|
||||
}
|
||||
const bannerUrl = computed(() => {
|
||||
let url = null
|
||||
if (isMobile.value) {
|
||||
url = isZh.value ? bannerZhMobile : bannerMobile
|
||||
} else if (isTablet.value) {
|
||||
url = isZh.value ? bannerPadZh : bannerPad
|
||||
} else {
|
||||
url = isZh.value ? bannerZh : banner
|
||||
}
|
||||
return url
|
||||
})
|
||||
|
||||
const handleSubmitApplication = () => {
|
||||
if (!start.value) return
|
||||
router.push({
|
||||
path: getPathByLocale('/contestants', locale.value as AppLocale),
|
||||
query: {
|
||||
...route.query
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.container {
|
||||
height: 97rem;
|
||||
}
|
||||
.container {
|
||||
height: 97rem;
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 2.4rem;
|
||||
height: 2.4rem;
|
||||
}
|
||||
.banner {
|
||||
height: 100rem;
|
||||
// background: url('@/assets/images/award/banner.png') no-repeat;
|
||||
// background-size: cover;
|
||||
position: relative;
|
||||
.banner-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.submit-btn {
|
||||
width: 41rem;
|
||||
height: 6.394rem;
|
||||
line-height: 6.394rem;
|
||||
text-align: center;
|
||||
border-radius: 3.2rem;
|
||||
background-color: rgba(35, 35, 35, 0.7);
|
||||
box-shadow: inset 0 0 1119px 0 rgba(255, 255, 255, 0.3),
|
||||
inset -0.8px -2.4px 1.6px 0.4px rgba(255, 255, 255, 0.1),
|
||||
inset 0.8px 2.4px 1.6px 0 rgba(255, 255, 255, 0.3);
|
||||
color: #fff;
|
||||
font-family: 'PoppinsBold';
|
||||
font-weight: 600;
|
||||
font-size: 2.4rem;
|
||||
column-gap: 3.2rem;
|
||||
position: absolute;
|
||||
left: 42.1rem;
|
||||
bottom: 15.7rem;
|
||||
backdrop-filter: blur(5px);
|
||||
cursor: pointer;
|
||||
.arrow {
|
||||
width: 3.83rem;
|
||||
height: 3.83rem;
|
||||
}
|
||||
.ddl {
|
||||
position: absolute;
|
||||
bottom: -4rem;
|
||||
left: 0;
|
||||
text-align: center;
|
||||
width: 41rem;
|
||||
font-family: 'ArialBold';
|
||||
font-weight: 700;
|
||||
font-size: 2rem;
|
||||
line-height: 2.2rem;
|
||||
color: #232323e5;
|
||||
}
|
||||
}
|
||||
}
|
||||
.banner {
|
||||
height: 100rem;
|
||||
// background: url('@/assets/images/award/banner.png') no-repeat;
|
||||
// background-size: cover;
|
||||
position: relative;
|
||||
.banner-video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
.submit-btn {
|
||||
width: 41rem;
|
||||
height: 6.394rem;
|
||||
line-height: 6.394rem;
|
||||
text-align: center;
|
||||
border-radius: 3.2rem;
|
||||
background-color: rgba(35, 35, 35, 0.7);
|
||||
box-shadow: inset 0 0 1119px 0 rgba(255, 255, 255, 0.3),
|
||||
inset -0.8px -2.4px 1.6px 0.4px rgba(255, 255, 255, 0.1),
|
||||
inset 0.8px 2.4px 1.6px 0 rgba(255, 255, 255, 0.3);
|
||||
color: #fff;
|
||||
font-family: 'PoppinsBold';
|
||||
font-weight: 600;
|
||||
font-size: 2.4rem;
|
||||
column-gap: 3.2rem;
|
||||
position: absolute;
|
||||
left: 42.1rem;
|
||||
bottom: 15.7rem;
|
||||
backdrop-filter: blur(5px);
|
||||
cursor: pointer;
|
||||
&.before {
|
||||
cursor: default;
|
||||
color: #a8a8a8;
|
||||
}
|
||||
.arrow {
|
||||
width: 3.83rem;
|
||||
height: 3.83rem;
|
||||
}
|
||||
.ddl {
|
||||
position: absolute;
|
||||
top: calc(100% + 1.8rem);
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
text-align: center;
|
||||
width: 41rem;
|
||||
white-space: nowrap;
|
||||
font-family: 'ArialBold';
|
||||
font-weight: 700;
|
||||
font-size: 2rem;
|
||||
line-height: 2.2rem;
|
||||
color: #232323e5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.is-zh {
|
||||
.submit-btn {
|
||||
padding: 0 7.5rem;
|
||||
height: 7.8rem;
|
||||
border-radius: 7.74rem;
|
||||
column-gap: 3.8rem;
|
||||
// justify-content: space-between;
|
||||
&,
|
||||
.ddl {
|
||||
width: 35.4rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
.is-zh {
|
||||
.submit-btn {
|
||||
padding: 0 7.5rem;
|
||||
height: 7.8rem;
|
||||
border-radius: 7.74rem;
|
||||
column-gap: 3.8rem;
|
||||
// justify-content: space-between;
|
||||
&,
|
||||
.ddl {
|
||||
width: 35.4rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.banner.mobile {
|
||||
height: auto;
|
||||
.submit-btn {
|
||||
top: 52%;
|
||||
left: 8%;
|
||||
width: 46.1rem;
|
||||
font-size: 2.6rem;
|
||||
column-gap: 1rem;
|
||||
letter-spacing: 0%;
|
||||
.arrow {
|
||||
width: 4rem;
|
||||
height: 4rem;
|
||||
}
|
||||
.ddl {
|
||||
white-space: pre-line;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
padding-left: 4rem;
|
||||
top: calc(100% + 2rem);
|
||||
left: 0;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
.banner.tablet {
|
||||
height: 136vw;
|
||||
.submit-btn {
|
||||
top: 74vw;
|
||||
left: 11vw;
|
||||
width: 46.1rem;
|
||||
.ddl {
|
||||
white-space: pre-line;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
padding-left: 4rem;
|
||||
top: calc(100% + 2rem);
|
||||
left: 0;
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
327
src/views/Preview/index.vue
Normal file
@@ -0,0 +1,327 @@
|
||||
<template>
|
||||
<div class="page-container">
|
||||
<div class="card">
|
||||
<section class="stats-section">
|
||||
<h1 class="title">{{ $t('Preview.title') }}</h1>
|
||||
<div class="count-badge">
|
||||
<span class="label">{{ $t('Preview.total') }}</span>
|
||||
<span class="number">{{ submittedCount }}</span>
|
||||
</div>
|
||||
<button v-loading="excelLoading" @click="handleDownloadAll" class="download-all-btn flex">
|
||||
<SvgIcon name="CDownload" size="22" color="#fff" />
|
||||
{{ $t('Preview.downloadExcel') }}
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<section class="download-section">
|
||||
<h2 class="subtitle">{{ $t('Preview.range') }}</h2>
|
||||
|
||||
<div class="input-group">
|
||||
<div class="field">
|
||||
<label>{{ $t('Preview.startIndex') }}</label>
|
||||
<input v-model.number="range.start" type="number" placeholder="10000" min="1" />
|
||||
</div>
|
||||
<div class="field">
|
||||
<label>{{ $t('Preview.endIndex') }}</label>
|
||||
<input v-model.number="range.end" type="number" :placeholder="maxIndex" min="1" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="tips">{{ $t('Preview.limit') }}</div>
|
||||
<button
|
||||
v-loading="fileDownloading"
|
||||
@click="handleDownload"
|
||||
:disabled="!isValid"
|
||||
class="download-btn"
|
||||
>
|
||||
<span class="btn-text">{{ $t('Preview.download') }}</span>
|
||||
</button>
|
||||
|
||||
<p v-if="errorMsg" class="status-text error">{{ $t(errorMsg, { maxIndex }) }}</p>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { Https } from '@/utils/request'
|
||||
import { debounce, subtract } from 'lodash-es'
|
||||
|
||||
// 模拟已提交人数
|
||||
const submittedCount = ref(0)
|
||||
|
||||
// 输入框绑定的数据模型
|
||||
const range = ref({
|
||||
start: null,
|
||||
end: null
|
||||
})
|
||||
const maxIndex = ref(10000)
|
||||
const handleFetchSubmittedCount = async () => {
|
||||
Https.axiosGet(Https.httpUrls.getContestCount).then((res) => {
|
||||
submittedCount.value = res.count
|
||||
maxIndex.value = res.maxContestantNumber
|
||||
})
|
||||
}
|
||||
|
||||
// 逻辑判断:区间是否合法
|
||||
const isValid = computed(() => {
|
||||
const { start, end } = range.value
|
||||
// 必须是数字,且起始>0,结束>=起始,结束不超过当前总数,相差不超过10
|
||||
return (
|
||||
start !== null &&
|
||||
end !== null &&
|
||||
start > 0 &&
|
||||
end >= start &&
|
||||
end <= maxIndex.value &&
|
||||
subtract(end, start) <= 9
|
||||
)
|
||||
})
|
||||
|
||||
// 错误提示文案
|
||||
const errorMsg = computed(() => {
|
||||
const { start, end } = range.value
|
||||
if (start === null || end === null) return ''
|
||||
if (start < 10000) return 'Preview.minIndex'
|
||||
if (end < start) return 'Preview.indexError'
|
||||
if (subtract(end, start) > 9) return 'Preview.limit'
|
||||
if (end > maxIndex.value) return `Preview.maxIndex`
|
||||
return ''
|
||||
})
|
||||
|
||||
const excelLoading = ref(false)
|
||||
const handleDownloadAll = debounce(() => {
|
||||
excelLoading.value = true
|
||||
|
||||
Https.axiosGet(Https.httpUrls.getExcel, {
|
||||
responseType: 'blob',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded'
|
||||
}
|
||||
})
|
||||
.then((res) => {
|
||||
// 创建下载链接
|
||||
|
||||
const url = window.URL.createObjectURL(new Blob([res.data]))
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.setAttribute('download', `Global_Awards_Applications_${Date.now()}.xlsx`)
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
})
|
||||
.finally(() => {
|
||||
excelLoading.value = false
|
||||
})
|
||||
}, 500)
|
||||
|
||||
const fileDownloading = ref(false)
|
||||
// 下载执行函数
|
||||
const handleDownload = debounce(() => {
|
||||
if (!isValid.value) return
|
||||
fileDownloading.value = true
|
||||
Https.axiosPost(
|
||||
Https.httpUrls.postExportFile,
|
||||
{
|
||||
minContestantNumber: range.value.start,
|
||||
maxContestantNumber: range.value.end
|
||||
},
|
||||
{ responseType: 'blob' }
|
||||
)
|
||||
.then((res) => {
|
||||
// 创建下载链接
|
||||
const url = window.URL.createObjectURL(new Blob([res.data]))
|
||||
const link = document.createElement('a')
|
||||
link.href = url
|
||||
link.setAttribute(
|
||||
'download',
|
||||
`Global_Awards_Applications_${range.value.start}_to_${range.value.end}.zip`
|
||||
)
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
})
|
||||
.finally(() => {
|
||||
fileDownloading.value = false
|
||||
})
|
||||
}, 500)
|
||||
|
||||
onMounted(() => {
|
||||
handleFetchSubmittedCount()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
/* 全局背景容器 */
|
||||
.page-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
/* 渐变背景:深邃蓝紫色调 */
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
}
|
||||
|
||||
/* 核心卡片样式 */
|
||||
.card {
|
||||
background: #ffffff;
|
||||
width: 80rem;
|
||||
padding: 4rem;
|
||||
border-radius: 2.4rem;
|
||||
box-shadow: 0 2rem 4rem rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
/* 统计区域 */
|
||||
.title {
|
||||
margin: 0 0 2.4rem 0;
|
||||
font-size: 2.4rem;
|
||||
text-align: center;
|
||||
color: #2d3436;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.count-badge {
|
||||
background: #f8f9fa;
|
||||
border: 0.2rem solid #edf2f7;
|
||||
border-radius: 1.6rem;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.count-badge .label {
|
||||
display: block;
|
||||
font-size: 1.4rem;
|
||||
color: #636e72;
|
||||
margin-bottom: 0.8rem;
|
||||
}
|
||||
|
||||
.count-badge .number {
|
||||
font-size: 4rem;
|
||||
font-weight: 900;
|
||||
color: #4834d4;
|
||||
}
|
||||
|
||||
.download-all-btn {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 1.2rem;
|
||||
background-color: #00b894;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 1rem;
|
||||
font-size: 2rem;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
column-gap: 1rem;
|
||||
margin-top: 2rem;
|
||||
.c-svg {
|
||||
width: initial;
|
||||
height: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.divider {
|
||||
height: 0.1rem;
|
||||
background: #eee;
|
||||
margin: 3.2rem 0;
|
||||
}
|
||||
|
||||
/* 表单区域 */
|
||||
.subtitle {
|
||||
font-size: 1.6rem;
|
||||
color: #2d3436;
|
||||
margin-bottom: 2rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
gap: 1.6rem;
|
||||
margin-bottom: 1.2rem;
|
||||
}
|
||||
|
||||
.field {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.field label {
|
||||
display: block;
|
||||
font-size: 1.2rem;
|
||||
color: #b2bec3;
|
||||
margin-bottom: 0.6rem;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.field input {
|
||||
width: 100%;
|
||||
padding: 1.2rem 1.6rem;
|
||||
border: 0.2rem solid #dfe6e9;
|
||||
border-radius: 1.2rem;
|
||||
font-size: 1.6rem;
|
||||
color: #2d3436;
|
||||
transition: all 0.3s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.field input:focus {
|
||||
outline: none;
|
||||
border-color: #6c5ce7;
|
||||
box-shadow: 0 0 0 4px rgba(108, 92, 231, 0.1);
|
||||
}
|
||||
.tips {
|
||||
text-align: center;
|
||||
margin-bottom: 1.2rem;
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
/* 按钮逻辑 */
|
||||
.download-btn {
|
||||
width: 100%;
|
||||
padding: 1.6rem;
|
||||
border: none;
|
||||
border-radius: 1.2rem;
|
||||
background: #6c5ce7;
|
||||
color: white;
|
||||
font-size: 1.6rem;
|
||||
font-weight: 700;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.download-btn:hover:not(:disabled) {
|
||||
background: #5849be;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 0.8rem 1.5rem rgba(108, 92, 231, 0.3);
|
||||
}
|
||||
|
||||
.download-btn:active:not(:disabled) {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.download-btn:disabled {
|
||||
background: #dfe6e9;
|
||||
color: #b2bec3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* 状态文本 */
|
||||
.status-text {
|
||||
text-align: center;
|
||||
font-size: 1.3rem;
|
||||
margin-top: 1.6rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-text.error {
|
||||
color: #ff7675;
|
||||
}
|
||||
|
||||
.status-text.success {
|
||||
color: #00b894;
|
||||
}
|
||||
</style>
|
||||
123
vite.config.ts
@@ -7,7 +7,7 @@ import Components from 'unplugin-vue-components/vite'
|
||||
import DefineOptions from 'unplugin-vue-define-options/vite'
|
||||
import { createSvgIconsPlugin } from 'vite-plugin-svg-icons'
|
||||
import path from 'path'
|
||||
import { ElementPlusResolver, AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
|
||||
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
|
||||
|
||||
// console.log(process)
|
||||
// console.log(import.meta.env.VITE_APP_URL)
|
||||
@@ -16,14 +16,14 @@ import { ElementPlusResolver, AntDesignVueResolver } from 'unplugin-vue-componen
|
||||
|
||||
export default defineConfig(({ mode }) => {
|
||||
const env = loadEnv(mode, process.cwd())
|
||||
const isProduction = mode === 'production'
|
||||
|
||||
return {
|
||||
plugins: [
|
||||
vue(),
|
||||
DefineOptions(),
|
||||
// ...
|
||||
AutoImport({
|
||||
resolvers: [ElementPlusResolver()],
|
||||
resolvers: [],
|
||||
imports: [
|
||||
'vue',
|
||||
'vue-router',
|
||||
@@ -44,17 +44,15 @@ export default defineConfig(({ mode }) => {
|
||||
'isUndefined'
|
||||
]
|
||||
}
|
||||
],
|
||||
]
|
||||
}),
|
||||
Components({
|
||||
resolvers: [ElementPlusResolver(), AntDesignVueResolver({ importStyle: false })]
|
||||
resolvers: [AntDesignVueResolver({ importStyle: false })]
|
||||
}),
|
||||
createSvgIconsPlugin({
|
||||
// 指定需要缓存的图标文件夹
|
||||
iconDirs: [path.resolve(process.cwd(), 'src/assets/icons')],
|
||||
// 指定symbolId格式
|
||||
symbolId: 'icon-[dir]-[name]',
|
||||
inject: 'body-last' // 注入位置优化
|
||||
inject: 'body-last'
|
||||
})
|
||||
],
|
||||
define: {
|
||||
@@ -67,7 +65,6 @@ export default defineConfig(({ mode }) => {
|
||||
'primary-color': '#ec6800'
|
||||
},
|
||||
javascriptEnabled: true,
|
||||
// 全局导入less变量文件
|
||||
additionalData: `@import "${path.resolve(__dirname, 'src/assets/css/style.less')}";`
|
||||
}
|
||||
}
|
||||
@@ -79,19 +76,113 @@ export default defineConfig(({ mode }) => {
|
||||
}
|
||||
},
|
||||
server: {
|
||||
host: '0.0.0.0', // 允许局域网内的IP访问
|
||||
port: 8060, // 根据环境设置端口
|
||||
open: true, // 自动打开浏览器
|
||||
strictPort: true, // 如果端口已被占用,则尝试下一个可用端口
|
||||
host: '0.0.0.0',
|
||||
port: 8088,
|
||||
open: true,
|
||||
strictPort: true,
|
||||
hmr: {
|
||||
overlay: true
|
||||
},
|
||||
proxy: {
|
||||
'/api': {
|
||||
//'/api'是自行设置的请求前缀
|
||||
target: env.VITE_APP_URL,
|
||||
changeOrigin: true, //用于控制请求头中的host值
|
||||
rewrite: (path) => path.replace(/^\/api/, '/api') //路径重写,(正则)匹配以api开头的路径为空(将请求前缀删除)
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, '/api')
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// ========== 打包配置 ==========
|
||||
build: {
|
||||
// 构建目标浏览器
|
||||
target: 'es2015',
|
||||
// 是否生成 sourcemap
|
||||
sourcemap: isProduction ? false : 'eval',
|
||||
// 打包文件输出目录
|
||||
outDir: 'dist',
|
||||
// 静态资源输出目录(相对于 outDir)
|
||||
assetsDir: 'assets',
|
||||
// 小于此阈值的导入将内联为 base64
|
||||
assetsInlineLimit: 4096,
|
||||
// 启用 CSS 代码分割
|
||||
cssCodeSplit: true,
|
||||
// 公共基础路径
|
||||
publicDir: 'public',
|
||||
// 启用 gzip 压缩大小限制
|
||||
chunkSizeWarningLimit: 500,
|
||||
// 自定义 chunk 分包策略
|
||||
rollupOptions: {
|
||||
output: {
|
||||
// 静态资源打包命名
|
||||
assetFileNames: (assetInfo) => {
|
||||
const info = assetInfo.name || ''
|
||||
if (/\.(woff|woff2?|eot|ttf|otf)$/i.test(info)) {
|
||||
return 'assets/fonts/[name]-[hash][extname]'
|
||||
}
|
||||
if (/\.(png|jpe?g|gif|svg|webp|avif|ico)$/i.test(info)) {
|
||||
return 'assets/images/[name]-[hash][extname]'
|
||||
}
|
||||
if (/\.(mp4|webm|ogg|mp3|wav|flac|aac)$/i.test(info)) {
|
||||
return 'assets/media/[name]-[hash][extname]'
|
||||
}
|
||||
return 'assets/[name]-[hash][extname]'
|
||||
},
|
||||
// chunk 分包命名
|
||||
chunkFileNames: 'assets/js/[name]-[hash].js',
|
||||
// 入口文件命名
|
||||
entryFileNames: 'assets/js/[name]-[hash].js',
|
||||
// 手动分包策略
|
||||
manualChunks: {
|
||||
// 核心框架
|
||||
'vue-core': ['vue', 'vue-router', 'pinia', 'pinia-plugin-persistedstate'],
|
||||
// 国际化
|
||||
'vue-i18n-core': ['vue-i18n'],
|
||||
// UI 框架
|
||||
'ant-design': ['ant-design-vue'],
|
||||
// 工具库
|
||||
'utils': ['lodash-es', 'axios'],
|
||||
// 其他依赖
|
||||
'vendor': ['gsap', 'crypto-js']
|
||||
}
|
||||
}
|
||||
},
|
||||
// 依赖预构建优化
|
||||
optimizeDeps: {
|
||||
include: [
|
||||
'vue',
|
||||
'vue-router',
|
||||
'pinia',
|
||||
'vue-i18n',
|
||||
'ant-design-vue',
|
||||
'axios',
|
||||
'lodash-es',
|
||||
'gsap',
|
||||
'crypto-js'
|
||||
]
|
||||
},
|
||||
// 压缩配置
|
||||
minify: 'terser',
|
||||
terserOptions: {
|
||||
compress: {
|
||||
drop_console: isProduction, // 生产环境移除 console
|
||||
drop_debugger: isProduction, // 生产环境移除 debugger
|
||||
pure_funcs: isProduction ? ['console.log'] : []
|
||||
},
|
||||
format: {
|
||||
comments: false // 移除注释
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// 预览服务器配置
|
||||
preview: {
|
||||
host: '0.0.0.0',
|
||||
port: 8088,
|
||||
open: true,
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: env.VITE_APP_URL,
|
||||
changeOrigin: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||